Wednesday, 21 July 2010

MVVM and Multiple Selection – Part III

Update: Feb 2nd 2012 - I've updated this code to work with DataGrid.

Download the source code for this article

In the previous two blog posts in this series I explored the current best practises for dealing with multiple selections in MVVM, and explained the problems one may encounter.

My feeling for a long time has been “What I really need is a CollectionView that supports multiple selections.” So I decided to write one, and it turned out to be pretty easy!

So without delay, here’s the code for MultiSelectCollectionView:

    public class MultiSelectCollectionView<T> : ListCollectionView, IMultiSelectCollectionView
    {
        public MultiSelectCollectionView(IList list)
            : base(list)
        {
            SelectedItems = new ObservableCollection<T>();
        }

        void IMultiSelectCollectionView.AddControl(Selector selector)
        {
            this.controls.Add(selector);
            SetSelection(selector);
            selector.SelectionChanged += control_SelectionChanged;
        }

        void IMultiSelectCollectionView.RemoveControl(Selector selector)
        {
            if (this.controls.Remove(selector))
            {
                selector.SelectionChanged -= control_SelectionChanged;
            }
        }

        public ObservableCollection<T> SelectedItems { get; private set; }

        void SetSelection(Selector selector)
        {
            MultiSelector multiSelector = selector as MultiSelector;
            ListBox listBox = selector as ListBox;

            if (multiSelector != null)
            {
                multiSelector.SelectedItems.Clear();

                foreach (T item in SelectedItems)
                {
                    multiSelector.SelectedItems.Add(item);
                }
            }
            else if (listBox != null)
            {
                listBox.SelectedItems.Clear();

                foreach (T item in SelectedItems)
                {
                    listBox.SelectedItems.Add(item);
                }
            }
        }

        void control_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (!this.ignoreSelectionChanged)
            {
                bool changed = false;

                this.ignoreSelectionChanged = true;

                try
                {
                    foreach (T item in e.AddedItems)
                    {
                        if (!SelectedItems.Contains(item))
                        {
                            SelectedItems.Add(item);
                            changed = true;
                        }
                    }

                    foreach (T item in e.RemovedItems)
                    {
                        if (SelectedItems.Remove(item))
                        {
                            changed = true;
                        }
                    }

                    if (changed)
                    {
                        foreach (Selector control in this.controls)
                        {
                            if (control != sender)
                            {
                                SetSelection(control);
                            }
                        }
                    }
                }
                finally
                {
                    this.ignoreSelectionChanged = false;
                }
            }
        }

        bool ignoreSelectionChanged;
        List<Selector> controls = new List<Selector>();
    }

As you can see, it’s pretty simple. The class derives from ListCollectionView and maintains an ObservableCollection to hold the selected items and a List to hold the controls that are bound to it.

One thing to notice is that the class is generic, in order to allow the SelectedItems collection to be strongly typed. This may seem like a strange choice considering that CollectionViews are usually not strongly typed.

The reason for this is that the items in a CollectionView will usually only be used by the View side of the application and view controls do not care about having strongly typed values. However, the new SelectedItems collection will usually be used from the ViewModel side of things, where having strongly typed values is important.

Now, WPF controls have no knowledge of MultiSelectCollectionView so if you hook it up to an ItemsControl now, it will work exactly the same as a ListCollectionView, with no multi-select support. We add in multi-select support using the magic of attached properties:

    public static class MultiSelect
    {
        static MultiSelect()
        {
            Selector.ItemsSourceProperty.OverrideMetadata(typeof(Selector), new FrameworkPropertyMetadata(ItemsSourceChanged));
        }

        public static bool GetIsEnabled(Selector target)
        {
            return (bool)target.GetValue(IsEnabledProperty);
        }

        public static void SetIsEnabled(Selector target, bool value)
        {
            target.SetValue(IsEnabledProperty, value);
        }

        public static readonly DependencyProperty IsEnabledProperty =
            DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(MultiSelect), 
                new UIPropertyMetadata(IsEnabledChanged));

        static void IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            Selector selector = sender as Selector;
            IMultiSelectCollectionView collectionView = selector.ItemsSource as IMultiSelectCollectionView;

            if (selector != null && collectionView != null)
            {
                if ((bool)e.NewValue)
                {
                    collectionView.AddControl(selector);
                }
                else
                {
                    collectionView.RemoveControl(selector);
                }
            }
        }

        static void ItemsSourceChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            Selector selector = sender as Selector;

            if (GetIsEnabled(selector))
            {
                IMultiSelectCollectionView oldCollectionView = e.OldValue as IMultiSelectCollectionView;
                IMultiSelectCollectionView newCollectionView = e.NewValue as IMultiSelectCollectionView;

                if (oldCollectionView != null)
                {
                    oldCollectionView.RemoveControl(selector);
                }

                if (newCollectionView != null)
                {
                    newCollectionView.AddControl(selector);
                }
            }
        }
    }

