09 September, 2010

WPF ListView, MVVM and (missing) ICommand support

As great an experience WPF can be in conjunction with the MVVM pattern, as poor an experience it can be to learn that the standard WPF ListView does not support the ICommand pattern that is used otherwise in MVVM implementations. And this in particular to the ListView.SelectionChanged event which is very commonly in use!
Of course you can handle this by subscribing directly to the SelectionChanged event and code your way inside the View (UI), but that kind of contradicts the purpose of the MVVM pattern in the first place. The View should know absolutely nothing about code nor should it know anything about the ViewModel. All interaction from the View’s point of view should be handled by the ViewModel (the “Presenter” if you will).
Well – WPF defines attached properties which allows us to make the ListView support the ICommand pattern. Here is how to declare a new class (SelectionBehavior) that caters for this.
public class SelectionBehavior{
    public static DependencyProperty SelectionChangedProperty =
       DependencyProperty.RegisterAttached("SelectionChanged",
       typeof(ICommand),
       typeof(SelectionBehavior),
       new UIPropertyMetadata(SelectionBehavior.SelectedItemChanged));

    public static void SetSelectionChanged(DependencyObject target, ICommand value)
    {
        target.SetValue(SelectionBehavior.SelectionChangedProperty, value);
    }
    
    static void SelectedItemChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        Selector element = target as Selector;
        if (element == null) throw new InvalidOperationException("This behavior can be attached to Selector item only.");
        if ((e.NewValue != null) && (e.OldValue == null))
        {
            element.SelectionChanged += SelectionChanged;
        }
        else if ((e.NewValue == null) && (e.OldValue != null))
        {
            element.SelectionChanged -= SelectionChanged;
        }
    }
    
    static void SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
    {
        UIElement element = (UIElement)sender;
        ICommand command = (ICommand)element.GetValue(SelectionBehavior.SelectionChangedProperty);
        command.Execute(((Selector)sender).SelectedValue);
    }
}


It will be used with this XAML:
<!--List of persons--><ListView ItemsSource="{Binding Persons}" 
   SelectedItem="{Binding CurrentPerson}" 
   local:SelectionBehavior.SelectionChanged="{Binding ItemSelectedCommand}">


Attached properties are in reality not properties at all. They are actually translated into method-calls by the XAML-parser when parsing the declarations found in the XAML-files. So, if you note the ‘local:SelectionBehavior.SelectionChanged’ section in the above XAML-snippet, this is where the XAML-parser will call the static SetSelectionChanged method (by convention!) on the SelectionBehavior class. This method in turn will call SelectionChanged when it is invoked.

So far so good. Now how do I bind this to an ICommand on my ViewModel, you might ask?The SelectionChanged method is linked to a local method (PresentPersonHelper)  in my ViewModel via a DelegateCommand (ItemSelectedCommand) I setup:
class ViewModel : INotifyPropertyChanged
 {
     List<Person> m_personLst;

     public ViewModel()
     {
         //default
         m_person = new Person();
       
         //wire ICommand to method 'PresentPersonHelper'
         ItemSelectedCommand = new DelegateCommand(x => PresentPersonHelper(CurrentPerson.Id));

         //load list
         var provider = new PersonProvider();
         Persons = provider.GetPersons();
     }

     public ICommand ItemSelectedCommand { get; set; }


So when ever the ItemSelectedCommand is invoked, it will delegate a call to PresentPersonHelper and do what ever that method does. Control is back inside the ViewModel where it belongs!

4 comments:

runeemche said...

Well - you can separate selectionchange from ui if you use a CollectionView... :)

Claus Konrad said...

CollectionView (System.Windows.dll) is not offically supported on WP7 as I recall?

Anonymous said...

would it kill you (or anyone else for that matter) to put the damn includes in with your code so people can see what namespaces / assemblies you are using, I am sick of having to search the internet every time I run into a new object declaration in someones code. Everybody leaves this kind of information out and it SUCKS! It's not for brevity - it's just half assed!

Jérémie Bertrand said...

7 years later and this is still needed.
Thanks a lot!

iPhone/XCode - not all cases are equal!

This bit me! Having made some changes to an iPhone application (Obj-C); everything worked fine in the simulator. But, when deploying the s...