Using MultiBinding to calculate sum of multiple textboxes - c#

Based on this previously answered question, I'm trying to create an IMultiValueConverter that will allow the Text property of a TextBox in WPF to be bound to the sum of several other TextBox values. I've mirrored the answer to the referenced question fairly strictly, yet when testing this I get an InvalidCastException. In the code below, the lines commented out are the code from the aforementioned answer. I did try running this with the var datatype instead of using double (I dislike var, just a preference), and received the
same error in the same place. I've tried changing the style of cast in various ways, including Convert.ToInt32, (int), and even int.Parse, but everything results in the same error, same location.
Does anybody have a clue as to what the problem with this could be? This is my first real foray into binding like this, so it could be I'm fundamentally misunderstanding it, but honestly don't think that's what it is...
public class AddListRecordsConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
double result = 0.0;
try
{
double[] doubleValues = values.Cast<double>().ToArray();
foreach (var doubleValue in doubleValues)
{
result += doubleValue;
}
//var doubleValues = values.Cast<double>().ToArray();
//var leftPart = string.Join(" x ", doubleValues);
//var rightPart = doubleValues.Sum().ToString();
//var result = string.Format("{0} = {1}", leftPart, rightPart);
//return result;
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
return result;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Destination TextBox:
<TextBox x:Name="allRecords" Style="{StaticResource dataEntryTextBox}" Grid.Column="1" Grid.Row="6">
<TextBox.Text>
<MultiBinding Converter="{StaticResource AddListRecordsConverter}">
<Binding ElementName="allRecordsOne" Path="Text"></Binding>
<Binding ElementName="allRecordsTwo" Path="Text"></Binding>
</MultiBinding>
</TextBox.Text>
</TextBox>
Source TextBoxes:
<TextBox x:Name="allRecordsOne" Style="{StaticResource dataEntryTextBox}" Grid.Column="0" Grid.Row="4" GotFocus="SelectAllOnFocus_GotFocus" LostFocus="allRecords_LostFocus" />
<TextBox x:Name="allRecordsTwo" Style="{StaticResource readOnlyTextBox}" Grid.Column="0" Grid.Row="5" Text="{Binding ElementName=allRecordsOne, Path=Text}" GotFocus="SelectAllOnFocus_GotFocus" LostFocus="allRecords_LostFocus" />
<TextBox x:Name="allRecordsThree" Style="{StaticResource readOnlyTextBox}" Grid.Column="0" Grid.Row="6" Text="{Binding ElementName=allRecordsOne, Path=Text}" GotFocus="SelectAllOnFocus_GotFocus" LostFocus="allRecords_LostFocus" />

I simplified your example. Note that I used Mode="OneWay" to avoid exception in ConvertBack method.
<StackPanel>
<TextBox x:Name="allRecords">
<TextBox.Text>
<MultiBinding Converter="{StaticResource AddListRecordsConverter}">
<Binding ElementName="allRecordsOne" Path="Text" Mode="OneWay"/>
<Binding ElementName="allRecordsTwo" Path="Text" Mode="OneWay"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
<TextBox x:Name="allRecordsOne" />
<TextBox x:Name="allRecordsTwo" />
</StackPanel>
the issue with the converter is that it receives two empty strings (default value of text) as input (values) and can't handle them properly. it has to be more defensive
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
double val = 0.0;
double result = 0.0;
foreach (var txt in values)
{
if (double.TryParse(txt.ToString(), out val))
result += val;
else
return "NaN";
}
return result.ToString();
}

Related

Bind a Converter Parameter in re-usable User Control

