Showing posts with label XCeed DataGrid. Show all posts
Showing posts with label XCeed DataGrid. Show all posts

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.

Wednesday, 25 April 2007

Implementing Copy & Paste between cells in Xceed WPF DataGrid

I am using the Xceed grid in my application to allow the user to enter monetary values into cells. They would like to be able to copy and paste values between cells in the grid, but this is not supported by the grid natively. Support from the Clipboard is very good in WPF (via the System.Windows.Clipboard static class) so this actually is not a particularly difficult thing to implement. My aim is to write a generic reusable solution here that will work for any Xceed grid in my application, rather than a specific one, so I neither want to do any copying or updating of actual data. What we first need to do is register execution handlers for the ApplicationCommands.Copy and the ApplicationCommands.Paste commands against the DataGrid. We then either copy the content of the current cell or paste into it depending on which command is executed. I think the best place to perform generic event handling against a class is in the static initialiser of the App class, like follows:

        static App()
        {
            CommandManager.RegisterClassCommandBinding(typeof(DataGridControl), newCommandBinding(ApplicationCommands.Paste, HandleXceedPasteCommandExecuted)); 
            CommandManager.RegisterClassCommandBinding(typeof(DataGridControl), newCommandBinding(ApplicationCommands.Copy, HandleXceedCopyCommandExecuted)); 
        }
We'll start with the Copy handler. This is quite simple - we first work out which cell has the focus, then copy the textual version of its content to the clipboard.
        private static void HandleXceedCopyCommandExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            DataGridControl dataGridControl = sender as DataGridControl;
 
            if (dataGridControl != null)
            {
                Xceed.Wpf.DataGrid.Row row = (Xceed.Wpf.DataGrid.Row) dataGridControl.ItemContainerGenerator.ContainerFromItem(dataGridControl.CurrentItem);
                Xceed.Wpf.DataGrid.Cell currentCell = row.Cells[dataGridControl.CurrentColumn.Index];
 
                if (currentCell != null && currentCell.HasContent)
                {
                    Clipboard.SetText(currentCell.Content.ToString());
                }
            }
        }
Note that we copy the ToString() of the content to the clipboard, so this technique would not work if the Cell's content was an object whose string was not valid to be entered into a Cell. As we are copying and pasting between Cells, this shouldn't be a problem. Now onto the paste command. The paste command is more tricky and I will explain why. We could just update the content directly, but that would bypass any text input handling and formatting that we may be doing in the Xceed grid. For example, one grid in my application uses the NumericInputScope class to block all non numbers from being typed into the Grid. If we just updated the grid at this point, the PreviewKeyDown and PreviewTextInput would never be fired, therefore we would be allowing invalid characters to be typed into the grid. The solution I came up with for this is to switch the cell into Edit Mode, simulate keypresses for each character being pasted (via raising the TextInput event), and the commit the changes back to the cell. This should ensure that the paste is essentially doing exactly the same thing as it would do if the user pressed the keys themselves! Note we dispatch the work back to the Dispatcher... this is to allow the cell's editor to activate and the cell to get focus before we simulate the key presses.
        public delegate void Callback();
 
        private static void HandleXceedPasteCommandExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            DataGridControl dataGridControl = sender as DataGridControl;
 
            if (dataGridControl != null && Clipboard.ContainsText())
            {
                Xceed.Wpf.DataGrid.Row row = (Xceed.Wpf.DataGrid.Row)dataGridControl.ItemContainerGenerator.ContainerFromItem(dataGridControl.CurrentItem);
                Xceed.Wpf.DataGrid.Cell currentCell = row.Cells[dataGridControl.CurrentColumn.Index];
 
                if (!currentCell.ReadOnly)
                {
                    string text = Clipboard.GetText();
 
                    currentCell.BeginEdit();
 
                    Application.Current.Dispatcher.Invoke(DispatcherPriority.Input, (Callback)delegate
                    {
                        if (Keyboard.FocusedElement != null)
                        {
                            foreach (char c in text)
                            {
                                TextComposition textComposition = new TextComposition(InputManager.Current, Keyboard.FocusedElement, new string(c, 1));
 
                                TextCompositionEventArgs textCompositionEventArgs = new TextCompositionEventArgs(Keyboard.PrimaryDevice, textComposition);
                                textCompositionEventArgs.RoutedEvent = UIElement.TextInputEvent;
 
                                Keyboard.FocusedElement.RaiseEvent(textCompositionEventArgs);
                            }
                        }
 
                        currentCell.EndEdit();
                    });
                }
            }
        }
I'd be interested to know what people think of this method, and if anyone has tried any other solutions to implement copy and paste in the grid let me know. The next requirement is to support selection of and pasting to multiple cells within the Grid. Again, this is not currently supported in the Xceed DataGrid, but I will hopefully come up with a way of doing it!