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

This blog post has moved to its new location:

http://www.scottlogic.co.uk/blog/wpf/2008/12/wpf-datagrid-detecting-clicked-cell-and-row/

Please update your bookmarks and post any comments to the article at its new location. Thanks.

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) &&
            !(dep is DataGridCell) &&
            !(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) && !(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 sample 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.

About these ads

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

  1. sreeraj says:

    Very use ful article . I was on research in this for past few days . To get the controls inside datatemplate in a grid cell, i think we can use this way
    ContentPresenter objPresent = Mygrid.Columns[ColumnNumber].GetCellContent(e.Row) as ContentPresenter;

    DependencyObject objdep = objPresent.ContentTemplate.LoadContent();
    from this dependancy object , we can access the control

  2. klauswiesel says:

    Hi Colin

    could you help me out to get the same info for right clicking on a column header?

    Thanks
    Klaus

    • Hi Klaus,

      Have you tried the sample application associated with this post? When you right click on a column header it should output the column index and the property which the column is bound to. e.g.

      “Header clicked [1] = Forename”

      Is that what you wanted?

      [ ---- edit ----]

      Hang on … did you mean the row header?

      If so, simply navigate up the visual tree to the row in the same way that you do for the cell. Try the following code snippet in the sample application:

      DependencyObject dep = (DependencyObject)e.OriginalSource;

      while ((dep != null) && !(dep is DataGridCell) &&
      !(dep is DataGridColumnHeader) && !(dep is DataGridRowHeader))
      {
      dep = VisualTreeHelper.GetParent(dep);
      }

      if (dep == null)
      return;

      if (dep is DataGridRowHeader)
      {
      DataGridRowHeader rowHeader = dep as DataGridRowHeader;

      // navigate further up the tree
      while ((dep != null) && !(dep is DataGridRow))
      {
      dep = VisualTreeHelper.GetParent(dep);
      }

      if (dep == null)
      return;

      DataGridRow row = dep as DataGridRow;

      int rowIndex = FindRowIndex(row);

      ClickedItemDisplay.Text = string.Format(
      “Row header clicked [{0}]“,
      rowIndex);
      }

      Colin E.

  3. klauswiesel says:

    yepp, thanks, thats what I meant

    Regards
    Klaus

  4. klauswiesel says:

    Colin,

    another one that falls into this context: how can I catch the event that is fired when a checkbox in a checkbox column is clicked (with left button/or by pressing space or other accelerators) ?

    Regards
    Klaus

  5. Hey Colin,

    Really great stuff! Keep it up.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: