Databinding to the value of a multiBinding - wpf

Maybe I am not quite grasping multibindings.
I have a property on my viewmodel called OfficeDisplayName that is written to the database.
This is a concatenated field based on a person's FirstName, Lastname, and office location.
So I have a multibinding on a textBlock...no biggie...works beautifully...but how do I bind the full value of this concatenation to the OfficeDisplayName property? Do I have to have a hidden element that binds to the multibound textbox? I have seen several examples that are almost what I need, but just dont answer the concat databinding question.

One way is to let the textblock bind directly to OfficeDisplayName and then put the concatenation logic in the OfficeDisplayName property on your viewmodel instead of in the MultiValueConverter. So when ever one of the properties FirstName, LastName, or office location change you'd fire the PropertyChanged event for OfficeDisplayName - i.e. something along the following lines. This way you will not need a converter at all:
class YourViewModel : ViewModel
{
string _firstName;
public string FirstName
{
get { return _firstName; }
set
{
if (_firstName != value)
{
_firstName = value;
OnPropertyChanged("FirstName");
OnPropertyChanged("OfficeDisplayName");
}
}
}
// More properties here
// ...
public string OfficeDisplayName
{
get { return String.Join(" ", new string[] { _firstName, _lastName, _officeLocation}); }
}
}
Another way is to pass your viewmodel itself as a parameter to your MultiValueConverter. In your converter you can set the value of OfficeDisplayName directly. I think this way is a bit "hack-ish" but it is a matter of taste. Your code would look like the following:
The binding in XAML:
<MultiBinding Converter="{StaticResource theConverter}" Mode="OneWay">
<Binding /> <!-- Pass the datacontext as the first parameter -->
<Binding Path="FirstName" />
<Binding Path="LastName" />
</MultiBinding>
The converter:
class TheMultiValueConverter : IMultiValueConverter
{
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var viewModel = values[0] as TheViewModel;
var ret = String.Join(" ", values.Skip(1).Cast<string>().ToArray());
viewModel.OfficeDisplayName = ret;
return ret;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}

Related

WPF MVVM Multibinding - How to pass two parameters to Command using RelayCommand

How to pass 2 parameters to command using RelayCommand. I need to pass COntrols as parameters (Grid and Window). I'm fully aware that such kind of problem has already existed on Stack Overflow but I'm struggling with adjusting that to my needs.
See look my first attempt is following. It obviously doesn't work because the Relay can't get 2 arguments.
Xaml code:
<Button Name="StretchScreenBtn" Content="Stretch screen" DataContext=" {StaticResource CommandWindow}" Command="{Binding ResizeScreenCommand}"
Width="100" Height="50">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource CommandParamsConv}">
<Binding ElementName="ScreenGrid" />
<Binding RelativeSource="{RelativeSource AncestorType=Window}" />
</MultiBinding>
</Button.CommandParameter>
</Button>
The Command code from ViewModel:
private ICommand _ResizeScreenCommand;
public ICommand ResizeScreenCommand
{
get
{
if (_ResizeScreenCommand == null)
{
_ResizeScreenCommand = new RelayCommand(
(argument1, argument2) =>
{
Grid grid = argument1 as Grid;
Window window = argument2 as Window;
if ((int)grid.GetValue(Grid.ColumnSpanProperty) == 2)
{
grid.SetValue(Grid.ColumnSpanProperty, 1);
window.WindowStyle = WindowStyle.None;
}
else
{
grid.SetValue(Grid.ColumnSpanProperty, 2);
window.WindowStyle = WindowStyle.SingleBorderWindow;
}
}
);
}
return _ResizeScreenCommand;
}
}
And the MultiValueConverter:
class CommandParamsConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
object[] parameters = values;
return parameters;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
My second attempt I followed the solution from How to Passing multiple parameters RelayCommand?
So, I've created within the ViewModel the new class, with two properties Grid and Window types and then tried to bind in xaml the Elements to these properties. But i this case the compiler complains that these properties are non-dependancy and cannot be bindded.
Please give me any hint how to solve it?
Below is the modified xaml code:
<Button Name="StretchScreenBtn" Content="Stretch screen" DataContext="{StaticResource CommandWindow}" Command="{Binding ResizeScreenCommand}"
Width="100" Height="50">
<Button.CommandParameter>
<vm:StretchingModel Grid="{Binding ElementName=ScreenGrid}" Win="{Binding RelativeSource={RelativeSource AncestorType=Window}}" />
</Button.CommandParameter>
</Button>
And the additionall class in the ViewModel:
class StretchingModel
{
public Grid Grid { get; set; }
public Window Win { get; set; }
}
Passing a Grid and a Window to a view model is not MVVM...a view model shouldn't have any dependencies upon any UI elements.
Anyway, to pass more than one value to the command you should combine your two approaches. The converter should return an instance of a StretchingModel:
class CommandParamsConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new StretchingModel() { Grid = values[0], Window = values[1] };
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
...that the command accepts:
private ICommand _ResizeScreenCommand;
public ICommand ResizeScreenCommand
{
get
{
if (_ResizeScreenCommand == null)
{
_ResizeScreenCommand = new RelayCommand(
(argument) =>
{
StretchingModel model = argument as StretchingModel;
...
}
);
}
return _ResizeScreenCommand;
}
}

