67

How can I select multiple items from a DataGrid in an MVVM WPF project?

DavidRR
  • 18,291
  • 25
  • 109
  • 191
Manvinder
  • 4,495
  • 16
  • 53
  • 100

9 Answers9

117

You can simply add a custom dependency property to do this:

public class CustomDataGrid : DataGrid
{
    public CustomDataGrid ()
    {
        this.SelectionChanged += CustomDataGrid_SelectionChanged;
    }

    void CustomDataGrid_SelectionChanged (object sender, SelectionChangedEventArgs e)
    {
        this.SelectedItemsList = this.SelectedItems;
    }
    #region SelectedItemsList

    public IList SelectedItemsList
    {
        get { return (IList)GetValue (SelectedItemsListProperty); }
        set { SetValue (SelectedItemsListProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemsListProperty =
            DependencyProperty.Register ("SelectedItemsList", typeof (IList), typeof (CustomDataGrid), new PropertyMetadata (null));

    #endregion
}

Now you can use this dataGrid in the XAML:

<Window x:Class="DataGridTesting.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:local="clr-namespace:DataGridTesting.CustomDatagrid"
    Title="MainWindow"
    Height="350"
    Width="525">
  <DockPanel>
    <local:CustomDataGrid ItemsSource="{Binding Model}"
        SelectionMode="Extended"
        AlternatingRowBackground="Aquamarine"
        SelectionUnit="FullRow"
        IsReadOnly="True"
        SnapsToDevicePixels="True"
        SelectedItemsList="{Binding TestSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
  </DockPanel>
</Window>

My ViewModel:

public class MyViewModel : INotifyPropertyChanged
{
    private static object _lock = new object ();
    private List<MyModel> _myModel;

    public IEnumerable<MyModel> Model { get { return _myModel; } }

    private IList _selectedModels = new ArrayList ();

    public IList TestSelected
    {
        get { return _selectedModels; }
        set
        {
            _selectedModels = value;
            RaisePropertyChanged ("TestSelected");
        }
    }

    public MyViewModel ()
    {
        _myModel = new List<MyModel> ();
        BindingOperations.EnableCollectionSynchronization (_myModel, _lock);

        for (int i = 0; i < 10; i++)
        {
            _myModel.Add (new MyModel
            {
                Name = "Test " + i,
                Age = i * 22
            });
        }
        RaisePropertyChanged ("Model");
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void RaisePropertyChanged (string propertyName)
    {
        var pc = PropertyChanged;
        if (pc != null)
            pc (this, new PropertyChangedEventArgs (propertyName));
    }
}

My model:

public class MyModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}

And finally, here is the code behind of MainWindow:

public partial class MainWindow : Window
{
    public MainWindow ()
    {
        InitializeComponent ();
        this.DataContext = new MyViewModel ();
    }
}

I hope this clean MVVM design helps.

DavidRR
  • 18,291
  • 25
  • 109
  • 191
Sandesh
  • 2,966
  • 1
  • 20
  • 34
  • I tried your code and it works fine when it is selecting the multiple rows but when I implement a delete function, it causes an exception. can you please see this link http://stackoverflow.com/questions/29675086/collection-was-modified-when-trying-to-remove-selected-items-of-datagrid – Emil Apr 16 '15 at 13:55
  • 1
    Looks like you got your answer :) – Sandesh Apr 17 '15 at 03:26
  • This would not work if you want to check if the new selected items list equals the previous in your setter of TestSelected. The reason is that both SelectedItemsList and SelectedItemseason in CustomDataGrid_SelectionChanged contain the same reference to the list. So they will always be equal. – tabina Sep 10 '15 at 11:38
  • @tabina You are right that this solution is useful only when you want to get all the __currently__ selected items and is of no use if you want to compare it with old selected items. What you can do is modify the `PropertyMetadata` of the Dependency Property to include a function which can do the comparison – Sandesh Sep 11 '15 at 05:47
  • I've made an attached property `local:SelectedItems` so I do not need to subclass the `DataGrid`. – xmedeko Apr 08 '16 at 15:38
  • 12
    `this.SelectedItemsList = this.SelectedItems;` did not work for me, as `SelectedItemsList` was always set to `null`. However, changing the code to `foreach (var item in this.SelectedItems) { this.SelectedItemsList.Add(item); }` did the trick. Please note, that this requires you to call `this.SelectedItemsList.Clear();` in advance, so the items of `SelectedItemsList` will not get duplicated. – M463 Apr 18 '16 at 09:32
  • 1
    Why do u have TwoWay binding of SelectedItemsList? I dont think your code can handle changing the SelectedItems property from its Source (VM). – Lukáš Koten Sep 18 '17 at 19:38
  • is there a way to bind it to a typed list? so instead of IList SelectedModel bind to a property of type List – Sjors Miltenburg Nov 24 '17 at 08:48
  • @sjorsmiltenburg just do it like M463 suggested, add the items to the existing list, then it also works with typed lists. – Coden Mar 26 '20 at 13:01
  • Why `TwoWay` if you cannot set the Grid's `SelectedItems`? – Daniel Möller Jun 11 '21 at 01:06
30

What I would do is create Behaviors using System.Windows.Interactivity. You would have to reference it manually in your project.

Given a control which doesn't expose SelectedItems e.g., (ListBox, DataGrid)

You can create a behavior class something like this

public class ListBoxSelectedItemsBehavior : Behavior<ListBox>
{
    protected override void OnAttached()
    {
        AssociatedObject.SelectionChanged += AssociatedObjectSelectionChanged;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.SelectionChanged -= AssociatedObjectSelectionChanged;
    }

    void AssociatedObjectSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var array = new object[AssociatedObject.SelectedItems.Count];
        AssociatedObject.SelectedItems.CopyTo(array, 0);
        SelectedItems = array;
    }

    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.Register("SelectedItems", typeof(IEnumerable), typeof(ListBoxSelectedItemsBehavior), 
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    public IEnumerable SelectedItems
    {
        get { return (IEnumerable)GetValue(SelectedItemsProperty); }
        set { SetValue(SelectedItemsProperty, value); }
    }
}

And on your XAML I would do the Binding like this where i is xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" and behaviors is the namespace of your Behavior class

<ListBox>
 <i:Interaction.Behaviors>
    <behaviors:ListBoxSelectedItemsBehavior SelectedItems="{Binding SelectedItems, Mode=OneWayToSource}" />
 </i:Interaction.Behaviors>

Assuming that your DataContext for the ListBox has the SelectedItems property in the ViewModel then it will automatically update the SelectedItems. You have encapsulated the event subscribing from the View i.e.,

<ListBox SelectionChanged="ListBox_SelectionChanged"/>

You can change the Behavior class to be of type DataGrid if you want.

123 456 789 0
  • 10,565
  • 4
  • 43
  • 72
  • @III What is the purpose of – Manvinder Apr 03 '15 at 12:41
  • @MegaMind I am trying to emphasize that you won't need SelectionChanged event when you used behaviors. – 123 456 789 0 Apr 04 '15 at 01:50
  • 1
    @III I have tried your solution, but in view model, selected items always remains null. I have compiled a simple solution depicting your code. If you could take a look at that. https://www.dropbox.com/s/lh4zrhnatpchqzi/MultiSelectedDataGrid.zip?dl=0 – Manvinder Apr 04 '15 at 04:50
  • If you do this with UWP use IList instead of IList. Nice solution. – Xcalibur37 Oct 10 '15 at 21:28
  • @Edward You are referencing the same answer? – 123 456 789 0 Mar 24 '16 at 21:17
  • 2
    @III - Yes, silly me, I referenced the same answer. I can't edit my bad comment now so I've deleted it - THIS was the one that worked for me: http://stackoverflow.com/a/8088926 . As I said in my old comment (now deleted), your answer didn't work for me, SelectedItems is always null, like for MegaMind. The other answer is almost the same as yours, and it works for me. – Edward Mar 24 '16 at 23:09
  • `DataGrid` HAS a `SelectedItems` property, but it's not a dependency property so binding does not work. > doesn't expose `SelectedItems` is a wrong statement. – Maxence Jan 31 '18 at 17:03
  • 1
    As long as `SelectedItems` is of type `IEnumerable` I also got `null`. But as soon as the type was changed to `IList` the data came. Unfortunately the Binding was not really TwoWay. The solution was a combination between this answer and this [blog entry](https://www.tyrrrz.me/Blog/WPF-ListBox-SelectedItems-TwoWay-binding) – Raphael Müller Feb 20 '18 at 13:34
  • Just repeating @RaphaelMüller comment above: for actually working `TwoWay` binding you can use the solution from [WPF ListBox SelectedItems TwoWay Binding](https://tyrrrz.me/blog/wpf-listbox-selecteditems-twoway-binding). I've just copied and it worked, not combined anything at all in my case. – Brains Mar 06 '20 at 17:06
23

I use this solution in my app:

XAML:

<i:Interaction.Triggers>
     <i:EventTrigger EventName="SelectionChanged">
         <i:InvokeCommandAction Command="{Binding SelectItemsCommand}" CommandParameter="{Binding Path=SelectedItems,ElementName=TestListView}"/>
     </i:EventTrigger>
</i:Interaction.Triggers>

at the top of you xaml file, add this line of code:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

SelectedItemsCommand is ICommand type which is written in your viewmodel.

Used DLL:

System.Windows.Interactivity.dll

Allen4Tech
  • 2,094
  • 3
  • 26
  • 66
12

With the default DataGrid of WPF it is not possible to use a Binding, as it is possible with the SelectedItem-Property, cause the SelectedItems-Property is not a DependencyProperty.

One way to to what you want is to register the SelectionChanged-Event of the DataGrid to update the property of your ViewModel, that stores the selected items.

The property SelectedItems of the DataGrid is of type IList so you need to cast the items in the list to your specific type.

C#

public MyViewModel {
  get{
    return this.DataContext as MyViewModel;
  }
}

private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) {
  // ... Get SelectedItems from DataGrid.
  var grid = sender as DataGrid;
  var selected = grid.SelectedItems;

  List<MyObject> selectedObjects = selected.OfType<MyObject>().ToList();

  MyViewModel.SelectedMyObjects = selectedObjects;
}

XAML

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
    <Grid>
    <DataGrid
        SelectionChanged="DataGrid_SelectionChanged"
        />
    </Grid>
</Window>
Jehof
  • 34,674
  • 10
  • 123
  • 155
  • 1
    Thanks for your answer but I am looking for something pure MVVM based and that should not involved code behind – Manvinder Apr 07 '14 at 10:26
  • I appreciate this answer. Even though it's not MVVM it's the first one to mention _why_ I can't access the `SelectedItems` property in XAML. – Steffen Winkler Apr 11 '17 at 08:59
  • 2
    And it doesn't corrupt your MVVM model at all. You still have a pure SelectedMyObjects property in your VM that knows nothing about the View or how it gets set. Just having some code in the View doesn't mean it's not pure MVVM. – Bill Lefler May 04 '17 at 18:40
2

You can add the "IsSelected" property in the Model and add a checkBox in the row.

Learner
  • 1,490
  • 2
  • 22
  • 35
2

You can maka a reusable generic base class. This way you can select rows both from code and UI.

This is my example class i want to be selectable

public class MyClass
{
    public string MyString {get; set;}   
}

Make generic base class for selectable classes. INotifyPropertyChanged makes the UI update when you set IsSelected.

public class SelectableItem<T> : System.ComponentModel.INotifyPropertyChanged
{
    public SelectableItem(T item)
    {
        Item = item;
    }

    public T Item { get; set; }

    bool _isSelected;

    public bool IsSelected {
        get {
            return _isSelected;
        }
        set {
            if (value == _isSelected)
            {
                return;
            }

            _isSelected = value;

            if (PropertyChanged != null)
            { 
                PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs("IsSelected"));
            }
        }
    }

    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
}

Create selectable class

public class MySelectableItem: SelectableItem<MyClass>
{
    public MySelectableItem(MyClass item)
       :base(item)
    {
    }
}

Create property to bind to

ObservableCollection<MySelectableItem> MyObservableCollection ...

Set propety

MyObservableCollection = myItems.Select(x => new MySelectableItem(x));

Bind to datagrid and add a style on the DataGridRow that binds to the IsSelected propety on the MySelectedItem

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

To get selected rows/items

var selectedItems = MyObservableCollection.Where(x=>x.IsSelected).Select(y=>y.Item);

To select rows/items

MyObservableCollection[0].IsSelected = true;

Edit———> It seems like it does not work when EnableRowVirtualization is true

AxdorphCoder
  • 1,104
  • 2
  • 15
  • 26
  • Unfortunately, this did not work reliably for me when rows that are currently not visible (because they are scrolled out of view) get selected or deselected. – Martin Jan 02 '18 at 14:40
  • It seems like it does not work when EnableRowVirtualization is true – AxdorphCoder Jan 03 '18 at 16:08
0

WPF DataGrid allows for this. Simply set the DataGrid.Rows.SelectionMode and DataGrid.Rows.SelectionUnit to "Extended" and "CellOrRowHeader" respectively. This can be done in Blend, as shown in the image I have included. This will allow user to select each cell, whole rows etc. as many as they like, using either shift or ctrl key to continue selecting. enter image description here

dylansweb
  • 674
  • 4
  • 8
0

The project I'm working on uses MVVM Light and I found this blog post to be the simplest solution. I'll repeat the solution here:

View Model:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
...

public class SomeVm : ViewModelBase {

    public SomeVm() {
        SelectItemsCommand = new RelayCommand<IList>((items) => {
            Selected.Clear();
            foreach (var item in items) Selected.Add((SomeClass)item);
        });

        ViewCommand = new RelayCommand(() => {
            foreach (var selected in Selected) {
                // todo do something with selected items
            }
        });
    }

    public List<SomeClass> Selected { get; set; }
    public RelayCommand<IList> SelectionChangedCommand { get; set; }
    public RelayCommand ViewCommand { get; set; }
}

XAML:

<Window
    ...
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:command="http://www.galasoft.ch/mvvmlight"
    ...
    <DataGrid
        Name="SomeGrid"
        ...
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="SelectionChanged">
                <command:EventToCommand
                    Command="{Binding SelectionChangedCommand}"
                    CommandParameter="{Binding SelectedItems, ElementName=SomeGrid}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
        ...
        <DataGrid.ContextMenu>
            <ContextMenu>
                <MenuItem Header="View" Command="{Binding ViewCommand}" />
            </ContextMenu>
        </DataGrid.ContextMenu>
        ...
Seth Reno
  • 5,350
  • 4
  • 41
  • 44
  • Your code is confusing because in the VM your Command is called SelectionChangedCommand when it's initialised but SelectItemsCommand when it's declared. – Joren Vandamme Jun 01 '23 at 20:04
0

My solution is almost a same like Sandesh. On the other hand, I did not use CustomDataGrid to solve this problem. Instead of that I used a plus button click event with the proper function in a viewmodel. In my code, the main point was that to be able to delete more than one person object from the Datagrid wich was bind to the PeopleList property (ObservableCollection)

This is my Model:

 public class Person
    {
        public Person()
        {

        }

        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }

    }
}