I am trying to create a re-usable user control (for data entry) in which there are two text boxes and they are linked to each by an IValueConvertor.
The following XAML is the original, normal code. This is what I am trying to reproduce in a user control.
<WrapPanel>
<TextBlock Text="Length of Fence"/>
<TextBox Name="Metric" Width="50" Text="{Binding Path=LengthFence, Mode=TwoWay}"/>
<TextBlock Text="Meters"/>
<TextBox Text="{Binding ElementName=Metric, Path=Text, Converter={StaticResource MetersToInches}, StringFormat=N8}"/>
<TextBlock Text="Inches"/>
</WrapPanel>
and the code-behind for the IValueConvertor (in MainWindow.xaml) is
public class MetersToInches : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.ToString() == "")
return 0.0;
try
{
double meters = System.Convert.ToDouble(value);
var result = meters * 39.3701;
return result;
}
catch
{
// Catch errors when users type invalid expressions.
return 0.0;
}
}
public object ConvertBack(object value, Type targettype, object parameter, CultureInfo culture)
{
if (value.ToString() == "")
return 0.0;
try
{
double inches = System.Convert.ToDouble(value);
var result = inches * 0.0254;
return result;
}
catch
{
// Catch errors when users type invalid expressions.
return 0.0;
}
}
}
This is what this XAML looks like:
Now I have made a re-usable UserControl with three dependency properties Label for label string, Value for binding a property inside the ViewModel, and Units - a string property to show the input units.
<UserControl ...
x:Name="parent">
<StackPanel DataContext="{Binding ElementName=parent}">
<TextBlock Text="{Binding Path=Label}"/>
<TextBox Text="{Binding Path=Value}"/>
<TextBlock Text="{Binding Path=Units}"/>
</StackPanel>
However, this re-usable control can only tackle the first TextBox of the input. I do not know how to bind the IValueConvertor in the second TextBox. I need to do this because I want to bind other converters such as meters to feet, kg to pound, etc.
I have read that ConvertorParameter cannot be bound because it is not a dependency property and I am not sure if I can use multi-binding, mostly because I do not know how to use it properly Binding ConverterParameter.
I would be very grateful if you could show me how to do this or direct me to the appropriate link on StackOverflow or elsewhere that solves this problem. Or if there is a better way of doing this.
Many many thanks in advance.
First, don't bind the TextBoxes to each other (as in your original code at the begining of the question), instead, bind each TextBox to the same backing property, which, in your UserControl, is Value.
As for how to implement multiple bindings, you probably don't need a MultiBinding.
We have to pick a "standard" unit of measure to begin with- this will be the unit that will be actually stored in the property and in any database or file. I'll assume this standard unit will be meters (m). An IValueConverter can be used to convert between meters and some other unit of distance and back, using the ConverterParameter to specify which other unit to convert to/from.
Here's a good example to get you started.
public enum DistanceUnit { Meter, Foot, Inch, }
public class DistanceUnitConverter : IValueConverter
{
private static Dictionary<DistanceUnit, double> conversions = new Dictionary<DistanceUnit, double>
{
{ DistanceUnit.Meter, 1 },
{ DistanceUnit.Foot, 3.28084 },
{ DistanceUnit.Inch, 39.37008 }
};
//Converts a meter into another unit
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return conversions[(DistanceUnit)parameter] * (double)value;
}
//Converts some unit into a meter
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null) { return 0; }
double v;
var s = value as string;
if (s == null)
{
v = (double)value;
}
else
{
if (s == string.Empty) { return 0; }
v = double.Parse(s);
}
if (v == 0) { return 0; }
return v / conversions[((DistanceUnit)parameter)];
}
}
The above has a few problems. I never check if parameter really is a DistanceUnit before using it, for example. But it works.
Here's an example of how I used it:
<StackPanel>
<StackPanel.Resources>
<local:DistanceUnitConverter x:Key="DistCon"/>
</StackPanel.Resources>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Distance, Converter={StaticResource DistCon}, ConverterParameter={x:Static local:DistanceUnit.Meter}}" MinWidth="20"/>
<TextBlock>m</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Distance, Converter={StaticResource DistCon}, ConverterParameter={x:Static local:DistanceUnit.Foot}}" MinWidth="20"/>
<TextBlock>ft</TextBlock>
</StackPanel>
</StackPanel>
The DistanceUnit enum and the internal conversions dictionary can be expanded with more units of measure. Alternatively, you can use a 3rd party library that already has all these included, like UnitsNet.
Not sure how you would like to bind mulitple converters in one single control. If i'm not wrong, you would like to build a control where when a user enters a particular value, you need to display it in different units. If this is the case, you can create a single converter with converterparameter as "m","cm","inch" etc and based on this you can return the result. Then in this case, you will have 4,5 controls and each will have same converter binding but different converter values. If this is not clear and you need further direction, please let know.
Multi Value binding
To answer your point 6, please see a sample multi binding converter and its implementation in xaml below. I have built a simple RolesFilter which will take different inputs from the xaml as object[] and since I already know what data is expected, i'm converting them in the converter.
public class RolesFilter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
try
{
FlipperObservableCollection<Role> _roles = (FlipperObservableCollection<Role>)values[0]; //Input
Department _dept_param = values[1] as Department;
bool _filter = (bool)values[2];
string _id = "NA";
if (values.Count() == 4 && values[3] is string) _id = (string)values[3] ?? "NA";
//If we need a filter, then without department, it should return empty results
if (!_filter) return _roles; //If no filter is required, then don't worry, go ahead with input values.
if (_dept_param == null) return new FlipperObservableCollection<Role>(); //If department is null, then
List<Role> _filtered_list = _roles.ToList().Where(p => p.department.id == _dept_param.id && p.id != _id)?.ToList() ?? new List<Role>();
return new FlipperObservableCollection<Role>(_filtered_list);
}
catch (Exception)
{
throw;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I'm using the multi value converter in the xaml as below. Here, i'm filtering an itemsource of a combo box based on another combobox and a check box. This is just an example and in your case, you can create a combo box with different Units values. Based on user selection, you can use the converter and return value to the textbox.
<ComboBox Height="30" SelectedItem="{Binding reports_to, NotifyOnTargetUpdated=True, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemsSource>
<MultiBinding Converter="{StaticResource roles_filter}">
<Binding Source="{StaticResource SingletonData__}" Path="roles" NotifyOnSourceUpdated="True" UpdateSourceTrigger="PropertyChanged"/>
<Binding Path="department" NotifyOnSourceUpdated="True" UpdateSourceTrigger="PropertyChanged"/>
<Binding ElementName="cbx_filter" Path="IsChecked"/>
<Binding Path="id" NotifyOnSourceUpdated="True" UpdateSourceTrigger="PropertyChanged"/>
</MultiBinding>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding department.name}"/>
<TextBlock Text=" - "/>
<TextBlock Text="{Binding name}"/>
</WrapPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

