Saturday, 28 April 2007

Pasting to multiple cells in Xceed WPF DataGrid

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.

12 comments:

Anonymous said...

You stated that you put all the code for pasting multiple cells in Xceed WPF Datagrid in a code file named DataGridControlHelper.cs. Where can I download this file?

Neil Mosafi said...

Yeah the place where I was hosting it died, and I haven't had a chance to upload it again. I will upload it and update the post when it's done.

Thanks
Neil

Neil Mosafi said...

It's uploaded now - see the inline link in the blog

Let me know if you have any issues using it

Neil

Rabin said...

I will start coding what I need now. Will let you know what the result is. Thanks in advance!

Rabin said...

Hey thanks for the code it works great. When using databinding one must not forget to set the binding mode to twoway and the cells should not be read-only.

Neil Mosafi said...

Great to hear. You are correct about the two way databinding. What was the problem with read only cells? They should be handled in the control helper - I actually made a very recent fix around read only columns and read only rows which may solve your problem?

Rabin said...

Yes you are right. I only have one questions left. Is it possible to update the source of each cell after leaving that cell? Currently the cell only gets updated when you leave the current row.

I've added a contextmenu to your datagridcontrolhelper.cs
CommandManager.RegisterClassCommandBinding(typeof(DataGridControl), new CommandBinding(ApplicationCommands.Paste, HandleDataGridControlPasteCommandExecuted));
CommandManager.RegisterClassCommandBinding(typeof(DataGridControl), new CommandBinding(ApplicationCommands.Copy, HandleDataGridControlCopyCommandExecuted));

To register copy and paste command.

<xcdg:DataGridControl.ContextMenu>
<ContextMenu>
<MenuItem Command="ApplicationCommands.Paste" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem}"/>
<MenuItem Command="ApplicationCommands.Copy" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem}"/>
</ContextMenu>
</xcdg:DataGridControl.ContextMenu>

to add the context menu with the commands

Neil Mosafi said...

Hi Rabin

Looks good!

I seem to remember having the same problem when I was building my WPF application with XCeed's Grid. We solved it in the following way:

- We had the CellEditor's EditTemplate set to an AutoSelectTextBox.

- We registered an event handler for the Unloaded event of this AutoSelectTextBox.

- In the Unloaded event handler, we use the following code to force the grid to commit data back to the source

DataRow dataRow = this.grid.GetContainerFromItem(this.grid.CurrentItem) as DataRow;
if (dataRow != null && dataRow.IsBeingEdited && DataGridCommands.EndEdit.CanExecute(null, dataRow))
{
DataGridCommands.EndEdit.Execute(null, dataRow);
}

Hope this helps
Neil

Venkat..!. said...

Hai,

I want to prevent the background color of the current cell to changing into different color.Can you please help me how to do?

Neil Mosafi said...

Venkat, my guess is you'd have to redefine the template for the cell. You are probably best off posting your question to the XCeed WPF forums

Regards
Neil

Anonymous said...

Hi,

Thanks for the great DataGridControlHelper, I am using it but I have a problem.
After selecting multiple cells when I am scrolling the data grid I see that other cells marked as selected instead the ones that I selected and if I scroll back to the first location sometimes it display the cells that I selected and sometimes others.

It seems that xceed "recycling" the cell objects and just change the content.

Any solution for that problem ?

Anonymous said...

Thanks for the Helper, it works fine except when you scroll. I agree that it seems that the grid is recycling the selection.
Any idea of what could be wrong?

I'm surprises that thsi feature has not been implemented in the WPF Datagrid 3.5.

Olivier