Here we create an attached property, MultiSelect.IsEnabled. When set to true on a control derived from Selector, it adds the control to the MultiSelectCollectionView’s list of bound controls.

We also override the metadata for the ItemsSource property for all Selector controls. In this way we can detect when a control’s ItemsSource changes and ensure that the control is registered with the correct MultiSelectCollectionView. Note that because MultiSelectCollectionView is generic, we use the non-generic IMultiSelectCollectionView interface to add/remove controls. IMultiSelectCollectionView looks like this:

    public interface IMultiSelectCollectionView
    {
        void AddControl(Selector selector);
        void RemoveControl(Selector selector);
    }

Now we can add multiple selection capabilites to a control like so:

<ListBox ItemsSource="{Binding Items}" SelectionMode="Extended" local:MultiSelect.IsEnabled="True"/>

That’s it! You can download the source code here. Let me know of any problems you find and also any improvements!

Monday, 19 July 2010

MVVM and Multiple Selection – Part II

In the last instalment in this series I explored a common way to communicate multiple selections between a View and a ViewModel and left the article facing a problem:

How do we handle multiple selections in the face of UI virtualization?

A solution can be found in this Stack Overflow question. The solution advises that instead of using an ItemContainerStyle to communicate the selection information between View and ViewModel, the selection should be passed using the CommandParameter property:

<ListBox x:Name="listBox" ItemsSource="{Binding Items}"/>

<Button Content="Remove Selected"
    Command="{Binding RemoveSelectedItemsCommand}"
    CommandParameter="{Binding ElementName=listBox, Path=SelectedItems}"/>

This technique bypasses UI virtualization and passes the selected items directly from the control’s SelectedItems collection to the command itself using the CommandParameter property. Problem solved.

Except we’ve now introduced a tight coupling between the ListBox containing the items and the Button that invokes the command. What if they’re located in different UserControls? For example, you might want add a MenuItem to an application’s main Window that looks something like this:

<MenuItem Header=”Print Selected” Command=”{Binding CurrentDocument.PrintSelectedItemsCommand}”/>

(this is a pattern I described recently in Using Binding.FallbackValue to Disable a UI Element When its Command can’t be Found)

Here, the MenuItem will not have access to the ListBox displaying the selected items to the user, as the ListBox is located in a child UserControl.

In this case, in order to locate the ListBox control the main Window would need to either have in-depth knowledge of its child UserControls, or some sort of mechanism to expose child ItemsControls would need to be invented. Messy.

What we really need is a MultiSelectCollectionView – I’ll describe such a thing in the next article in the series.

Friday, 16 July 2010

Using Binding.FallbackValue to Disable a UI Element When its Command can’t be Found

I don’t know about you, but I often find myself using this pattern in my MVVM applications:

  • An ApplicationViewModel which represents the application’s top level state. This will usually contain a property called something like CurrentDocument which will hold…
  • A DocumentViewModel containing information about a single document.

This might look something like the following:

class ApplicationViewModel : ObservableObject
{
   public ApplicationViewModel()
   {
       NewDocumentCommand = new DelegateCommand(NewDocument);
   }

   public DocumentViewModel CurrentDocument { get; private set; }
   public DelegateCommand NewDocumentCommand { get; private set; }

   void NewDocument()
   {
       CurrentDocument = new DocumentViewModel();
       RaisePropertyChanged(() => this.CurrentDocument);
   }
}

Here we have the application’s current document exposed as a property in the application view model, and an ICommand that creates a new document. Here’s how the UI’s xaml might look:

<Window x:Class="NullCommand.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:local="clr-namespace:NullCommand"
       Title="MainWindow" Height="350" Width="525">
   <Window.DataContext>
       <local:ApplicationViewModel/>
   </Window.DataContext>
   <DockPanel>
       <Menu DockPanel.Dock="Top">
           <MenuItem Header="_File">
               <MenuItem Header="New" Command="{Binding NewDocumentCommand}"/>
           </MenuItem>
       </Menu>
       <ContentPresenter Content="{Binding CurrentDocument}"/>
   </DockPanel>
