Mittwoch, 11. Januar 2012

DataGrid with DataGridCheckBoxColumn and Header to select/deselect all CheckBoxes

The topic says what our target is:


Because we’d like to stick to the MVVM pattern, first of all there should be a ViewModel base class which implements the INotifyPropertyChanged-interface:


  public class ViewModelBase : INotifyPropertyChanged
  {
    #region INotifyPropertyChanged
    protected void OnPropertyChanged(string propertyName)
    {
      if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion INotifyPropertyChanged
  }

Next there is a basic class for each row:

  public class RowViewModelClass : ViewModelBase
  {
    private bool _bSelected = false;
    public bool Selected
    {
      get { return this._bSelected; }
      set
      {
        if (this._bSelected != value)
        {
          this._bSelected = value;
          this.OnPropertyChanged("Selected");
        }
      }
    }

    // all the other stuff for each Row...
  }

To save some work (and for simplicity) we implement an ObservableCollection of that basic class (based on http://dzaebel.net/WpfObservable.htm):


  public class RowCollection : ObservableCollection<RowViewModelClass>
  {
    protected override void InsertItem(int index, RowViewModelClass item)
    {
      base.InsertItem(index, item);
      this[index].PropertyChanged += new PropertyChangedEventHandler(RowCollection_PropertyChanged);
    }

    void RowCollection_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
      if (this.ItemPropertyChanged != null)
        this.ItemPropertyChanged(sender as RowViewModelClass, e.PropertyName);
    }

    public event Action<RowViewModelClass, string> ItemPropertyChanged;
  }



Some notes: we’d like to react to changes of property Selected of each row. That means: if all CheckBoxes of all rows are selected, and one is deselected, our header for selecting all rows should be deselected, too.
We achieve this by implementing an Action which fires/is executed every time a property of an item is changed (we ignore that this implementation is very basic…removing items for example isn’t even mentioned).

So finally we have our class with the flag to select/deselect all row-values:

  public class ViewModel : ViewModelBase
  {
    private bool _bAllSelected = false;
    public bool AllSelected
    {
      get { return this._bAllSelected; }
      set
      {
        if (this._bAllSelected != value)
        {
          this._bAllSelected = value;
          this.OnPropertyChanged("AllSelected");
        }
      }
    }

    private RowCollection _rcRows = new RowCollection();
    public RowCollection Rows
    {
      get { return this._rcRows; }
    }
  }
That’s our ViewModel. Now follows the Model:
  public class Model
  {
    private ViewModel _viewmodel;

    public void SetViewModel(ViewModel viewmodel)
    {
      this._viewmodel = viewmodel;
      this._viewmodel.PropertyChanged += new PropertyChangedEventHandler(_viewmodel_PropertyChanged);
      this._viewmodel.Rows.ItemPropertyChanged += this.Rows_ItemPropertyChanged;
    }

    void Rows_ItemPropertyChanged(RowViewModelClass item, string property)
    {
      if (property == "Selected"
        && item.Selected == false)
        this._viewmodel.AllSelected = false;
    }

    void  _viewmodel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
      if (e.PropertyName == "AllSelected")
      {
        if (this._viewmodel.AllSelected == true)
          foreach (var item in this._viewmodel.Rows)
            item.Selected = true;
        else
        {
          bool bAllSelected = true;
          foreach (var item in this._viewmodel.Rows)
          {
            if (item.Selected == false)
              bAllSelected = false;
          }
          if (bAllSelected)
            foreach (var item in this._viewmodel.Rows)
              item.Selected = false;
        }
      }
    }
  }

Very basic…we register to all events that are important to us.
The last step: our View:

<Window x:Class="WpfApplication1.View"
        xmlns:util="clr-namespace:WpfApplication1"
        xmlns:controls="clr-namespace:System.Windows.Controls;assembly=PresentationFramework"
        Title="MainWindow" Height="400" Width="600">
  <DataGrid AlternationCount="2"
            AutoGenerateColumns="False"
            CanUserAddRows="False"
            ItemsSource="{Binding Rows}">
    <!--Column Definitions-->
    <DataGrid.Columns>
      <DataGridCheckBoxColumn Binding="{Binding Selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        <DataGridCheckBoxColumn.Header>
          <DockPanel>
            <Border Name="border"
                    Background="#BAFFFFFF">
              <CheckBox IsChecked="{Binding AllSelected}"/>
            </Border>
          </DockPanel>
        </DataGridCheckBoxColumn.Header>
      </DataGridCheckBoxColumn>
    </DataGrid.Columns>
  </DataGrid>
</Window>

With code behind:

  public partial class View : Window
  {
    public View()
    {
      InitializeComponent();
    }

    public void SetViewModel(ViewModel viewmodel)
    {
      this.DataContext = viewmodel;
      this.border.DataContext = viewmodel;
    }
  }

Now here I have to mention two lines:

      this.DataContext = viewmodel;
      this.border.DataContext = viewmodel;

Why that? Well, when you leave line two, then your Binding in the Header won’t work. The Binding in XAML will not find any DataContext beyond the Border. Also all the stuff with RelativeSource and so on won’t work, the property DataGridCheckBoxColumn.Header seems to be a bit primitive for WPF:
public Object Header { get; set; }
GroupBox.Header for example looks like this:
[BindableAttribute(true)]
[LocalizabilityAttribute(LocalizationCategory.Label)]
public Object Header { get; set; } 
This way or the other: this is my solution for this.

1 Kommentar:

  1. Hi,
    you code is not wrking. Can u provide me with the solution? Thanks

    AntwortenLöschen