Wednesday, 23 March 2011

Single-click editing for checkboxes in WPF and Silverlight datagrids

Possibly the single most annoying thing about the (otherwise excellent) datagrid in WPF and Silverlight is that the check box column requires two clicks to actually change state.  There’s a perfectly good reason for this – the cell must be selected before editing – but the user experience is a bit irritating.  I first came across this problem in WPF and found a solution, only to discover that the same thing wouldn’t work in Silverlight.  So this post is more of an aide memoire to myself for the next time this happens.

I should add that this isn’t completely original code, more of a piecing together of code from much cleverer people on the Silverlight forums and Stack Overflow, but it works happily with MVVM/data binding.  Both versions use the FindVisualChildren method mentioned in this post:

public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj)
    where T : DependencyObject
{
    if (depObj != null)
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
            if (child != null && child is T)
            {
                yield return (T)child;
            }

            foreach (T childOfChild in FindVisualChildren<T>(child))
            {
                yield return childOfChild;
            }
        }
    }
}

The WPF version:

public static class DataGridCellHelper
{
    #region IsSingleClickInCell

    public static readonly DependencyProperty IsSingleClickInCellProperty =
        DependencyProperty.RegisterAttached("IsSingleClickInCell",
        typeof(bool), typeof(DataGrid),
        new FrameworkPropertyMetadata(false, OnIsSingleClickInCellSet));

    public static void SetIsSingleClickInCell(UIElement element, bool value)
    {
        element.SetValue(IsSingleClickInCellProperty, value);
    }

    public static bool GetIsSingleClickInCell(UIElement element)
    {
        return (bool)element.GetValue(IsSingleClickInCellProperty);
    }

    private static void OnIsSingleClickInCellSet(DependencyObject sender,
        DependencyPropertyChangedEventArgs e)
    {
        if (!(bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
        {
            if ((bool)e.NewValue)
            {
                var dataGrid = sender as DataGrid;
                Debug.Assert(dataGrid != null);

                EventManager.RegisterClassHandler(typeof(DataGridCell),
                    DataGridCell.PreviewMouseLeftButtonUpEvent,
                    new RoutedEventHandler(OnPreviewMouseLeftButtonDown));
            }
        }
    }

    private static void OnPreviewMouseLeftButtonDown(object sender, RoutedEventArgs e)
    {
        DataGridCell cell = sender as DataGridCell;

        if (cell != null && !cell.IsEditing && !cell.IsReadOnly)
        {
            var checkBoxes = ControlHelper.FindVisualChildren<CheckBox>(cell);

            if (checkBoxes != null && checkBoxes.Count() > 0)
            {
                foreach (var checkBox in checkBoxes)
                {
                    if (checkBox.IsEnabled)
                    {
                        checkBox.Focus();
                        checkBox.IsChecked = !checkBox.IsChecked;

                        var bindingExpression = checkBox.GetBindingExpression(CheckBox.IsCheckedProperty);

                        if (bindingExpression != null)
                        {
                            bindingExpression.UpdateSource();
                        }
                    }
                    break;
                }
            }
        }
    }

    #endregion
}

The Silverlight version:

public static class DataGridCellHelper
{
    #region IsSingleClickInCell

    public static readonly DependencyProperty IsSingleClickInCellProperty =
        DependencyProperty.RegisterAttached("IsSingleClickInCell",
        typeof(bool), typeof(DataGrid),
        new PropertyMetadata(false, OnIsSingleClickInCellSet));

    public static void SetIsSingleClickInCell(UIElement element, bool value)
    {
        element.SetValue(IsSingleClickInCellProperty, value);
    }

    public static bool GetIsSingleClickInCell(UIElement element)
    {
        return (bool)element.GetValue(IsSingleClickInCellProperty);
    }

    public static void OnIsSingleClickInCellSet(DependencyObject sender,
        DependencyPropertyChangedEventArgs e)
    {
        if (!(bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(
            typeof(DependencyObject)).DefaultValue))
        {
            if ((bool)e.NewValue)
            {
                var dataGrid = sender as DataGrid;
                Debug.Assert(dataGrid != null);

                dataGrid.MouseLeftButtonUp += OnDataGridMouseLeftButtonUp;
            }
        }
    }

    private static void OnDataGridMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        var dataGrid = sender as DataGrid;
        Debug.Assert(dataGrid != null);

        var hitPoint = e.GetPosition(null);
        var controlsInRange = VisualTreeHelper.FindElementsInHostCoordinates(
                                    hitPoint, dataGrid);
        var dataGridCellInRange = (from element in controlsInRange
                                    where element is DataGridCell
                                    select element).FirstOrDefault();

        if (dataGridCellInRange != null)
        {
            HandleSingleClickCheckBox(dataGridCellInRange);
        }
    }

    private static void HandleSingleClickCheckBox(UIElement dataGridCellInRange)
    {
        var checkBox = ControlHelper.FindVisualChildren<CheckBox>(
            dataGridCellInRange).FirstOrDefault();

        if (checkBox != null)
        {
            if (checkBox.IsEnabled)
            {
                checkBox.Focus();
                checkBox.IsChecked = !checkBox.IsChecked;

                var bindingExpression = checkBox.GetBindingExpression(CheckBox.IsCheckedProperty);

                if (bindingExpression != null)
                {
                    bindingExpression.UpdateSource();
                }
            }
        }
    }

    #endregion
}

Usage is the same in both (where p is the reference to the namespace that the DataGridCellHelper class is in):

<DataGrid p:DataGridCellHelper.IsSingleClickInCell="True" />

This should ensure that any checkbox columns respond to a single click, even if data bound.

4 comments:

  1. Finally someone actually solved the problem. Thanks a lot!

    ReplyDelete
  2. Thank for your solution. It's so great

    ReplyDelete
  3. There is an easier solution here: http://tapiocacom.blogspot.com.br/2013/04/checkbox-on-datagrid-with-single-click.html

    no C# code required, just XAML.

    ReplyDelete
    Replies
    1. You're quite right - your XAML solution is both easier and more elegant. Kudos :-)

      The reason I went for the rather convoluted solution that I did is because I needed dynamic generation of columns so that I could do pivoting (like here).

      With the benefit of hindsight (and half an hour spare) I could probably force dynamically generated columns to use your data template checkbox using data template binding. Next time!

      Delete