Manvinder
Manvinder

Reputation: 4591

Select multiple items from a DataGrid in an MVVM WPF project

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

Upvotes: 68

Views: 71627

Answers (9)

tursza
tursza

Reputation: 11

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

Upvotes: 1

AxdorphCoder
AxdorphCoder

Reputation: 1222

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

Upvotes: 2

Seth Reno
Seth Reno

Reputation: 5450

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>
        ...

Upvotes: 0

Sandesh
Sandesh

Reputation: 3004

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.

Upvotes: 119

123 456 789 0
123 456 789 0

Reputation: 10865

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.

Upvotes: 31

Learner
Learner

Reputation: 1542

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

Upvotes: 2

Allen4Tech
Allen4Tech

Reputation: 2184

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

Upvotes: 23

dylansweb
dylansweb

Reputation: 684

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

Upvotes: 0

Jehof
Jehof

Reputation: 35564

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>

Upvotes: 12

Related Questions