In my previous post (INotifyPropertyChanged and WPF), I discussed how WPF can detect when the values of properties change. In that post, I fired off a Message Box from the View Model, and commented that you shouldn't do this. However, I didn't expound on how to work around this. In this post, I am going to show how we can pass multiple values into the Command as the Command Parameter so we can interact with the user through the View with a much cleaner implementation.
Instead of a message box, we are going to add a label to the form that will display the current price of the Product. When we update the price, the label gets updated with the current price of the product displayed. Note the it isn't bound to anything in the product, it is purely a UI item. The finished product will look like this (with the INotifyPropertyChanged event handler NOT getting fired):
The XAML changes for the label are minor. We add an additional RowDefinition to the table, and in that row, add two label controls. One for the actual "Label" and the other to hold the price.
The changes for the Button allow us to pass in the multiple parameters into the CommandParameter. To implement this, we need to execute the following steps:
- Create a class to hold our parameters. This is due to the fact that the Execute method of the ICommand interface allows for only a single object. I create custom classes to give type safety to my code and therefore a cleaner implementation.
- Create a class that implements IMultiValueConverter. This converts our multiple parameters into the class that we created in step 1.
- Change the XAML for the button to use a MultiBinding instead of a single binding.
- Change the Execute method of the ICommand implementation to use this new parameter type.
The Parameter class is very straightforward, having two properties. One is for the product that is the target of the invocation, and the other is for the UIElement that will receive the updated value for display to the user. I typed the UIElement as a ContentControl to allow for more re-use instead of typing it as a Label. I don't have any other uses currently, and some would argue YAGNI (you ain't gonna need it) here, but the code is still very clear, so I opted for the less specific implementation.
public class ChangeNotificationParameter
{
public ContentControl CallBackControl { get; set; }
public Product Prod { get; set; }
}
To implement IMultiValueConverter, create a new class. There are two methods in the interface, Convert and ConvertBack. For this use case we only need to implement Covert. The first parameter to the Convert method is an array of objects that are the binding values set in the XAML of the control on the Window. There are several other parameters that we won't need to worry about here, but will be discussed in a later post.
A new ChangeNotificationParameter is created, and it's properties are set in the Convert method. I iterate through the list of objects, checking their type, and then correctly assign the properties of the parameter class. Again, one could argue that you know the order of the parameters set in the XAML, but I opt for a more defensive stance in case the order isn't guaranteed. The return value of the method is what gets passed into the Execute method if the ICommand implementation.
public class ChangeNotificationMultiConverter : IMultiValueConverter
{
public object Convert(
object[] values,
Type targetType,
object parameter,
System.Globalization.CultureInfo culture)
{
var param = new ChangeNotificationParameter();
foreach (var obj in values)
{
if (obj is Product)
{
param.Prod = obj as Product;
}
if (obj is ContentControl)
{
param.CallBackControl = obj as ContentControl;
}
}
return param;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
To wire up the XAML to take advantage of this class, we have to do a few tasks. First, add a namespace reference to where your MultiValueConverter's namespace. This is done at the top of the Window in the <Window element>. This element now looks like this (assuming your namespace is "WPF.ViewModels":
<Window x:Class="WPF.Views.ChangeNotification"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModels="clr-namespace:WPF.ViewModels"
Title="Change Notification" Height="235" Width="300">
Next, we remove the CommandParameter attribute and include the <Button.CommandParameter> element. This contains the <MultiBinding> element, which has the "Converter" attribute. The Window needs to know where it can find the type of the converter, so we need to add the type as a resource. I chose to add it to the button to make this post easier to read, but convention usually dictates resources are added at the Window level to allow reuse and readability. Under the MultiBinding.Bindings element, we add a <Binding> element for each parameter that we need to pass into the command. In this case the SelectedItem from the ProductSelector ComboBox (to get the current Product) and the Label "CurrentPrice".
<Button Margin="3" Content="Update Price"
Command="{Binding Path=UpdatePriceCmd}">
<Button.Resources>
<ViewModels:ChangeNotificationMultiConverter x:Key="ChangeNotificationMultiConverter"/>
</Button.Resources>
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource ChangeNotificationMultiConverter}">
<MultiBinding.Bindings>
<Binding ElementName="ProductSelector" Path="SelectedItem" />
<Binding ElementName="CurrentPrice" />
</MultiBinding.Bindings>
</MultiBinding>
</Button.CommandParameter>
</Button>
The final change is to the PriceUpdaterCommand to utilize the new parameter. The updated Execute is shown below (with some guards thrown in for safety).
private class PriceUpdaterCommand : ICommand
{
public void Execute(object parameter)
{
var p = parameter as ChangeNotificationParameter;
if (p == null || p.Prod == null) return;
p.Prod.Price *= 1.1M;
if (p.CallBackControl != null)
{
p.CallBackControl.Content = p.Prod.Price.ToString();
}
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}
In this way we've gotten rid of that nasty MessageBox call in the initial implementation, and extended our capabilities significantly. We will use this pattern in the next segment, which dives into INotifyCollectionChanged.
Happy Coding!