WPF: Programmatically Selecting and Focusing a Row or Cell in a DataGrid
Introduction
You may have tried to select a row in a DataGrid in WPF programmatically by setting its SelectedItem property or SelectedIndex property only to find out that doing this doesn’t result in the exact same behaviour as when you select a row by clicking on it with the mouse.
Setting any of these properties in code does in fact select the row and give it some kind of focus but the behaviour is slightly different compared to when clicking on it. For example, the row doesn’t get highlighted the same way and if you try to use the arrow keys of the keyboard to navigate between the rows after you have set any of the mentioned properties in code the row will lose its focus.
It is however possible to select and focus a row in code and get the same behaviour as when using the mouse by accessing the visual user interface elements of the DataGrid control and calling the UIElement.Focus() method on a particular System.Windows.Controls.DataGridCell object as described in this article. You can also select an individual cell using almost the same approach.
DataGrid.SelectionUnit
The DataGrid control has a SelectionUnit property that decides whether rows, cells or both can be selected. For you to be able to set the SelectedItem or SelectedIndex properties of the DataGrid without an exception being thrown, this property must be set to its default value of System.Windows.Controls.DataGridSelectionUnit.FullRow.
There is also a SelectionMode property that specifies whether only a single item or multiple items can be selected in the DataGrid and these two properties together defines the selection behaviour for the DataGrid control.
The visual tree
The visual tree in a WPF application describes the structure of all visual elements that are part of the user interface, i.e. everything you see on the screen.
Trees in WPF (MSDN)
WPF Graphics Rendering Overview (MSDN)
The DataGrid control renders a System.Windows.Controls.DataGridRow object for each data object in its Item collection and a System.Windows.Controls.DataGridCell for each cell of each row.
There is a built-in System.Windows.Media.VisualTreeHelper class that provides functionality for enumerating the members of a visual tree and this one is very useful whenever you need programmatic access to any object in the visual tree. The VisualTreeHelper class is for example used in the below generic method that recursively searches for a user interface element of a specific type among the descendants of a visual object of type System.Windows.DependencyObject – this is the base class for all visual elements that adds support for dependency properties – that is passed to the method as a parameter. The specific type to search for is specified by the type argument for the method.
Recursion (MSDN)
Generic Methods (C# Programming Guide) (MSDN)
Dependency Properties Overview (MSDN)
public static T FindVisualChild<T>(DependencyObject obj) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is T)
return (T)child;
else
{
T childOfChild = FindVisualChild<T>(child);
if (childOfChild != null)
return childOfChild;
}
}
return null;
}
Selecting a single row
You can get hold of the generated DataGridRow element in the visual tree that corresponds to a given item or index within a DataGrid’s collection of items by using the ContainerFromItem or ContainerFromIndex methods of the System.Windows.Controls.ItemContainerGenerator of the DataGrid control respectively. The ItemContainerGenerator object is responsible for generating the DataGridRow objects on behalf of the DataGrid. Each ItemsControl, such as the DataGrid control, has its own instance of an ItemContainerGenerator object which is exposed through a property with the same name:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
/* initialize a DataGrid with a number of Product objects */
List<Product> products = new List<Product>();
products.Add(new Product(1, "Product A"));
products.Add(new Product(2, "Product B"));
products.Add(new Product(3, "Product C"));
products.Add(new Product(4, "Product D"));
products.Add(new Product(5, "Product E"));
dataGrid.ItemsSource = products;
this.Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
/* select "Product C" */
SelectRowByIndex(dataGrid, 2);
}
public static void SelectRowByIndex(DataGrid dataGrid, int rowIndex)
{
if (!dataGrid.SelectionUnit.Equals(DataGridSelectionUnit.FullRow))
throw new ArgumentException("The SelectionUnit of the DataGrid must be set to FullRow.");
if (rowIndex < 0 || rowIndex > (dataGrid.Items.Count - 1))
throw new ArgumentException(string.Format("{0} is an invalid row index.", rowIndex));
dataGrid.SelectedItems.Clear();
/* set the SelectedItem property */
object item = dataGrid.Items[rowIndex]; // = Product X
dataGrid.SelectedItem = item;
DataGridRow row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
if (row == null)
{
/* bring the data item (Product object) into view
* in case it has been virtualized away */
dataGrid.ScrollIntoView(item);
row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
}
//TODO: Retrieve and focus a DataGridCell object
}
...
}
public class Product
{
public Product(int id, string name)
{
this.Id = id;
this.Name = name;
}
public int Id { get; private set; }
public string Name { get; set; }
}
<Window x:Class="Mm.DataGridFocus.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid x:Name="dataGrid" />
</Grid>
</Window>
User interface virtualization
Note that the ContainerFromIndex method of the ItemContainerGenerator object will return a null reference if the visual DataGridRow object for the data item has been virtualized away. The DataGrid control in WPF 4.5 by default uses user interface virtualization for performance reasons. This means that the item container generation and associated layout computation for a data bound item is deferred until the item is visible and only a subset of the controls for the data bound items is displayed on the screen at the same time. As the user scrolls through the list, elements are added and removed from the visual tree.
If you for example bind the DataGrid to a collection of a hundred (100) Product objects, only the ones that are currently visible on the screen and a few more will actually be present in the visual tree. You can bring a virtualized item into view by using the ScrollIntoView method of the DataGrid control as shown in the SelectRowByIndex method in the code snippet above. After the call to this method, you will be able to get the DataGridRow object for the item that was passed to the method as a parameter.
Once you have obtained a reference to the particular DataGridRow element for the data object or row index you want to focus, the next step is to use the generic FindVisualChild method to find the System.Windows.Controls.Primitives.DataGridCellsPresenter element in the visual tree. A DataGridCellsPresenter object is used within the template of a DataGrid control to specify the location in the control’s visual tree where the cells are to be added. It has its own instance of an ItemContainerGenerator object that is responsible for generating the actual DataGridCell objects to be added to the visual tree. Remember that a DataGridCell object represents a cell of a DataGrid control in the same way as a DataGridRow represents a row.
Below is another method that returns a DataGridCell object from a specific DataGridRow that is passed as a parameter to the method. The particular cell of the row that will be returned from the method is decided by the column parameter. If you for example pass a value of 0 as the column parameter, the leftmost cell will be returned. Also note that the FindVisualChild method is used to find the DataGridCellsPresenter object.
For a virtualized row, the FindVisualChild method won’t be able to find a DataGridCellsPresenter object because it hasn’t been added to the visual tree yet. You can then call the ApplyTemplate() method of the DataGridRow to build its visual tree, causing both the DataGridCellsPresenter and consequently the DataGridCell objects to be created.
public static DataGridCell GetCell(DataGrid dataGrid, DataGridRow rowContainer, int column)
{
if (rowContainer != null)
{
DataGridCellsPresenter presenter = FindVisualChild<DataGridCellsPresenter>(rowContainer);
if (presenter == null)
{
/* if the row has been virtualized away, call its ApplyTemplate() method
* to build its visual tree in order for the DataGridCellsPresenter
* and the DataGridCells to be created */
rowContainer.ApplyTemplate();
presenter = FindVisualChild<DataGridCellsPresenter>(rowContainer);
}
if (presenter != null)
{
DataGridCell cell = presenter.ItemContainerGenerator.ContainerFromIndex(column) as DataGridCell;
if (cell == null)
{
/* bring the column into view
* in case it has been virtualized away */
dataGrid.ScrollIntoView(rowContainer, dataGrid.Columns[column]);
cell = presenter.ItemContainerGenerator.ContainerFromIndex(column) as DataGridCell;
}
return cell;
}
}
return null;
}
To select and focus the row, you can then simply call this method from the SelectRowByIndex method in the sample code above and then call the UIElement.Focus() method on the returned DataGridCell element:
public static void SelectRowByIndex(DataGrid dataGrid, int rowIndex)
{
...
DataGridRow row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
...
if (row != null)
{
DataGridCell cell = GetCell(dataGrid, row, 0);
if(cell != null)
cell.Focus();
}
}
This will cause the row to get selected, highlighted and focused in the exact same way as when you are selecting it using the mouse:
http://magnusmontin.files.wordpress.com/2013/11/datagrid.png?w=529&h=171
Selecting multiple rows
By making some slight modifications to the SelectRowByIndex method, or by creating another method, you could also select several rows provided that the SelectionMode property of the DataGrid control is set to DataGridSelectionMode.Extended, which it is by default. The following sample code selects both the “Product B” and “Product D” objects:
public partial class MainWindow : Window
{
...
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
/* select "Product B" and "Product D" */
SelectRowByIndexes(dataGrid, 1, 3);
}
public static void SelectRowByIndexes(DataGrid dataGrid, params int[] rowIndexes)
{
if (!dataGrid.SelectionUnit.Equals(DataGridSelectionUnit.FullRow))
throw new ArgumentException("The SelectionUnit of the DataGrid must be set to FullRow.");
if (!dataGrid.SelectionMode.Equals(DataGridSelectionMode.Extended))
throw new ArgumentException("The SelectionMode of the DataGrid must be set to Extended.");
if (rowIndexes.Length.Equals(0) || rowIndexes.Length > dataGrid.Items.Count)
throw new ArgumentException("Invalid number of indexes.");
dataGrid.SelectedItems.Clear();
foreach (int rowIndex in rowIndexes)
{
if (rowIndex < 0 || rowIndex > (dataGrid.Items.Count - 1))
throw new ArgumentException(string.Format("{0} is an invalid row index.", rowIndex));
object item = dataGrid.Items[rowIndex]; //=Product X
dataGrid.SelectedItems.Add(item);
DataGridRow row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
if (row == null)
{
dataGrid.ScrollIntoView(item);
row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
}
if (row != null)
{
DataGridCell cell = GetCell(dataGrid, row, 0);
if (cell != null)
cell.Focus();
}
}
}
...
}
The params keyword in C# lets you specify a method parameter that takes a variable number of arguments. In the above method you can pass a variable number of indexes of rows in the DataGrid control to be selected.
Selecting a single cell
If you have set the SelectionUnit property of the DataGrid control to DataGridSelectionUnit.Cell and want to select and focus an individual cell of the DataGrid control rather than an entire row, the solution is similar to the previously described one.
The difference is that you add a System.Windows.Controls.DataGridCellInfo object to the DataGrid’s SelectedCells collection for each cell that you want to select, instead of setting its SelectedItem property or adding items to its SelectedItems collection.
A DataGridCellInfo object provides information about a cell and the data item that is associated with the cell. It is used instead of a reference to the actual DataGridCell object when the DataGrid control gets a cell, for example in the CurrentCell or SelectedCells properties. If you have a reference to a DataGridCell object and want to select this one, you can simply create a new DataGridCellInfo object that takes this DataGridCell object as a constructor argument and then add it to the SelectedCells collection of the DataGrid control:
public partial class MainWindow : Window
{
...
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
dataGrid.SelectionUnit = DataGridSelectionUnit.Cell;
/* select the second cell (index = 1) of the fourth row (index = 3) */
SelectCellByIndex(dataGrid, 3, 1);
}
public static void SelectCellByIndex(DataGrid dataGrid, int rowIndex, int columnIndex)
{
if (!dataGrid.SelectionUnit.Equals(DataGridSelectionUnit.Cell))
throw new ArgumentException("The SelectionUnit of the DataGrid must be set to Cell.");
if (rowIndex < 0 || rowIndex > (dataGrid.Items.Count - 1))
throw new ArgumentException(string.Format("{0} is an invalid row index.", rowIndex));
if (columnIndex < 0 || columnIndex > (dataGrid.Columns.Count - 1))
throw new ArgumentException(string.Format("{0} is an invalid column index.", columnIndex));
dataGrid.SelectedCells.Clear();
object item = dataGrid.Items[rowIndex]; //=Product X
DataGridRow row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
if (row == null)
{
dataGrid.ScrollIntoView(item);
row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
}
if (row != null)
{
DataGridCell cell = GetCell(dataGrid, row, columnIndex);
if (cell != null)
{
DataGridCellInfo dataGridCellInfo = new DataGridCellInfo(cell);
dataGrid.SelectedCells.Add(dataGridCellInfo);
cell.Focus();
}
}
}
...
}
http://magnusmontin.files.wordpress.com/2013/11/datagridcell1.png?w=528&h=170
Selecting multiple cells
To be able to select more than one cell, you could use a variation of the SelectCellByIndex method above that takes a list of row and cell combinations as a parameter in the form of a System.Collections.Generic.IList collection with System.Collections.Generic.KeyValuePair objects where the Key property of the KeyValuePair specifies the row index and the Value property specifies the column index:
public static void SelectCellsByIndexes(DataGrid dataGrid, IList<KeyValuePair<int,int>> cellIndexes)
{
if (!dataGrid.SelectionUnit.Equals(DataGridSelectionUnit.Cell))
throw new ArgumentException("The SelectionUnit of the DataGrid must be set to Cell.");
if (!dataGrid.SelectionMode.Equals(DataGridSelectionMode.Extended))
throw new ArgumentException("The SelectionMode of the DataGrid must be set to Extended.");
dataGrid.SelectedCells.Clear();
foreach (KeyValuePair<int, int> cellIndex in cellIndexes)
{
int rowIndex = cellIndex.Key;
int columnIndex = cellIndex.Value;
if (rowIndex < 0 || rowIndex > (dataGrid.Items.Count - 1))
throw new ArgumentException(string.Format("{0} is an invalid row index.", rowIndex));
if (columnIndex < 0 || columnIndex > (dataGrid.Columns.Count - 1))
throw new ArgumentException(string.Format("{0} is an invalid column index.", columnIndex));
object item = dataGrid.Items[rowIndex]; //= Product X
DataGridRow row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
if (row == null)
{
dataGrid.ScrollIntoView(item);
row = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndex) as DataGridRow;
}
if (row != null)
{
DataGridCell cell = GetCell(dataGrid, row, columnIndex);
if (cell != null)
{
DataGridCellInfo dataGridCellInfo = new DataGridCellInfo(cell);
dataGrid.SelectedCells.Add(dataGridCellInfo);
cell.Focus();
}
}
}
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
dataGrid.SelectionUnit = DataGridSelectionUnit.Cell;
List<KeyValuePair<int, int>> cellIndexes = new List<KeyValuePair<int, int>>();
/* select the first cell of the first row */
cellIndexes.Add(new KeyValuePair<int, int>(0, 0));
/* select the second cell of the second row */
cellIndexes.Add(new KeyValuePair<int, int>(1, 1));
/* select the first cell of the third row */
cellIndexes.Add(new KeyValuePair<int, int>(2, 0));
/* select the second cell of the fourth row */
cellIndexes.Add(new KeyValuePair<int, int>(3, 1));
/* select the first cell of the fifth row */
cellIndexes.Add(new KeyValuePair<int, int>(4, 0));
SelectCellsByIndexes(dataGrid, cellIndexes);
}
http://magnusmontin.files.wordpress.com/2013/11/datagridcells.png?w=528&h=170