Bind to SelectedItems from DataGrid or ListBox in MVVM

Solution 1:

SelectedItems is bindable as a XAML CommandParameter.

After a lot of digging and googling, I have finally found a simple solution to this common issue.

To make it work you must follow ALL the following rules:

  1. Following Ed Ball's suggestion', on you XAML command databinding, define CommandParameter property BEFORE Command property. This a very time-consuming bug.

    enter image description here

  2. Make sure your ICommand's CanExecute and Execute methods have a parameter of object type. This way you can prevent silenced cast exceptions that occurs whenever databinding CommandParameter type does not match your command method's parameter type.

    private bool OnDeleteSelectedItemsCanExecute(object SelectedItems)  
    {
        // Your code goes here
    }
    
    private bool OnDeleteSelectedItemsExecute(object SelectedItems)  
    {
        // Your code goes here
    }
    

For example, you can either send a listview/listbox's SelectedItems property to you ICommand methods or the listview/listbox it self. Great, isn't it?

Hope it prevents someone spending the huge amount of time I did to figure out how to receive SelectedItems as CanExecute parameter.

Solution 2:

You cannot bind to SelectedItems because it is a read-only property. One fairly MVVM-friendly way to work around this is to bind to the IsSelected property of DataGridRow.

You can set up the binding like this:

<DataGrid ItemsSource="{Binding DocumentViewModels}"
          SelectionMode="Extended">
    <DataGrid.Resources>
        <Style TargetType="DataGridRow">
            <Setter Property="IsSelected"
                    Value="{Binding IsSelected}" />
        </Style>
    </DataGrid.Resources>
</DataGrid>

Then you need to create a DocumentViewModel that inherits from ViewModelBase (or whatever MVVM base class you are using) and has the properties of your Document you want to present in the DataGrid, as well as an IsSelected property.

Then, in your main view model, you create a List(Of DocumentViewModel) called DocumentViewModels to bind your DataGrid to. (Note: if you will be adding/removing items from the list, use an ObservableCollection(T) instead.)

Now, here's the tricky part. You need to hook into the PropertyChanged event of each DocumentViewModel in your list, like this:

For Each documentViewModel As DocumentViewModel In DocumentViewModels
    documentViewModel.PropertyChanged += DocumentViewModel_PropertyChanged
Next

This allows you to respond to changes in any DocumentViewModel.

Finally, in DocumentViewModel_PropertyChanged, you can loop through your list (or use a Linq query) to grab the info for each item where IsSelected = True.

Solution 3:

With a bit of trickery you can extend the DataGrid to create a bindable version of the SelectedItems property. My solution requires the binding to have Mode=OneWayToSource since I only want to read from the property anyway, but it might be possible to extend my solution to allow the property to be read-write.

I assume a similar technique could be used for ListBox, but I haven't tried it.

public class BindableMultiSelectDataGrid : DataGrid
{
    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.Register("SelectedItems", typeof(IList), typeof(BindableMultiSelectDataGrid), new PropertyMetadata(default(IList)));

    public new IList SelectedItems
    {
        get { return (IList)GetValue(SelectedItemsProperty); }
        set { throw new Exception("This property is read-only. To bind to it you must use 'Mode=OneWayToSource'."); }
    }

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);
        SetValue(SelectedItemsProperty, base.SelectedItems);
    }
}

Solution 4:

Here is one simple solution. This way you can pass/update any data to ViewModel

Designer.xaml

<DataGrid Grid.Row="1" Name="dgvMain" SelectionChanged="DataGrid_SelectionChanged" />

Designer.cs

ViewModel mModel = null;
public Designer()
{
    InitializeComponent();

    mModel = new ViewModel();
    this.DataContext = mModel;
}

private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    mModel.SelectedItems = dgvMain.SelectedItems;
}

ViewModel.cs

public class ViewModel
{
    public IList SelectedItems { get; set; }
}

Solution 5:

I came here for the answer and I got a lot of great ones. I combined them all into an attached property, very similar to the one offered by Omar above, but in one class. Handles INotifyCollectionChanged and switching the list out. Doesn't leak events. I wrote it so it would be pretty simple code to follow. Written in C#, handles listbox selectedItems and dataGrid selectedItems.

This works on both DataGrid and ListBox.

(I just learned how to use GitHub) GitHub https://github.com/ParrhesiaJoe/SelectedItemsAttachedWpf

To Use:

<ListBox ItemsSource="{Binding MyList}" a:Ex.SelectedItems="{Binding ObservableList}" 
         SelectionMode="Extended"/>
<DataGrid ItemsSource="{Binding MyList}" a:Ex.SelectedItems="{Binding OtherObservableList}" />

And here's the code. There is a little sample on Git.

public class Ex : DependencyObject
{
    public static readonly DependencyProperty IsSubscribedToSelectionChangedProperty = DependencyProperty.RegisterAttached(
        "IsSubscribedToSelectionChanged", typeof(bool), typeof(Ex), new PropertyMetadata(default(bool)));
    public static void SetIsSubscribedToSelectionChanged(DependencyObject element, bool value) { element.SetValue(IsSubscribedToSelectionChangedProperty, value); }
    public static bool GetIsSubscribedToSelectionChanged(DependencyObject element) { return (bool)element.GetValue(IsSubscribedToSelectionChangedProperty); }

