Colin Eberhardt's Adventures in .NET

WPF DataGrid – Committing changes cell-by-cell

January 21st, 2009

In my recent codeproject article on the DataGrid I described a number of techniques for handling the updates to DataTables which are bound to the grid. These examples all worked on the assumption that you want to keep your database synchronised with the DataGrid, with changes being committed on a row-by-row basis, i.e. when the user finishes editing a row the changes are written to the database. The WPF DataGrid operates in a row-oriented manner making this a relatively straightforward scenario to implement.

However, what if you want to commit changes on a cell-by-cell basis? Firstly, lets have a look at the problem in a bit more detail. The following code displays a DataGrid, together with a ‘details’ view. Note that IsSynchronizedWithCurrentItem is set to true so that the details view and currently selected item within the grid will remain synchronised:

<DockPanel DataContext="{Binding Source={StaticResource PersonData}}">
 
  <Border DockPanel.Dock="Bottom" Padding="10">
    <Grid x:Name="RootElement">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="1.8*"/>
      </Grid.ColumnDefinitions>
 
      <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
      </Grid.RowDefinitions>
 
      <Label Content="Forename:"/>
      <TextBox Grid.Column="1" Text="{Binding Forename}"/>
 
      <Label Grid.Row="1" Content="Surname:"/>
      <TextBox  Grid.Row="1" Grid.Column="1" Text="{Binding Surname}"/>
 
      <Label Grid.Row="2"  Content="Age:"/>
      <TextBox Grid.Row="2"   Grid.Column="1" Text="{Binding Age}"/>
    </Grid>
  </Border>
 
  <dg:DataGrid ItemsSource="{Binding}" Name="dataGrid"
         IsSynchronizedWithCurrentItem="true"/>
</DockPanel>

The ‘PersonData’ in this case is a DataTable that is constructed in the code-behind.

Now, with this example, if you make changes to a persons surname, then click on the age cell and make changes there, the changes in surname are not reflected in the details view below:

rownotcommitted

In the above example the user has entered the surname “Blunt” and has moved onto the age cell, however the changes are not reflected in the details view below.

Why is this?

The reason is that when you bind to a DataTable, you are actually binding to your DataTable’s DefaultView, which is of type DataView. As a result, each row of your table will be bound to a DataRowView. If you look at the documentation for DataRowView you will find that it implements the IEditableObject interface which is the significant factor here. This interface allows you to perform trasnactional changes to your object, i.e. you can change the object’s properties within a ‘transaction’, then commit then all in a single atomic action. By default, when you bind to a DataGrid this occurs when the user finishes editing a row, either by moving focus or hitting Enter. In order to allow cell-by-cell changes, we need to commit each time the user moves from one cell to the next in the currently selected row.

The DataGrid exposes a CellEditEnding event which looks like a good candidate for this, from the event arguments we can locate the current EditingElement (i.e. the TextBox which now occupies or cell that is in edit mode), the cell’s Column and Row, and from here we can locate the Row.Item whcih is our bound DataRowView. You might think that we can just commit the change in this event handler, however, this is an ‘Ending’ event, not an ‘Ended’ event. In other words the value has not yet been written to the row. This catches me out far too often – as does its RowEditEnding counterpart!

So, if we cannot commit the edit here, how about the CurrentCellChanged event? however this event does not tell us which cell we just left. Although a simple combination of the two provides the effect we are after:

private DataRowView rowBeingEdited = null;
 
private void dataGrid_CellEditEnding(object sender,
                                  DataGridCellEditEndingEventArgs e)
{
    DataRowView rowView = e.Row.Item as DataRowView;
    rowBeingEdited = rowView;
}
 
private void dataGrid_CurrentCellChanged(object sender, EventArgs e)
{
    if (rowBeingEdited != null)
    {
        rowBeingEdited.EndEdit();
    }
}

With this change in place, changes are committed cell-by-cell and the two view remain synchronised:

cellsynchronized

You can download an example project, wpfdatagridcellbycellcommits, changing the file extension from .doc to .zip.

Regards, Colin E.

WPF DataGrid – detecting the column, cell and row that has been clicked

December 2nd, 2008

The WPF DataGrid is a very flexible tool, however in its current state certain simple tasks can prove to be rather tricky. A fairly common task when working with DataGrid is detecting which row, or cell a user has clicked on, or whether they clicked a column header. You might expect this information to be readily available in the form of events, after all, the Windows Forms DataGridView has CellClicked and ColumnHeaderMouseClick events (among many others). However, sadly this is not the case. In order to implement this behaviour you have to understand the visual tree of the DataGrid and how it can be navigated.

