Introduction
WPF is great to quickly build tools because it’s very versatile. In this article, we’re going to disregard the WPF DataGrid’s row selection functionality, and create a DataGrid that allows (a) selection of individual items using a checkbox, and (b) selection of all/none using a master checkbox:
We’re going to do this using the Model-View-ViewModel approach, which you should be already familiar with (at least in theory) before reading on.
The source code for this article is available at the Gigi Labs BitBucket repository.
Setting up MVVM
After creating a new WPF application, install the MVVM Light Toolkit using NuGet. install the libs-only version to avoid getting a lot of extra junk in your project:
Install-Package MvvmLightLibs
Create a class called MainWindowViewModel, and make it public:
public class MainWindowViewModel
{
}
In MainWindow.xaml.cs (the codebehind file for the main window), set up the view model as the DataContext:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
A Model Object
We need to create a data object (Country in this case) whose fields will be displayed in the DataGrid. For this sort of thing, it typically needs to have:
- An ID. We won’t use this here, but if your data is coming from a database, you’ll usually need it.
- A name. This will be displayed in the DataGrid.
- A selection boolean. This will be used to indicate whether the item has been selected or not, and we will bind the checkbox to it.
Create a class called Country. It needs to be public, and it also needs to derive from GalaSoft.MvvmLight.ObservableObject:
public class Country : ObservableObject
This will allow us to call the handy Set()
method which handles all the INotifyPropertyChanged
stuff and makes sure that WPF’s data binding engine gets notified that the property has changed, so that it can update the UI:
public class Country : ObservableObject
{
private int id;
private string name;
private bool selected;
public int Id
{
get
{
return this.id;
}
set
{
this.Set(() => Id, ref id, value);
}
}
public string Name
{
get
{
return this.name;
}
set
{
this.Set(() => Name, ref name, value);
}
}
public bool Selected
{
get
{
return this.selected;
}
set
{
this.Set(() => Selected, ref selected, value);
}
}
public Country(int id, string name)
{
this.id = id;
this.name = name;
}
}
The need to do the INotifyPropertyChanged
bit prevents us from using autoproperties when implementing WPF models (and adds a significant amount of boilerplate), but the MVVM Light Toolkit reduces this overhead as much as possible.
Setting Up the DataGrid
Let’s add a list of countries to our MainWindowViewModel:
public class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<Country> Countries { get; }
public MainWindowViewModel()
{
var countries = new Country[]
{
new Country(1, "Japan"),
new Country(2, "Italy"),
new Country(3, "England"),
new Country(4, "Norway"),
new Country(5, "Poland")
};
this.Countries = new ObservableCollection<Country>(countries);
}
}
Here, we’re inheriting from GalaSoft’s own ViewModelBase class, which gives us access to various methods we typically need when doing WPF with MVVM (we’ll use a couple of these later). Other than that, we’re simply creating an ObservableCollection
of countries. ObservableCollection
is great because it automatically notifies the UI that an item has been added or removed. Although it’s not strictly necessary here, it’s quite ubiquitous in WPF code that uses MVVM.
Next, let’s add the actual DataGrid in MainWindow.xaml:
<Window x:Class="WpfDataGridSelectAll.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfDataGridSelectAll"
mc:Ignorable="d"
Title="Choose Countries" Height="350" Width="525">
<DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding Path=Countries, Mode=OneWay}">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox HorizontalAlignment="Center"
IsChecked="{Binding Path=Selected,
UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Country" Binding="{Binding Path=Name}" />
</DataGrid.Columns>
</DataGrid>
</Window>
Note that I could have used a DataGridCheckBoxColumn for the checkbox, but I didn’t. That’s because there’s this annoying problem where you need 2 clicks to select a checkbox in a DataGrid (one to give the row focus, and the other to check the checkbox). A simple solution for this issue is to use a DataGridTemplateColumn instead, and set UpdateSourceTrigger=PropertyChanged
.
So now we have a simple DataGrid:
Basic Select All
Let’s add a checkbox in the header of the checkbox column. This will act as the master checkbox. When you click this, it will toggle all the other checkboxes.
<DataGridTemplateColumn>
<DataGridTemplateColumn.Header>
<CheckBox IsChecked="{Binding Path=DataContext.AllSelected,
UpdateSourceTrigger=PropertyChanged,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Window}}}" />
</DataGridTemplateColumn.Header>
The binding is a little elaborate because it needs to look for the AllSelected
property (which we’ll implement next) on the MainWindowViewModel, which is actually the DataContext of the window.
Next, in the MainWindowViewModel, let’s add a property that the above binding will work with:
private bool allSelected;
public bool AllSelected
{
get
{
return this.allSelected;
}
set
{
this.Set(() => AllSelected, ref allSelected, value);
foreach (var country in this.Countries)
country.Selected = value;
}
}
Note how in the setter, we’re doing some extra logic. When we change the checked value of the master checkbox, we go in and change the Selected
state of all the countries accordingly.
This is enough to give us a working Select All/None toggle:
If you click the top checkbox, all the others will be checked. And if you uncheck it, all the others will be unchecked.
Full Select All
What we did in the previous section has a flaw:
If you check the master checkbox, but then uncheck one of the other checkboxes, the master checkbox remains checked! And likewise, if start with nothing checked, and check all of them one by one, this is not reflected in the master checkbox.
In order to make this work properly, the individual checkboxes need a way to tell the view model that their state has changed, so that it can update the master checkbox as needed. One way to do this is to have the Country take a function that it will call whenever the selection state changes:
public class Country : ObservableObject
{
private int id;
private string name;
private bool selected;
private Action<bool> onSelectionChanged;
// ...
public bool Selected
{
get
{
return this.selected;
}
set
{
this.Set(() => Selected, ref selected, value);
this.onSelectionChanged(value);
}
}
public Country(int id, string name, Action<bool> onSelectionChanged)
{
this.id = id;
this.name = name;
this.onSelectionChanged = onSelectionChanged;
}
}
In the MainWindowViewModel, we now need to update the Country initialisation to pass in a method which we’ll create next:
public MainWindowViewModel()
{
var countries = new Country[]
{
new Country(1, "Japan", OnCountrySelectionChanged),
new Country(2, "Italy", OnCountrySelectionChanged),
new Country(3, "England", OnCountrySelectionChanged),
new Country(4, "Norway", OnCountrySelectionChanged),
new Country(5, "Poland", OnCountrySelectionChanged)
};
this.Countries = new ObservableCollection<Country>(countries);
}
The method that gets called whenever a checkbox changes needs to cater for two edge cases:
- Master checkbox is checked (i.e. all are selected), but a checkbox just got unchecked. The master checkbox needs to be unchecked.
- Master checkbox is unchecked (i.e. not all are selected), and all checkboxes are now checked. The master checkbox needs to be checked.
Here’s the implementation:
private void OnCountrySelectionChanged(bool value)
{
if (allSelected && !value)
{ // all are selected, and one gets turned off
allSelected = false;
RaisePropertyChanged(() => this.AllSelected);
}
else if (!allSelected && this.Countries.All(c => c.Selected))
{ // last one off one gets turned on, resulting in all being selected
allSelected = true;
RaisePropertyChanged(() => this.AllSelected);
}
}
Note how we are not setting the AllSelected
property directly, as that would execute the foreach
statement that affects the other checkboxes. Instead, we are bypassing this by setting the backing field, and notifying WPF’s data binding engine that it needs to get the latest value for the AllSelected
property.
And this gives us a fully working master checkbox: