Wednesday, November 2, 2016

Binding to a DataGrid's SelectedItems property

I have a requirement to only enable a button when there is at least one row selected in a DataGrid. My initial response was to attach the button to a routed command, bind the SelectedItems property of the DataGrid to a collection, and test the collection's count in the CanExecute method. However SelectedItems is not a dependency property for some reason so you cannot bind it in XAML.

We found two solutions to this problem so I will present them both.

1. Create a custom dependency property.

This solution is more work but has the advantage of being easier to consume. I.e. once the work is done, it can be used in many places easily. It is also more 'pure' MVVM.

a. Start by creating a custom class that derives from DataGrid and adds a custom dependency property that can be bound to. Whenever the SelectedItems property changes, the custom dependency property gets changed too.

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

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

    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));
}

b. Now add a "local" reference to the XAML so we can use the custom data grid like this.

<Window x:Class="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"
            Title="MainWindow" Height="350" Width="525">
    <Window.CommandBindings>
        <CommandBinding Command="Copy" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"></CommandBinding>
    </Window.CommandBindings>
    <DockPanel>
        <local:CustomDataGrid ItemsSource="{Binding Model}"
                              SelectionMode="Extended"
                              IsReadOnly="True"
                              AutoGenerateColumns="False"
                              SelectedItemsList="{Binding SelectedItemsList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding}" Width="100" Header="Name"></DataGridTextColumn>
            </DataGrid.Columns>
        </local:CustomDataGrid>
        <Button Content="How Many?" Command="Copy"></Button>
    </DockPanel>

</Window>

c. The code behind looks like this. Note I broke strict MVVM by using a routed command for simplicity. You won't do that, I'm sure.

using System;
using System.Windows;
using System.Windows.Input;

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

    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        if (this.DataContext != null)
            e.CanExecute = (((MyViewModel)this.DataContext).SelectedItemsList.Count > 0);
    }

    private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        MessageBox.Show("You selected " + ((MyViewModel)this.DataContext).SelectedItemsList.Count);
    }
}

d. And here is my ViewModel. Note I used a list of strings so I don't have a Model class. 

using System;
using System.Collections.Generic;

public class MyViewModel
{
    private List<String> _myModel = new List<String> {"Anne","Bob","Connie","David"};

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

    private IList _selectedModels = new ArrayList();

    public IList SelectedItemsList
    {
        get { return _selectedModels; }
        set
        {
            _selectedModels = value;
        }
    }
}

2. Passing SelectedItems through the command parameter.

My colleague used the ability to bind the command parameter so that the command passes the DataGrid's SelectedItems property. It isn't perfect MVVM because the command has to bind to the DataGrid using it's name. I'll use Routed Commands to simplify this example too.

a. Start with the XAML this time. We don't need the local reference but we have to name the grid and reference it in the new CommandParameter of the button.

<Window x:Class="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"
            Title="MainWindow" Height="350" Width="525">
    <Window.CommandBindings>
        <CommandBinding Command="Copy" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"></CommandBinding>
    </Window.CommandBindings>
    <DockPanel>
        <DataGrid x:Name="AvailableNames"
                              ItemsSource="{Binding Model}"
                              SelectionMode="Extended"
                              IsReadOnly="True"
                              AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding}" Width="100" Header="Name"></DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
        <Button Content="How Many?" Command="Copy" CommandParameter="{Binding ElementName=AvailableNames, Path=SelectedItems}"></Button>
    </DockPanel>
</Window>

b. The code behind looks like this. Note how the CanExecute and Executed methods use the command parameter.

using System;
using System.Windows;
using System.Windows.Input;
using System.Collections;

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

    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        if (e.Parameter != null)
            e.CanExecute = ((e.Parameter as IList).Count > 0);
    }

    private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        MessageBox.Show("You selected " + (e.Parameter as IList).Count);
    }
}

c. Lastly the ViewModel is much simpler now.

using System;
using System.Collections.Generic;

public class MyViewModel
{
    private List<String> _myModel = new List<String> { "Anne", "Bob", "Connie", "David" };

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