Let’s say for example you wanted to find the DataGrid item (cell, row, header) that was clicked when the right mouse button is released. Firstly, we add an event handler for the mouse click in our code-behind:

XAML:

<dg:DataGrid Name="DataGrid"
             MouseRightButtonUp="DataGrid_MouseRightButtonUp"/>

C#:

private void DataGrid_MouseRightButtonUp(object sender,
                                        MouseButtonEventArgs e)
{
}

The above event is a ‘bubbling’ event, which means that it it started on the element that was originally clicked (for example a TextBlock which renders the cell’s value within a DataGridCell), then bubbled up the logical tree until it reaches our event handler in the Window. The e.OriginalSource property gives us access to the element that initiated this event.

The problem is that while we have access to the lement which was clicked on, this element is part of the control or data template of the element that we are really interested, the cell or header. The WPF rich-content model means that our cells could contain all sorts of visual element, therefore we have no way of guessing exactly what e.OriginalSource will be. However, the one thing of which we can be certain is that this element is a child of the element which we are interested in.

If you place a bearkpoint within yoru event handler, you can then use the excellent Mole debug visualiser to locate the clicked element within the visual tree as illustrated below:

dgvisualtree

As you can see, the visual tree is a complex beast! I have highlighted the items of interest:

  1. The TextBlock, which is the element I clicked on, which is e.OriginalSource parameter.
  2. The DataGridCell, the cell which was clicked on.
  3. The DataGridRow which the cell belongs to.
  4. And finally, the DataGrid.

Therefore, in order to locate the cell and row that was clicked on we must navigate up the Visual Tree, searching by type:

private void DataGrid_MouseRightButtonUp(object sender,
                                                  MouseButtonEventArgs e)
{
    DependencyObject dep = (DependencyObject)e.OriginalSource;
 
    // iteratively traverse the visual tree
    while ((dep != null) &amp;&amp;
            !(dep is DataGridCell) &amp;&amp;
            !(dep is DataGridColumnHeader))
    {
        dep = VisualTreeHelper.GetParent(dep);
    }
 
    if (dep == null)
        return;
 
    if (dep is DataGridColumnHeader)
    {
        DataGridColumnHeader columnHeader = dep as DataGridColumnHeader;
        // do something
    }
 
    if (dep is DataGridCell)
    {
        DataGridCell cell = dep as DataGridCell;
        // do something
    }
}

Fantastic, we have now have our header and cell. All that’s left to do is extract the cell’s row and column indices, and cell value. Wait a minute … where are the cell.RowIndex and cell.ColumnIndex properties? It looks like there’s more work to be done.

Once we have navigated up the tree to the DataGridCell, we can continue our journey upwards to obtain the DataGridRow:

if (dep is DataGridCell)
{
    DataGridCell cell = dep as DataGridCell;
 
    // navigate further up the tree
    while ((dep != null) &amp;&amp; !(dep is DataGridRow))
    {
        dep = VisualTreeHelper.GetParent(dep);
    }
 
    DataGridRow row = dep as DataGridRow;
}

Does the DataGridRow have a RowIndex property? I think you can guess the answer to that question.

The DataGrid is an ItemsControl – WPF users are probably most familiar with the ListView which is also an ItemsControl whcih has a number of similarities with the DataGrid. In the ItemsControl terminology, the DataGridRow is an ItemContainer and the DataGrid has an ItemContainerGenerator associated with it for generating the rows. I don’t want to go into the details of how ItemContainers work, Dr. WPF has a good series on the ItemsControl for those who are interested. The following code can be used to determine the index of a row:

private int FindRowIndex(DataGridRow row)
{
    DataGrid dataGrid =
        ItemsControl.ItemsControlFromItemContainer(row)
        as DataGrid;
 
    int index = dataGrid.ItemContainerGenerator.
        IndexFromContainer(row);
 
    return index;
}

Now that we have the row index, the column index is thankfully a little easier to locate, cell.Column.DisplayIndex does the trick. The final piece of information which we might like is the cell value. Is there a cell.Value properly? don’t make me laugh!

The following method determines the property binding for the cells column, then extracts the value from the data items associated with the row:

