Wednesday 6 March 2013

Checkbox header column using AttachedProperty

        During our project some time ago, we've got a new requirement introduced. The requirement was to extend datagrids across the application, by adding a selection column, checkbox selection column. It's a boring and daunting task to change every view and viewmodel to support this new scenerio. Hence I opted for a more elegant, simple and automated way of doing things. I chose attached properties as the method of choice, bear in mind you can make this functionality work in a completely different fashion. Whatever suits you. I'd like to also add, that I do use indentation and curly braces in most of my code. Here for the sake of brevity I've let myself be a little bit less strict with the style of coding.

Code Snippet
  1. public static class SelectionColumnExtension
  2. {
  3.  
  4. public static bool GetSelectionColumn(DependencyObject obj)
  5. {
  6.     return (bool)obj.GetValue(SelectionColumnProperty);
  7. }
  8.  
  9. public static void SetSelectionColumn(DependencyObject obj, bool value)
  10. {
  11.     obj.SetValue(SelectionColumnProperty, value);
  12. }
  13.  
  14. // Selected column dependency property
  15. public static readonly DependencyProperty SelectionColumnProperty =
  16.     DependencyProperty.RegisterAttached("SelectionColumn",
  17.     typeof(bool),
  18.     typeof(SelectionColumnExtension),
  19.     new UIPropertyMetadata(false, OnSelectionColumnSet));

        Above is the attached property (added quickly with the good old "propa" visual studio snippet) which will be settable/bindable from XAML as the switch to add selection checkbox column to the datagrid.

Code Snippet
  1. // Selected column setup
  2. public static void OnSelectionColumnSet(DependencyObject obj,
  3.             DependencyPropertyChangedEventArgs args)
  4. {
  5.     var dataGrid = obj as DataGrid;
  6.     if (dataGrid != null && (bool)args.NewValue == true)
  7.     {
  8.         DataGridCheckBoxColumn column = new DataGridCheckBoxColumn();
  9.         column.Binding = new Binding("IsChecked");
  10.         DataTemplate dataTemplate = GetHeaderTemplate(dataGrid);
  11.         column.HeaderTemplate = dataTemplate;
  12.         dataGrid.Columns.Add(column);
  13.     }
  14. }

        Next is the callback method, that gets called every time "SelectionColumn" property changes. As you can see I didn't implement column removal without the need of such requirements. It is quite an easy task though and might get implemented later on.
       Binding constructor is set to "IsChecked" property by default - this is the property in your ViewModel to which checkboxes in the checkbox column will bind.

Code Snippet
  1. public static DataTemplate GetHeaderTemplate(DataGrid datagrid){
  2.     DataTemplate dataTemplate = new DataTemplate();
  3.     FrameworkElementFactory factory =
  4.         new FrameworkElementFactory(typeof(Grid));
  5.     FrameworkElementFactory cbFactory =
  6.         new FrameworkElementFactory(typeof(CheckBox));
  7.     Binding checkBoxBinding =
  8.         new Binding("GlobalIsChecked");
  9.     cbFactory.SetBinding(CheckBox.IsCheckedProperty, checkBoxBinding);
  10.     cbFactory.AddHandler(CheckBox.CheckedEvent,
  11.         new RoutedEventHandler(OnGlobalChecked));
  12.     cbFactory.AddHandler(CheckBox.UncheckedEvent,
  13.         new RoutedEventHandler(OnGlobalUnchecked));
  14.     cbFactory.SetValue(CheckBox.TagProperty, datagrid.Items);
  15.     factory.AppendChild(cbFactory);
  16.     dataTemplate.VisualTree = factory;
  17.     return dataTemplate;
  18. }

       We need a DataTemplate for the Checkbox column. We can create visuals in C#  code using FrameworkElementFactory. The most important part of this method is hooking up Checked and Unchecked event handling for the header checkbox and putting Datagrid.Items collection in the Tag of the checkbox for later operations.
       Note that Items property of the DataGrid is of ItemsCollection type and is actually a CollectionView. Because of that by refering this property through Checkbox's tag, we will have constant access to the current list of items in our datagrid without the need to follow changes in the collection. Lastly we need to add Checked and Unchecked events handling, which is pretty straightforward:

Code Snippet
  1. // Changing checked state of checkboxes according to the global checkbox
  2. public static void ChangeCheckboxStateTo(CheckBox cb , bool state)
  3. {          
  4.     if (cb != null)
  5.     {
  6.         ItemCollection ic = cb.Tag as ItemCollection;
  7.         foreach (var item in ic)
  8.         {
  9.             var property = item.GetType().GetProperty("IsChecked");                     
  10.             if (property != null)                    
  11.                 property.SetValue(item, state, null);
  12.         }
  13.     }
  14. }
  15. public static void OnGlobalUnchecked(object sender, RoutedEventArgs e)
  16. {
  17.     CheckBox cb = sender as CheckBox;
  18.     ChangeCheckboxStateTo(cb, false);
  19. }
  20. public static void OnGlobalChecked(object sender, RoutedEventArgs e)
  21. {
  22.     CheckBox cb = sender as CheckBox;
  23.     ChangeCheckboxStateTo(cb, true);
  24. }

Use in xaml:

Code Snippet
  1. <DataGrid ItemsSource="{Binding List3,UpdateSourceTrigger=PropertyChanged}"
  2.           SelectedItem="{Binding Item3}"                              
  3.           Ext:SelectionColumnExtension.SelectionColumn="True">
  4.     <DataGrid.Columns>
  5.         <DataGridTextColumn Binding="{Binding Data1}" Header="Data1"/>
  6.         <DataGridTextColumn Binding="{Binding Data2}" Header="Data2"/>
  7.         <DataGridTextColumn Binding="{Binding Data3}" Header="Data3"/>
  8.     </DataGrid.Columns>
  9. </DataGrid>

That's all you need to do to get this:

No comments:

Post a Comment