</Window>

Now, in Document we might want to provide a command to say, print the document:

class DocumentViewModel
{
   public DocumentViewModel()
   {
       PrintCommand = new DelegateCommand(Print);
   }

   public DelegateCommand PrintCommand { get; private set; }

   void Print()
   {
       System.Windows.MessageBox.Show("Print!");
   }
}

This logically belongs in DocumentViewModel, as it’s an operation that is carried out on the document. However, we want to invoke this command from the main window’s menu:

<MenuItem Header="Print" Command="{Binding CurrentDocument.PrintCommand}"/>

Because the PrintCommand is a member of DocumentViewModel, we use the ApplicationViewModel.CurrentDocument property to access it. But there is one problem here – if we haven’t already created a new document, ApplicationViewModel.CurrentDocument will be null, and so WPF won’t be able to bind to PrintCommand. This is the point where we discover:

If WPF can’t find the bound command, it doesn’t disable the control.

Now, this is pretty stupid behaviour if you ask me. It would make more sense to disable a control when a bound command cannot be located (at least with an option). So what do we do here? The answer lies with the BindingBase.FallbackValue property. FallbackValue allows us to specify a value to use when a binding can’t be found. So all we need now is a NullCommand which we can fall back to, to disable the Print menu:

public class NullCommand : ICommand
{
   public bool CanExecute(object parameter)
   {
       return false;
   }

   public event EventHandler CanExecuteChanged;

   public void Execute(object parameter)
   {
       throw new NotImplementedException("NullCommand cannot be executed.");
   }
}

The way I usually use NullCommand is to add it as a resource to my App.xaml:

<Application.Resources>
    <local:NullCommand x:Key="NullCommand"/>
</Application.Resources>

And our Print menu item now looks like this:

<MenuItem Header="Print" Command="{Binding Path=CurrentDocument.PrintCommand, FallbackValue={StaticResource NullCommand}}"/>

And hey presto! If the PrintCommand can’t be found, the MenuItem will now be disabled.

MVVM and Multiple Selection – Part I

When using the MVVM pattern in the Windows Presentation Foundation you’ll often want to display a list of items to your user in a control such as a ListBox. To do this, you can expose a collection of child items in your ViewModel:

public ObservableCollection<ItemViewModel> Items { get; private set; }

And in xaml, bind your ListBox’s ItemsSource property to that collection:

<ListBox ItemsSource="{Binding Items}"/>

If you want to do anything useful with this list, your ViewModel will usually want to be able to find out what the ListBox’s current selection is. To do this, WPF provides the ICollectionView mechanism. Instead of exposing “Items” as an ObservableCollection, you’ll expose it as an ICollectionView, set ListBox.IsSynchronizedWithCurrentItem="True" and the user’s selection magically becomes available to your ViewModel through the ICollectionView.CurrentItem property.

However, one major omission from ICollectionView is that it doesn’t support multiple selections. The usual solution to this is to add an IsSelected property to your ItemViewModel, and use a style to keep the IsSelected property on the View in sync with the IsSelected property in the ViewModel:

<ListBox.ItemContainerStyle>
  <Style TargetType="{x:Type ListBoxItem}">
      <Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
  </Style>
</ListBox.ItemContainerStyle>

Here, we’re setting up a two-way binding between the ListBoxItem.IsSelected property and the ItemViewModel.IsSelected property. If you download the sample project, you’ll see that this works: select 3 items in the list, click the “Display Selection Count” button and you’ll see that the ViewModel now knows what the user has selected:

three-items-selected

(Yes, I know we shouldn’t be calling MessageBox.Show from our ViewModel, but this is for illustration purposes only.)

But hold on, what’s this? There are 200 items in that ListBox, but if you press Ctrl+A to select all items, and then click “Display Selection Count” you’ll see this:

nineteen-items-selected

19 Items? The problem we’re seeing is related to a WPF feature called UI virtualization. This feature basically means that WPF only creates ListBoxItems for the items currently visible. Because only visible ListBoxItems are being created, the style we set up to keep the View and ViewModel in sync fails.

Now, we can turn off UI virtualization, but it’s actually a very useful feature, particularly when working with large amounts of complex data, so we need another solution. I’ll explain that (and its associated problems) in my next blog post.