This is my ViewModel (only thoose parts, which are necessary):

public class PersonViewModel : BindableBase
    {
        private ObservableCollection<Person> _peopleList;
        // to be able to delete or save more than one person object
        private List<Person> _selectedPersonList;

        //MyICommand
        public MyICommand AddCommand { get; set; }
        public MyICommand DeleteCommand { get; set; }

        private string _firstName;
        private string _lastName;
        private int _age;

        public PersonViewModel()
        {
            _peopleList = new ObservableCollection<Person>();
            LoadPerson();
            AddCommand = new MyICommand(AddPerson);
            DeleteCommand = new MyICommand(DeletePerson, CanDeletePerson);
            // To be able to delete or save more than one person
            _selectedPersonList = new List<Person>();
        } 
public ObservableCollection<Person> PeopleList
        {
            get { return _peopleList; }
            set
            {
                _peopleList = value;
                RaisePropertyChanged("PeopleList");
            }
        }
 public List<Person> SelectedPersonList
        {
            get { return _selectedPersonList; }
            set
            {
                if (_selectedPersonList != value)
                {
                    RaisePropertyChanged("SelectedPersonList");
                }
            }
        }
 private void DeletePerson()
        {
            // to be able to delete more than one person object
            foreach (Person person in SelectedPersonList)
            {
                PeopleList.Remove(person);
            }
            MessageBox.Show(""+SelectedPersonList.Count); // it is only a checking
            SelectedPersonList.Clear(); // it is a temp list, so it has to be cleared after the button push
        }
 public void GetSelectedPerson(DataGrid datagrid)
        {

            IList selectedItems = datagrid.SelectedItems;
            foreach (Person item in selectedItems)
            {
                SelectedPersonList.Add(item);
            }
        }

My View (xmal):

<UserControl x:Class="DataBase.Views.PersonView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:DataBase.Views"
             xmlns:viewModel="clr-namespace:DataBase.ViewModels" d:DataContext="{d:DesignInstance Type=viewModel:PersonViewModel}"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <StackPanel Orientation="Vertical" Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2" Background="AliceBlue">
            <TextBlock Text="First Name:"/>
            <TextBox x:Name="firstNameTxtBox" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}"/>
            <TextBlock Text="Last Name:"/>
            <TextBox x:Name="lastNameTxtBox" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}"/>
            <TextBlock Text="Age:"/>
            <TextBox x:Name="ageTxtBox" Text="{Binding Age}"/>
            <TextBlock Text="{Binding FullName}"/>
            <Button Content="Add" IsEnabled="{Binding CanAddPerson}" Command="{Binding AddCommand}"/>
            <Button Content="Delete" Command="{Binding DeleteCommand}" Click="Delete_Click"/>
            <DataGrid x:Name="datagridPeopleList" ItemsSource="{Binding PeopleList}" AutoGenerateColumns="True" SelectedItem="{Binding SelectedPerson}" SelectionMode="Extended" SelectionUnit="FullRow"/>
            <!--<ListView Height="50" ItemsSource="{Binding PeopleList}" SelectedItem="{Binding SelectedPerson}" Margin="10">
            </ListView>-->
        </StackPanel>
    </Grid>
</UserControl>

My View (.cs):

 public partial class PersonView : UserControl
    {
        public PersonViewModel pvm;
        public PersonView()
        {
            pvm = new PersonViewModel();
            InitializeComponent();
            DataContext = pvm;
        }

        private void Delete_Click(object sender, RoutedEventArgs e)
        {
            pvm.GetSelectedPerson(datagridPeopleList);
        }
    }

I hope that it is useful and not the worst (non elegant) solution in the world :D

tursza
  • 1