Multibinding not working on TextBox.Text

I have a MultiBinding that is not working on TextBox.Text. I have the same code that is binding properly to Value of Extended WPF Toolkit's IntegerUpDown.
It is going through an IMultiValueConverter that takes the bound POCO and the listbox it is part of (it is displaying the order of the item in the listbox)
Here is the code:
<!--works-->
<wpf:IntegerUpDown ValueChanged="orderChanged" x:Name="tripOrder">
<wpf:IntegerUpDown.Value>
<MultiBinding Converter="{StaticResource listBoxIndexConverter}" Mode="OneWay">
<Binding />
<Binding ElementName="listTrips" />
</MultiBinding>
</wpf:IntegerUpDown.Value>
</wpf:IntegerUpDown>
<!--doesn't work-->
<TextBox x:Name="tripOrder2">
<TextBox.Text>
<MultiBinding Converter="{StaticResource listBoxIndexConverter}" Mode="OneWay">
<Binding />
<Binding ElementName="listTrips" />
</MultiBinding>
</TextBox.Text>
</TextBox>
Here is the result:
I don't believe it is relevant, but just in case, here is the class that performs the conversion:
public class ListBoxIndexConverter : IMultiValueConverter
{
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var trip = values[0] as TripBase;
if (trip == null)
{
return null;
}
var lb = values[1] as CheckListBox;
if (lb == null)
{
return null;
}
//make it 1 based
return lb.Items.IndexOf(trip) + 1;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
The converter should return the type that the property expects. The reason is that in regular use of the properties (i.e. without Binding), the properties may have type converters that convert from one type (or more) to the type required by the property. For example, when you write:
<ColumnDefinition Width="Auto"/>
there's a converter that converts string "Auto" to:
new GridLength(1, GridUnitType.Auto)
When using binding, this mechanism is bypassed since the converter should return the right type.
So, to fix your issue, at the return of your converter:
return (lb.Items.IndexOf(trip) + 1).ToString();
This should fix the TextBox.
Now, for the IntegerUpDown. It sounds like it actually expects to receive an int and returning a string will break it. So, again, change the return of the converter:
if (targetType == typeof(int))
{
return lb.Items.IndexOf(trip) + 1;
}
else if (targetType == typeof(string))
{
return (lb.Items.IndexOf(trip) + 1).ToString();
}
else
{
throw new NotImplementedException(String.Format("Can not convert to type {0}", targetType.ToString()));
}
The binding is not going to work, because the listTrips is not changing when the list box's selected value changes. The thing that changes is listTrips.SelectedItem, so you should bind against it:
<Binding Path="SelectedItem" ElementName="listTrips"/>
Actually, I wonder why it works for the first example.

Using WPF TextBox with a URI converter, invalid input wipes textbox

I have a WPF textbox on a form to allow input of a URI.
I tried to do this using a data converter. The problem is that when the textbox binding updates and the textbox doesn't contain a valid URI,
The data converter returns a null ;
which sets my model property to null ;
which results in a property changed event firing ;
which sets the text box value to the empty string, wiping out the users invalid input
I'm a WPF novice, and I'm at a loss to find a simple pattern using a data converter that doesn't result in this behaviour. I'm thinking there must be a standard pattern to use, that I'd know about if I was an experienced WPF programmer.
Looking at the examples included with Prism 4, there seems to be two different approaches used. I dislike them both.
The first approach is to throw an exception when my model property is set null, which is caught and shown as a validation error. The problem is that I want the property to be able to be set to null - each time you open the form, the fields are set to their previous values. If the application has never been ran before, the URI will be set to null - this shouldn't throw an exception. Also, the use of exceptions for validation is ugly.
The second approach is when the property is set to null, set the validation state of the model to include the property invalidity, but don't actually update the property. Which I think is awful. It results in the model being internally inconsistent, claiming that the DCSUri is invalid, but containing the previous valid value of DCSUri.
The approach I'm using to avoid these issues is to have a string DCSUri in my ViewModel, which only updates the Uri typed DCSUri property of my Model if it is a valid URI. But I'd prefer an approach which allows use of a converter and binding my textbox directly to my model.
My converter code:
/// <summary>
/// Converter from Uri to a string and vice versa.
/// </summary>
public class StringToUriConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
Uri uri = null;
string stringValue = value as string;
if (stringValue != null)
Uri.TryCreate(stringValue, UriKind.Absolute, out uri);
return uri;
}
}
The XAML for the textbox:
<TextBox Grid.Row="1" Grid.Column="1" Name="DCSUriTextBox"
Text="{Binding Path=DCSLoadSettings.DCSUri, Mode=TwoWay, UpdateSourceTrigger=LostFocus, ValidatesOnExceptions=True, NotifyOnValidationError=True, ValidatesOnDataErrors=True, Converter={StaticResource StringToUriConverter} }"
HorizontalAlignment="Stretch" Height="Auto" VerticalAlignment="Center" Margin="5,0,20,0" IsReadOnly="{Binding Path=IsNotReady}" Grid.ColumnSpan="2" />
And the code for the DCSUri property within my model:
/// <summary>
/// The Uri of the DCS instance being provided configuration
/// </summary>
public Uri DCSUri
{
get
{
return mDCSUri;
}
set
{
if (!Equals(value, mDCSUri))
{
mDCSUri = value;
this["DCSUri"] = value == null
? "Must provide a Uri for the DCS instance being provided configuration"
: string.Empty;
RaisePropertyChanged(() => DCSUri);
}
}
}
You should use ValidationRules for Validation, and name your converters the right way around; i would approach it like this (assuming that you want to be able to set the Uri to null):
public class UriToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Uri input = value as Uri;
return input == null ?
String.Empty : input.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string input = value as string;
return String.IsNullOrEmpty(input) ?
null : new Uri(input, UriKind.Absolute);
}
}
public class UriValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
string input = value as string;
if (String.IsNullOrEmpty(input)) // Valid input, converts to null.
{
return new ValidationResult(true, null);
}
Uri outUri;
if (Uri.TryCreate(input, UriKind.Absolute, out outUri))
{
return new ValidationResult(true, null);
}
else
{
return new ValidationResult(false, "String is not a valid URI");
}
}
}
Then use it like this (or by defining the converter and rule as a resource somewhere):
<TextBox MinWidth="100">
<TextBox.Text>
<Binding Path="Uri">
<Binding.ValidationRules>
<vr:UriValidationRule />
</Binding.ValidationRules>
<Binding.Converter>
<vc:UriToStringConverter/>
</Binding.Converter>
</Binding>
</TextBox.Text>
</TextBox>
If the input text does not pass validation the Converter will not be called, that is why i have no TryCreate or anything like that in there.
There is a decent article about input validation on CodeProject which you might find to be helpful.
To test the value for null you could use another converter and a helper TextBlock:
public class NullToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value == null ?
"NULL" : value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
<TextBlock>
<TextBlock.Text>
<Binding Path="Uri">
<Binding.Converter>
<vc:NullToStringConverter/>
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>

