WPF/MVVM DataGrid with Select All

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:

wpf-dgselectall-finalresult

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:

  1. An ID. We won’t use this here, but if your data is coming from a database, you’ll usually need it.
  2. A name. This will be displayed in the DataGrid.
  3. 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:

wpf-dgselectall-justdatagrid

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:

wpf-dgselectall-basic

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:

wpf-dgselectall-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:

  1. Master checkbox is checked (i.e. all are selected), but a checkbox just got unchecked. The master checkbox needs to be unchecked.
  2. 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:

wpf-dgselectall-finalresult

6 thoughts on “WPF/MVVM DataGrid with Select All”

  1. Hi,

    Just spent several hours trying to identify why your code doesn’t work for me – turns out that if the DataGrid is inside a TabControl but not on the initial tab, the binding expression for the checkbox in the header just silently fails to evaluate.

    I was able to replicate with your code.

    Do you have any clues what could be causing this? Some gotcha that I’m not aware of?

    e.g.

  2. how to pass the method “OnCountrySelectionChanged”
    —–
    var countries1 = 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)
    };

    var countries = countries1.Select(a => new Country
    {
    Id = a.Id,
    Name = a.Name,
    ??? =OnCountrySelectionChanged

    }).ToList();

    1. You just need to expose a property of the same action type. It’s the same, just that I’m doing it with a constructor, and you (for some reason) want to use property initialisation.

Leave a Reply

Your email address will not be published. Required fields are marked *