Sunday, 5 February 2012

MVVM and Multiple Selection – Part IV - DataGrid

Download the source for this article.

Previous posts in this series:

Please note, this is WPF only - I don't use Silverlight!

It's been a while since my previous post in this series, MVVM and Multiple Selection – Part III. In the comments to that post, I had a number of people asking about the DataGrid control, however by that point I was no longer in WPF-land and didn't have the time or inclination to investigate properly.

Recently though, I needed to track multiple selections on a DataGrid and as the commenters pointed out, it didn't work. The problem came down to the method I was using to track changes to the ItemsSource property on a control:

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

This doesn't work for DataGrid as the ItemsSource metadata on that control is already overridden, so I needed to find a different approach.

Here's what I came up with:

static void IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    Selector selector = sender as Selector;
    bool enabled = (bool)e.NewValue;

    if (selector != null)
    {
        DependencyPropertyDescriptor itemsSourceProperty =
            DependencyPropertyDescriptor.FromProperty(Selector.ItemsSourceProperty, typeof(Selector));
        IMultiSelectCollectionView collectionView = selector.ItemsSource as IMultiSelectCollectionView;

        if (enabled)
        {
            if (collectionView != null) collectionView.AddControl(selector);
            itemsSourceProperty.AddValueChanged(selector, ItemsSourceChanged);
        }
        else
        {
            if (collectionView != null) collectionView.RemoveControl(selector);
            itemsSourceProperty.RemoveValueChanged(selector, ItemsSourceChanged);
        }
    }
}
Instead of overriding the property metadata globally for all Selectors, I attach a listener to the PropertyDescriptor. Now, the only problem here is that the event handler for PropertyDescriptor changed events are plain old EventHandler delegates, so by the time the property has changed we can't get hold of the old IMultiSelectCollectionView in order to remove the control from it. I get round this by storing all attached controls in a static Dictionary.

This leaves us the problem that we're going to be keeping controls alive when after they're gone, but that's a post for another day. In the meantime, download the code!

10 comments:

  1. Have you overcome the problem where you bind the IsSelected property of the DateGridRow to a boolean IsSelected property to your viewmodel. Then in a select all command, i.e. add a new button which on the click event you go through the items view model and set the IsSelected property to true, this highlights the row in the grid automatically. But you still run into an issue where if RowVirtualization is enabled not all the rows are populated in the SelectedItems. I was able to reproduce this with your sample and was wondering if you had a workaround or solution. Thanks for the great blog posts, very informative.

    ReplyDelete
    Replies
    1. Hi NEO,

      I'm not sure I understand your question correctly, but it sounds like you're referring to the problem outlined in part 1 of this series? If that's the case then the workaround is to use MultiSelectCollectionView - that's the whole purpose of this class, and indeed this series of blog posts!

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. I tried your great solution. It was just wath I needed. Thank you but ...

    Trying to Unselect all the selected items with a ICommand like this ...

    Items.SelectedItems.Clear();
    Items.Refresh();

    ... one Item remain selected.
    What is your opinion about that ?
    Is there another way to unselect all items ?


    thank you
    (Luca from Italy)

    ReplyDelete
    Replies
    1. I found this solution my self.
      In MultiSelectCollectionView.cs added this two methods.
      It seems to work fine.

      public void DeselectAll() {

      // Remove all the selected items
      SelectedItems.Clear();

      // Update the UI controls.
      foreach( Control control in controls )
      SetSelection( (Selector)control );
      }

      public void SelectAll() {

      // Reload all the elements in the selected collection
      SelectedItems.Clear();
      foreach( T item in SourceCollection )
      SelectedItems.Add( item );

      // Update the UI controls.
      foreach( Control control in controls )
      SetSelection( control as Selector );
      }

      Delete
    2. Hi Luca,

      Yes as you have noticed I haven't implemented setting the selection from code as I've never needed it for my own purposes. Please let me know if Barna's solution works for you!

      Delete
    3. Yes it works fine.
      (Barna is my nick name)
      Bye Luca+

      Delete
  4. This is a very good solution.

    Previously I'd been either turning off virtualization for small lists (and taking the performance hit), or rolling my own multi-select in the view models using the Mediator pattern.

    I've been doing this a while, and I'm rarely impressed enough to leave a comment! Good work.

    Pete

    ReplyDelete
  5. Pete, can you provide any information about your Mediator method? Does it perform better than the one outlined above? if so would you be willing to share this code? Thanks for your time...

    ReplyDelete
  6. Hi Steven! Looks like I have spotted a small bug in the demo app. If you select multiple items by holding down the mouse button and moving the pointer down the list, and then you move it back up to deselect the selected items, you'll end up with some of the items still being selected on the other DataGrid?

    Is there a way to fix this?

    ReplyDelete