How can I bind against two DynamicResources?

I am trying to bind the value of an element to a first resource , if present, then another one otherwise.
In other words, if the resources look like
<s:String x:Key="first">Hello<s:String>
<s:String x:Key="second">World<s:String>
my element's value would hold Hello
But if the resources have only
<s:String x:Key="second">World<s:String>
the value would be World
I have tried a number of solution but none seems to work or is elegant enough.
I wish I could write
<MyElement>
<MyElement.Value><MultiBinding Converter=...><DynamicResource Key=First/><DynamicResource Key=Second/> ...
where the converter takes care of finding the first non null value.
However, WPF does not allow mixing DynamicResource and MultiBinding
Do you have a solution?
Edit 1: I may have read your question a little too fast... you're binding to dynamic resources... not class properties... so the solution below is probably not what your looking for. But I'll leave it for now in case it helps you come up a solution.
Edit 2: I tried the following code in Visual Studio 2010 SP1 and it works like it should in the designer (commentout/uncomment the resource 'first'). But it fails to successfully build... which I find strange:
<TextBlock>
<TextBlock.Resources>
<!--<s:String x:Key="first">Hello</s:String>-->
<s:String x:Key="second">World</s:String>
<l:NullItemConverter x:Key="NullItemConverter" />
</TextBlock.Resources>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource NullItemConverter}">
<Binding Source="{DynamicResource first}" />
<Binding Source="{DynamicResource second}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
public class NullItemConverter : IMultiValueConverter
{
object IMultiValueConverter.Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return values[0] ?? values[1];
}
object[] IMultiValueConverter.ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Original Answer (that doesn't answer your question but may help depending on your situation):
Assuming your two properties are in the same class, could you make a third property that smartly outputs the correct value and bind to that instead:
public class MyObject : INotifyPropertyChanged
{
private string property1;
public string Property1
{
get { return this.property1; }
set
{
if (this.property1 != value)
{
this.property1 = value;
NotifyPropertyChanged("Property1");
NotifyPropertyChanged("PropertyForBinding");
}
}
}
private string property2;
public string Property2
{
get { return this.property2; }
set
{
if (this.property2 != value)
{
this.property2 = value;
NotifyPropertyChanged("Property2");
NotifyPropertyChanged("PropertyForBinding");
}
}
}
public string PropertyForBinding
{
get
{
return this.Property1 ?? this.Property2;
}
}
public MyObject() { }
#region -- INotifyPropertyChanged Contract --
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion INotifyPropertyChanged Contract
}

WPF ListBox Binding ItemsSource

In a related question I asked about binding to a particular element of an array indexed by another property. The answer provided worked really well for the sample code example provided.
Where I'm running into trouble is that I specify an ItemSource for a ListBox and I get a DependencyProperty.UnsetValue in my converter when I step through it. No doubt this is a problem with my understanding of Binding.
My ListBox looks like so:
<ListBox ItemsSource="{Binding Path=MyList}">
<ListBox.Resources>
<local:FoodIndexConverter x:Key="indexConverter" />
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource indexConverter}">
<Binding Path="MyIndex" />
<Binding Path="Fields" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
and the code behind looks as so:
public MainWindow()
{
InitializeComponent();
MyList.Add(new SomeData() { Fields = new object[] {"liver", "onions", "cake" } } );
MyList.Add(new SomeData() { Fields = new object[] {"liver", "onions", "candy" } } );
MyList.Add(new SomeData() { Fields = new object[] {"liver", "onions", "pie" } } );
DataContext = this;
}
MyList is a List.
MyIndex is an int.
The converter code is
public class FoodIndexConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values == null || values.Length != 2)
return null;
int? idx = values[0] as int?;
object[] food = values[1] as object[];
if (!idx.HasValue || food == null)
return null;
return food[idx.Value];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
When I step through the debugger in the converter code the MyIndex (value[0]) is DependencyProperty.UnsetValue - the object array is as I'd expect.
I'm assuming it is a Binding issue with:
in that it doesn't know what MyIndex is.
If MyIndex were a property of the SomeData class, it works as I would expect but it's not, it's a property of the class MainWindow, just like MyList.
How do I specify that I'm interested in the MyIndex property that is part of my DataContext and not the MyData List?
It is looking for the MyIndex property on the ListBoxItem (in this case, a SomeData class) and that property doesn't exist.
Set the ElementName in your binding to the window name to get it to look for the property elsewhere.
<Binding ElementName=RootWindow, Path=DataContext.MyIndex />

Resources