DependencyProperty.unsetValue in a multibinding

I have a listView of items which are 'Book' instances, and when I click on a book the combobox should display its keywords; in fact it's a little trickier : the combobox contains the list of all the keywords of all books (duplicates removed)(the comboboxItems are checkboxes), and those of the selected book are checked.
here is the multibinding:
<ComboBox
x:Name="cbb_Keywords"
Grid.Column="2"
Width="300"
Margin="5,0,0,0"
HorizontalAlignment="Left"
ItemsSource="{Binding Source={StaticResource AllBooks}}"
DataContext="{Binding ElementName=listBoxBooks,Path=SelectedItem,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Width="200">
<CheckBox.IsChecked>
<MultiBinding Converter="{StaticResource TextInListTrueFalseConverter}" >
<Binding Path="KeywordsForTextbox"></Binding>
<Binding RelativeSource="{RelativeSource Self}" Path="Content"></Binding>
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
When I run my program, is seems ok when I click on a book, but I get an exception when I click on the combobox : impossible cast from 'MS.Internal.NamedObject' to 'System.String' type. I saw that value[0] is UnsetValue.
At debugging, when I use spies to track the value of WpfApp1.App.Books[0].KeywordsForTextbox, it gives me the good value (a string which is the list of the keywords of Book[0]. maybe the problem comes from listboxBooks.SelectedItem.KeywordsForTextBox? I can't spy in VS the value of 'listboxBooks'.
some related content...
the beginning of the constructor of MainWindow:
public MainWindow()
{
InitializeComponent();
listBoxBooks.ItemsSource = App.Books;
the convert method of the converter:
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var check = false;
if ((values != null && values.Length == 2))
{
string listString = (string)values[0];
string wordToFind = (string) values[1];
if ((listString != null))
{
List<string> keywordsList = listString.Split(',').ToList();
if (keywordsList.Contains(wordToFind)) check = true;
}
}
return check;
}
the KeywordsForTextbox method:
public string KeywordsForTextbox
{
get { return string.Join(",", _keywords); }
}
and finally the implementation of AllBooks:(as a window resource)
<ObjectDataProvider
x:Key="AllBooks"
MethodName="listOfAllKeywords"
ObjectType="{x:Type mangmt:BookManagement}" />
thank you.
The first Binding of the Multi should be to be to the SelectedItem in the ListBox of Books. I have added in the <CheckBox.IsChecked> where appropriate, and Content="{Binding}" to the CheckBox:
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Width="200" Content={Binding}>
<CheckBox.IsChecked>
<MultiBinding Converter="{StaticResource TextInListTrueFalseConverter}" >
<Binding ElementName=listBoxBooks, Path=SelectedItem.KeywordsForTextbox"></Binding>
<Binding RelativeSource="{RelativeSource Self}" Path="Content"></Binding>
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
You may also wish to add some validation to the IMultiValueConverter to make sure the passed values are not unset, to avoid an exception: If Not values(0) Is DependencyProperty.UnsetValue And Not values(1) Is DependencyProperty.UnsetValue Then in VB.
Regarding the behaviour on checking the checkbox, I am guessing this is because of the ConvertBack Method of the IMultiValueConverter. You can remove the 'Throw Exception' code, and write a method to add/remove the text of the checked/unchecked box to your keyword list.

How to keep my items in datagrid in sequence order when item from middle is deleted WPF C#

I am loading items from database to my DataGrid on this way (on form load):
dataGridArticles.ItemsSource = null;
dataGridArticles.ItemsSource = listCurrentArticles;
And I got ability to add item to my datagrid list, and also I have ability to remove them by pressing DEL key <datagrid automatically removes it from a list>.
But When I am entering item to my list which is source to datagrid I am also setting OrdinalNumber for each item, for example when I adding new item to a list I am doing next thing:
article.OrdinalNumber = GetCurrentlyMaxOrdinalNumber()+1;
so if I'm adding items one after other, ordinals numbers will be 1,2,3,4,5... and so on, something like this (marked red) :
My stored procedure which is returning max ordinal number from articles table:
CREATE DEFINER=`root`#`localhost` PROCEDURE `ArticleGetNextCodeNumber`()
BEGIN
Select ifnull(max(OriginalArticleCode),0)
from articles;
END
And this is what is happening when I am adding new item to my list:
I am setting next ordinal numbers and some other attributes for new article:
private void btnSaveChangesOnArticle_Click(object sender, RoutedEventArgs e)
{
try
{
if (newItem)
{
Article newArticle = new Article();
newArticle.ArticleCode = txtArticleCode.Text.Trim();
newArticle.OriginalArticleCode = Convert.ToInt32(ArticlesController.Instance.GetNextArticleCode()) + 1; // Here I'm setting new article ordinal number
newArticle.Title = txtArticleTitle.Text.Trim();
newArticle.Price = Convert.ToDecimal(txtPrice.Text.Trim());
var lastInserted = ArticlesController.Instance.Save(newArticle);
MessageBox.Show("Operation Successful.");
listCurrentArticles.Add(lastInserted);
dataGridArticles.ItemsSource = null;
dataGridArticles.ItemsSource = listCurrentArticles;
}
}
}
}
And I am wondering right now, when user press DELETE KEY to remove some item from my datagrid,how can I reorganize my ordinal numbers, because if I add 5 items for example to my datagrid, and if user delete second item, than my order might look like this: 1 3 4 5.. insted of 1 2 3 4
So my question is guys, how can i reorganize that, when something changes to keep it sequenced all the time ( so maybe then I dont need to keep ordinal number in database, because it is not important to me, I am using it just because of better UI look, to show to user how many items he added to that DataGrid).
Thanks a lot,
Cheers!
You could replace the OrdinalNumber DataGridTextColumn with a DataGridTemplateColumn and use a converter to return the value of the GetIndex() method of the parent DataGridRow container.
You need to bind to both the parent DataGridRow and the Count property of the Items property of the DataGrid itself for the binding to get refreshed when the number of items changes, i.e. when an item is added or removed.
This should be pretty simple to implement. You create a class that implements the IMultiValueConverter interface:
namespace WpfApplication3
{
public class RowNumberConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return ((values[0] as DataGridRow).GetIndex() + 1).ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
And use it like this:
<DataGrid x:Name="dataGridArticles" AutoGenerateColumns="False" xmlns:local="clr-namespace:WpfApplication3">
<DataGrid.Resources>
<local:RowNumberConverter x:Key="RowNumberConverter" />
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Header="#" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource RowNumberConverter}">
<Binding Path="." RelativeSource="{RelativeSource AncestorType=DataGridRow}" />
<Binding Path="Items.Count" RelativeSource="{RelativeSource AncestorType=DataGrid}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- + the rest of your columns -->
</DataGrid.Columns>
</DataGrid>
Check out this related question that might solve your problem.
Column/Row index in a DataGrid column
Your best option is to remove OrdinalNumber property (Since you do not need it, as you said) and use DataGrid.RowHeaderTemplate with a converter. Here is a working example:
MainWindow
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
listCurrentArticles = new ObservableCollection<Article>() { new Article { Text = "k" }, new Article { Text = "l" } };
}
public ObservableCollection<Article> listCurrentArticles { get; set; }
Xaml
<Window.Resources>
<local:HeaderConverter x:Key="headerConverter"/>
</Window.Resources>
<Grid>
<DataGrid DataContext="{Binding}" ItemsSource="{Binding listCurrentArticles}">
<DataGrid.RowHeaderTemplate>
<DataTemplate>
<TextBlock MinWidth="25" TextAlignment="Center">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource headerConverter}">
<Binding Path="ItemsSource" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=DataGrid}" />
<Binding Path="Item" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=DataGridRow}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</DataGrid.RowHeaderTemplate>
</DataGrid>
</Grid>
Converter
public class HeaderConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
ObservableCollection<Article> list = values[0] as ObservableCollection<Article>;
Article obj = values[1] as Article;
int ind = list.IndexOf(obj);
if (ind == -1)
return "+";
else
return (ind + 1).ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

