Reputation: 4591
How can I select multiple items from a DataGrid
in an MVVM WPF project?
Upvotes: 68
Views: 71627
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
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
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
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
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
Reputation: 1542
You can add the "IsSelected" property in the Model and add a checkBox in the row.
Upvotes: 2
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
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.
Upvotes: 0
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