In my previous article, Implementing Copy & Paste between cells in Xceed WPF DataGrid, I outined the method I was using to copy and paste values between cells in the Xceed WPF grid. To recap, this involves using the ApplicationCommands to register to the Copy command and copying the content to the clipboard as text. We then register to the Paste command and when this is received, set the current cell to Edit mode, simulate the text input, and commit the change.
The next requirement is to support the selection of a range of cells, and to allow the user to paste a single value to all selected cells. Selection should be in one of three ways - either by dragging the mouse from the first cell to the last, by clicking the cell then clicking the last with shift held down, or by clicking cells with control pressed.
As Xceed grid doesn't support multiple cell selection, and I don't want to mess around with the internal workings of Xceed's grid, I decided to create an attached property of type Boolean to attach to each selected cell. The attached property would indicate that that cell is currently selected. However, I then realised that as the Xceed grid is not inheriting from the Selector class, I might as well just reuse the Selector.IsSelected attached property, and avoid having to create my own.
So here is the function to select a range of cells and set the attached property on them all. The range to be selected is defined by the "current" cell (which we can get natively from the DataGridControl) and a cell passed to the function.
/// <summary>
/// Selects the range of cell from the "current" cell to the specified cell
/// </summary>
/// <param name="toCell">The cell to select to</param>
private static void SelectRangeFromCurrent(Cell toCell)
{
DataGridControl dataGridControl = DataGridControl.GetParentDataGridControl(toCell);
// Need a current item to do anything
if (dataGridControl.CurrentItem == null)
{
return;
}
// Find the current cell
Row currentRow = (Row) dataGridControl.GetContainerFromItem(dataGridControl.CurrentItem);
Cell currentCell = null;
if (currentRow != null && dataGridControl.CurrentColumn != null)
{
currentCell = currentRow.Cells[dataGridControl.CurrentColumn.Index];
}
// Do nothing if it's being edited
if (currentCell == null || currentCell.IsBeingEdited)
{
return;
}
// Clear the selected status on all the cells
ClearSelectedStatus(DataGridControl.GetParentDataGridControl(toCell));
// Get the indexes of the rows and columns we need to loop through
int fromRow, toRow, fromColumn, toColumn;
GetFromAndToRowAndColumns(dataGridControl, currentCell, toCell, out fromRow, out toRow, out fromColumn, out toColumn);
// Loop and set the IsSelected on all cells
for (int rowIndex = fromRow; rowIndex <= toRow; rowIndex++)
{
Row row = (Row) dataGridControl.GetContainerFromIndex(rowIndex);
for (int columnIndex = fromColumn; columnIndex <= toColumn; columnIndex++)
{
Selector.SetIsSelected(row.Cells[columnIndex], true);
}
}
This function uses two other functions - ClearSelectedStatus(), which clears the IsSelected on all cells in a grid, and GetFromAndToRowAndColumns() which works out the indexes of the start and end points based on the cells. Once we have the indexes, it's a simple case of looping through setting the IsSelected on all cells between those indexes.
Visually, setting the attached property will do nothing to the cells (and rightly so - this WPF we're talking about!). Therefore I define a style in my App.xaml which will change the background colour of the cells when the IsSelected is set
<Style TargetType="xcdg:DataCell">
<Style.Triggers>
<Trigger Property="Selector.IsSelected" Value="True">
<Setter Property="Background" Value="{StaticResource LightBlueBrush}" />
</Trigger>
</Style.Triggers>
</Style>
So now that we can select multiple of cells, we need to wire up the event handling to actually trigger the selection. We subscribe to the MouseDown and MouseEnter events and detect if the left mouse button is pressed. If so, we act accordingly:
/// <summary>
/// Handles when the mouse goes down over a cell, either deselecting everything, or selecting the single cell (if control is pressed)
/// or selecting the range of cells (if shift is pressed)
/// </summary>
private static void HandleCellMouseDown(object sender, RoutedEventArgs e)
{
Cell cell = sender as Cell;
if (cell != null && Mouse.LeftButton == MouseButtonState.Pressed)
{
if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
{
SelectRangeFromCurrent(cell);
}
else if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
{
Selector.SetIsSelected(cell, true);
}
else
{
ClearSelectedStatus(DataGridControl.GetParentDataGridControl(cell));
}
}
}
/// <summary>
/// Handles when the mouse enters a cell with the left button depressed, selecting the appropriate range of cells
/// </summary>
private static void HandleCellMouseEnter(object sender, RoutedEventArgs e)
{
Xceed.Wpf.DataGrid.Cell cell = sender as Xceed.Wpf.DataGrid.Cell;
if (cell != null && Mouse.LeftButton == MouseButtonState.Pressed)
{
SelectRangeFromCurrent(cell);
}
}
And that's it - we have multiple cell selection working pretty nicely! The last thing to do is update my previous code for pasting, and rather than just pasting to the current cell we paste to every cell which has the Selector.IsSelected property set to true.
I have put all the code to do this into a helper class -
DataGridControlHelper.cs. Therefore I won't include any more code here as it's mostly straightforward glue code with a few usability tweaks (such as unselecting everything when someone navigates with the keyboard).
Just call the static RegisterEventHandlers() method when your application starts up.