Using a MultiConverter with ComboBox.SelectedValue: value for "ConvertBack" is always null

I have the following scenario: In my View Model I have two properties, let them be CreateNewThing and SelectedExistingThing. CreateNewThing is of type bool and SelectedExistingThing is of type Thing.
I now have a ComboBox that displays two static entries, "None" and "Create new", and a list of Things. I wired it up like this:
Collection for the list of things:
<ComboBox.Resources>
<CollectionViewSource x:Key="AllThings" Source="{Binding ViewModel.AllThings, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyCustomControl}}}" />
<tools:ThingSelector x:Key="ThingSelector" />
</ComboBox.Resources>
Setup for the ItemsSource:
<ComboBox.ItemsSource>
<CompositeCollection>
<tools:PlaceholderForNone />
<tools:PlaceholderForNew />
<Separator />
<CollectionContainer Collection="{Binding Mode=OneWay, Source={StaticResource AllThings}}" />
</CompositeCollection>
</ComboBox.ItemsSource>
Binding for SelectedValue:
<ComboBox.SelectedValue>
<MultiBinding Converter="{StaticResource ThingSelector}">
<Binding Path="ViewModel.CreateNewThing" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type local:MyCustomControl}}" />
<Binding Path="ViewModel.SelectedExistingThing" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type local:MyCustomControl}}" />
</MultiBinding>
</ComboBox.SelectedValue>
My ThingSelector looks like this:
internal sealed class ThingSelector: IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
if((values == null) ||
(values.Length != 2) ||
(!(values[0] is bool))) {
return Binding.DoNothing;
}
if((bool)values[0]) {
return new PlaceholderForNew();
}
if(values[1] is Thing) {
return values[1];
}
return new PlaceholderForNone();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
if(value is PlaceholderForNew) {
return new object[] { true, null };
}
return new object[] { false, value as Thing};
}
}
The PlaceholderForXXX are done in a way that their HashCode is always 0 and Equals is true for all objects of the same type, so all two PlaceholderForXXX are equal.
The strange thing is now: I can select the "None" and "Create new" options from my combo box, and they are correctly propagated (i.e., selecting the "new" option sets the CreateNewThing property to true and the SelectedExistingThing property to null).
However, when I select a Thing from the combo box, the ConvertBack method is invoked with the parameter value being null where, to my understanding, it should be the selected value of the ComboBox which is clearly not null (if I put an appropriate event into the code-behind, I really see that SelectedValue is a Thing and not null).
What am I missing?

