Continuing in my series of “Adventures in MVVM”, I want to talk about a few different approaches to working with List Boxes with the MVVM pattern. What I am writing here is generally true of all controls that derive from Selector, including ListBox and ComboBox. This example was developed in Silverlight, but the same concepts also apply to WPF.
The Problem
You have a list box in your view, and you want your ViewModel to do something when an item in the ListBox is selected. You want to do this without any code-behind, using the MVVM pattern. There are three methods that I have come up with, and I will outline them here. In this post, I will be using a VERY simple data class in my ListBox called Person with a First and Last name. It is so simple, in fact, that I have chosen not to include the source for this class.
Method 1: Quick and Dirty SelectedItem binding
This method sets up a SelectedPerson property in the view model and does something when the property is changed.
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<Person> People { get; private set; }
private Person _selectedPerson = null;
public Person SelectedPerson
{
get { return _selectedPerson; }
set
{
_selectedPerson = value;
OnPropertyChanged("SelectedPerson");
DoSomething(value);
}
}
// ... rest of ViewModel
}
<ListBox ItemsSource="{Binding People}" SelectedItem="{Binding SelectedPerson, Mode=TwoWay}" />
Pros: This method is quick and simple to get going
Cons: You are introducing side effects in your property code. If you are OK with this, then read no further. If this bothers you the way it does for me, then lets look at our next option.
Method 2: Button Command
There are plenty of commanding libraries out there to choose from. I will take advantage of the Prism commanding system (Microsoft.Practices.Composite.Presentation.Commands). They have implemented bindable commands for ButtonBase. The only problem: ListBox is not a ButtonBase. To get around this, replace the ItemTemplate with a Button that has a template of textblock.
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
PersonSelected = new DelegateCommand<Person>(DoSomething);
// ... rest of constructor
}
public ObservableCollection<Person> People { get; private set; }
public ICommand PersonSelected { get; private set; }
// ... rest of ViewModel
}
<ListBox ItemsSource="{Binding People}">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Commands:Click.Command="{Binding PersonSelected, Source={StaticResource ViewModel}}" Commands:Click.CommandParameter="{Binding}" >
<Button.Template>
<ControlTemplate>
<TextBlock Text="{Binding}" />
</ControlTemplate>
</Button.Template>
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Pros: The ViewModel is much more simple with no side effects.
Cons: The XAML is ugly as sin. It also changes the behavior of the ListBox in a subtle way. Every time you select an item, the command fires, not just when it changes. This is my LEAST favorite approach. We can do better
Method 3: Bind Commands to the ListBox
The final mechanism is my favorite. Even though Prism doesn’t give us the ability to bind commands to ListBoxes, we can extend their attached behavior infrastructure such that all ListBoxes and ComboBoxes (or anything that derives from Selector) can take advantage of it. The ViewModel doesn’t change from “Method 2”, but the XAML does:
<ListBox ItemsSource="{Binding People}" Commands:Selected.Command="{Binding PersonSelected}" />
Pros: Best of both worlds. Simple ViewModel. Simple XAML
Cons: You have to write some extensions to the Prism infrastructure. This code is boilerplate. I have written some generics that can reduce the boilerplate code somewhat, but not completely, due to the static properties.
The Winner Is….
I like “Method 3” the best. With a bit of some infrastructure code that you can tuck away, you get to bind the selected items to a command in any case. It plays well, and it is easy to follow.
But wait… you want the Prism extensions? Here they are:
public class SelectorSelectedCommandBehavior : CommandBehaviorBase<Selector>
{
public SelectorSelectedCommandBehavior(Selector selectableObject)
: base(selectableObject)
{
selectableObject.SelectionChanged += OnSelectionChanged;
}
void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
CommandParameter = TargetObject.SelectedItem;
ExecuteCommand();
}
}
public static class Selected
{
private static readonly DependencyProperty SelectedCommandBehaviorProperty = DependencyProperty.RegisterAttached(
"SelectedCommandBehavior",
typeof(SelectorSelectedCommandBehavior),
typeof(Selected),
null);
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached(
"Command",
typeof(ICommand),
typeof(Selected),
new PropertyMetadata(OnSetCommandCallback));
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Only works for selector")]
public static void SetCommand(Selector selector, ICommand command)
{
selector.SetValue(CommandProperty, command);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Only works for selector")]
public static ICommand GetCommand(Selector selector)
{
return selector.GetValue(CommandProperty) as ICommand;
}
private static void OnSetCommandCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var selector = dependencyObject as Selector;
if (selector != null)
{
GetOrCreateBehavior(selector).Command = e.NewValue as ICommand;
}
}
private static SelectorSelectedCommandBehavior GetOrCreateBehavior(Selector selector)
{
var behavior = selector.GetValue(SelectedCommandBehaviorProperty) as SelectorSelectedCommandBehavior;
if (behavior == null)
{
behavior = new SelectorSelectedCommandBehavior(selector);
selector.SetValue(SelectedCommandBehaviorProperty, behavior);
}
return behavior;
}
}