    public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.RegisterAttached(
        "SelectedItems", typeof(IList), typeof(Ex), new PropertyMetadata(default(IList), OnSelectedItemsChanged));
    public static void SetSelectedItems(DependencyObject element, IList value) { element.SetValue(SelectedItemsProperty, value); }
    public static IList GetSelectedItems(DependencyObject element) { return (IList)element.GetValue(SelectedItemsProperty); }

    /// <summary>
    /// Attaches a list or observable collection to the grid or listbox, syncing both lists (one way sync for simple lists).
    /// </summary>
    /// <param name="d">The DataGrid or ListBox</param>
    /// <param name="e">The list to sync to.</param>
    private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is ListBox || d is MultiSelector))
            throw new ArgumentException("Somehow this got attached to an object I don't support. ListBoxes and Multiselectors (DataGrid), people. Geesh =P!");

        var selector = (Selector)d;
        var oldList = e.OldValue as IList;
        if (oldList != null)
        {
            var obs = oldList as INotifyCollectionChanged;
            if (obs != null)
            {
                obs.CollectionChanged -= OnCollectionChanged;
            }
            // If we're orphaned, disconnect lb/dg events.
            if (e.NewValue == null)
            {
                selector.SelectionChanged -= OnSelectorSelectionChanged;
                SetIsSubscribedToSelectionChanged(selector, false);
            }
        }
        var newList = (IList)e.NewValue;
        if (newList != null)
        {
            var obs = newList as INotifyCollectionChanged;
            if (obs != null)
            {
                obs.CollectionChanged += OnCollectionChanged;
            }
            PushCollectionDataToSelectedItems(newList, selector);
            var isSubscribed = GetIsSubscribedToSelectionChanged(selector);
            if (!isSubscribed)
            {
                selector.SelectionChanged += OnSelectorSelectionChanged;
                SetIsSubscribedToSelectionChanged(selector, true);
            }
        }
    }

    /// <summary>
    /// Initially set the selected items to the items in the newly connected collection,
    /// unless the new collection has no selected items and the listbox/grid does, in which case
    /// the flow is reversed. The data holder sets the state. If both sides hold data, then the
    /// bound IList wins and dominates the helpless wpf control.
    /// </summary>
    /// <param name="obs">The list to sync to</param>
    /// <param name="selector">The grid or listbox</param>
    private static void PushCollectionDataToSelectedItems(IList obs, DependencyObject selector)
    {
        var listBox = selector as ListBox;
        if (listBox != null)
        {
            if (obs.Count > 0)
            {
                listBox.SelectedItems.Clear();
                foreach (var ob in obs) { listBox.SelectedItems.Add(ob); }
            }
            else
            {
                foreach (var ob in listBox.SelectedItems) { obs.Add(ob); }
            }
            return;
        }
        // Maybe other things will use the multiselector base... who knows =P
        var grid = selector as MultiSelector;
        if (grid != null)
        {
            if (obs.Count > 0)
            {
                grid.SelectedItems.Clear();
                foreach (var ob in obs) { grid.SelectedItems.Add(ob); }
            }
            else
            {
                foreach (var ob in grid.SelectedItems) { obs.Add(ob); }
            }
            return;
        }
        throw new ArgumentException("Somehow this got attached to an object I don't support. ListBoxes and Multiselectors (DataGrid), people. Geesh =P!");
    }
    /// <summary>
    /// When the listbox or grid fires a selectionChanged even, we update the attached list to
    /// match it.
    /// </summary>
    /// <param name="sender">The listbox or grid</param>
    /// <param name="e">Items added and removed.</param>
    private static void OnSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var dep = (DependencyObject)sender;
        var items = GetSelectedItems(dep);
        var col = items as INotifyCollectionChanged;

        // Remove the events so we don't fire back and forth, then re-add them.
        if (col != null) col.CollectionChanged -= OnCollectionChanged;
        foreach (var oldItem in e.RemovedItems) items.Remove(oldItem);
        foreach (var newItem in e.AddedItems) items.Add(newItem);
        if (col != null) col.CollectionChanged += OnCollectionChanged;
    }

    /// <summary>
    /// When the attached object implements INotifyCollectionChanged, the attached listbox
    /// or grid will have its selectedItems adjusted by this handler.
    /// </summary>
    /// <param name="sender">The listbox or grid</param>
    /// <param name="e">The added and removed items</param>
    private static void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // Push the changes to the selected item.
        var listbox = sender as ListBox;
        if (listbox != null)
        {
            listbox.SelectionChanged -= OnSelectorSelectionChanged;
            if (e.Action == NotifyCollectionChangedAction.Reset) listbox.SelectedItems.Clear();
            else
            {
                foreach (var oldItem in e.OldItems) listbox.SelectedItems.Remove(oldItem);
                foreach (var newItem in e.NewItems) listbox.SelectedItems.Add(newItem);
            }
            listbox.SelectionChanged += OnSelectorSelectionChanged;
        }
        var grid = sender as MultiSelector;
        if (grid != null)
        {
            grid.SelectionChanged -= OnSelectorSelectionChanged;
            if (e.Action == NotifyCollectionChangedAction.Reset) grid.SelectedItems.Clear();
            else
            {
                foreach (var oldItem in e.OldItems) grid.SelectedItems.Remove(oldItem);
                foreach (var newItem in e.NewItems) grid.SelectedItems.Add(newItem);
            }
            grid.SelectionChanged += OnSelectorSelectionChanged;
        }
    }
 }