How to use StringFormat in MultiBinding?

I have the following code:
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1}">
<Binding Path=".[ServerName]" TargetNullValue=""/>
<Binding Path=".[InstanceName]" TargetNullValue="" StringFormat="{}\\{0}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
but the result I get is "ServerNameInstanceName" instead of "ServerName\InstanceName"
StringFormat is ignored if the target of the binding is not typed as String. Therefore, the StringFormat on the MultiBinding will be used, because TextBlock.Text is String (it wouldn't work if you were binding to Label.Content, because Label.Content is Object -- a common complaint). The inner one, the binding to .[InstanceName], doesn't have a target that's typed as String, because MultiBindings must accept values of any type. So that StringFormat is ignored.
The following works.
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}\{1}">
<Binding Path=".[ServerName]" TargetNullValue=""/>
<Binding Path=".[InstanceName]" TargetNullValue="" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
If you did it your way in order to eliminate the backslash in cases where .[InstanceName] is null, you've got a problem. I would write a multivalue converter to insert the backslash, or not, and skip StringFormat. There's no way to get StringFormat to do what you need it to do here. It's just not smart enough.
C#
public class ServerInstance : MarkupExtension, IMultiValueConverter
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
public object Convert(object[] values, Type targetType,
object parameter, CultureInfo culture)
{
var server = $"{values[0]}0";
var instance = $"{values[1]}";
if (instance == "")
return server;
if (server == "")
return "";
return $"{server}\\{instance}";
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
XAML
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{local:ServerInstance}">
<Binding Path=".[ServerName]" />
<Binding Path=".[InstanceName]" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>

Categories