private object ExtractBoundValue(DataGridRow row,
                                 DataGridCell cell)
{
    // find the column that this cell belongs to
    DataGridBoundColumn col =
       cell.Column as DataGridBoundColumn;
 
    // find the property that this column is bound to
    Binding binding = col.Binding as Binding;
    string boundPropertyName = binding.Path.Path;
 
    // find the object that is related to this row
    object data = row.Item;
 
    // extract the property value
    PropertyDescriptorCollection properties =
        TypeDescriptor.GetProperties(data);
 
    PropertyDescriptor property = properties[boundPropertyName];
    object value = property.GetValue(data);
 
    return value;
}

Putting it all together, this blog post has a small smaple application which displays the header index and value, or cell’s row/column indices and value in response to a right mouse click:

clickedvalue

The sample project can be download, wpfdatagridmouseclicks, changing the file extension from .doc to .zip.

Regards, Colin E.

Multiselect DataGrid with CheckBoxes

November 26th, 2008

I am currently very interested in the new WPF DataGrid which was released on codeplex recently. Someone posted an interesting question in the codeplex forums asking about whether it would be possible to configure the DataGrid so that a user can make multiple row selections via checkboxes which are associated with each row. I thought that this sounded like an excellent idea – afterall, the standard behaviour of ctrl-leftclick might be intuitive to the computer savvy, however you can bet the average user (which is certainly the majority) does not know this.

Fortunately the solution is quite simple to implement in WPF:

<dg:DataGrid ItemsSource="{Binding}">
  <dg:DataGrid.RowHeaderTemplate>
    <DataTemplate>
      <Grid>
        <CheckBox IsChecked="{Binding Path=IsSelected, Mode=TwoWay,
                  RelativeSource={RelativeSource FindAncestor,
                  AncestorType={x:Type dg:DataGridRow}}}"/>
      </Grid>
    </DataTemplate>
  </dg:DataGrid.RowHeaderTemplate>
</dg:DataGrid>

The above code provides a DataTemplate for the RowHeader allowing us to render a CheckBox for each row. The IsChecked property uses a RelativeSource binding which navigates the Visual Tree to locate the first ancestor of type DataGridRow. From here the IsSelected property which dictates that the selected state of the row is available. The binding is TwoWay so that ctrl-leftclick behaviour is still visible.

The result is illustrated below:

multiselect

The above example is a concise illustration of the beauty of WPF. Performing the above customisation of the DataGridView in Windows Forms would be at least an afternoons work. However the solution is not without its problems, if you try the above you will find that the checkboxes are rather difficult to click on because the mouse cursor will be displaying the up-down arrow that indicates that it is currently over the gripper that allows you to specify the row height.

Finding the cause of this means delving deep into the DataGrid control templates …

The template for the DataGridRowHeader is given below (in edited form):

<Style x:Key="{x:Type dgp:DataGridRowHeader}"
       TargetType="{x:Type dgp:DataGridRowHeader}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type dgp:DataGridRowHeader}">
        <Grid>
          ... snipped header content + validation error indicator ...
          <Thumb x:Name="PART_TopHeaderGripper"
                 VerticalAlignment="Top"
                 Style="{StaticResource RowHeaderGripperStyle}"/>
          <Thumb x:Name="PART_BottomHeaderGripper"
                 VerticalAlignment="Bottom"
                 Style="{StaticResource RowHeaderGripperStyle}"/>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

The header template includes a pair of grippers. The visible state of these is toggled depending on the row’s location (i.e. there is no top gripper on the first row), and whether the DataGrid CanResizeRows is true.

The RowHeaderGripperStyle specifies a Transparent background for the grippers which renders them invisible. If we change this to Green we can see the culprits:

datagridselectproblem

In order to allow our Checkboxes to be clickable, we simply reduce the height of the grippers as follows:

<Style x:Key="RowHeaderGripperStyle" TargetType="{x:Type Thumb}">
  <Setter Property="Height" Value="2"/>
  <Setter Property="Background" Value="Green"/>
  <Setter Property="Cursor" Value="SizeNS"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type Thumb}">
        <Border Padding="{TemplateBinding Padding}"
                Background="{TemplateBinding Background}"/>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

Unfortunately this means that we have to duplicate the DataGrid parts; the DataGridRowHeader template and the above style, in order to perform this customisation. The Beauty of XAML and a Beast of a control template!

You can download the demo project, wpfdatagridmultiselect, changing the file extension from .doc to .zip.

Regards, Colin E.

P.S. You can achieve the same effect with a ListView as detailed in this blog post.