Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Begin edit mode on key press with DataGridTemplateColumn

Tags:

c#

wpf

datagrid

I have a DataGrid bound to an ObservableCollection called MyObjects. The DataGrid has 2 columns: one is a DataGridTextColumn, the other a DataGridTemplateColumn.

What I'm trying to achieve is have the template column behave like the text column when a key is pressed while a cell is selected.

For example, when you select a cell from the text column and hit the "A" key, the cell editing template activates and the letter "A" is input into the textbox.

What I want to know is how to implement this behaviour into a template column (ie. key press activates its' cell editing template and pass the character to a control within the template as input).

My search results could only find answers pertaining to which control in the editing template gets focus when tabbing between cells, which isn't the same question as mine. Below is the XAML of my DataGrid.

<DataGrid ItemsSource="{Binding MyObjects}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Test" Binding="{Binding Test}"/>
        <DataGridTemplateColumn Header="Date">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Date}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <!--This is the control that I want to focus!-->
                    <DatePicker SelectedDate="{Binding Date}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

Edit:

I've written a simple helper class that allows a XAML-specified control to be focused when the cell template loads... Combined with Aled's answer, this is very close to what I want! I just need to work out how to pass the input to the focused control...

Problem is that the key press event gets handled before the control loaded event, so I need to work out how to bridge them together... Or work on a new approach entirely.

public sealed class FrameworkElementFocusHelper
{
    private static readonly DependencyProperty FocusOnLoadProperty =
        DependencyProperty.RegisterAttached("FocusOnLoad",
                                            typeof(bool),
                                            typeof(FrameworkElementFocusHelper),
                                            new UIPropertyMetadata(FocusOnLoadPropertyChanged));

    public static void FocusOnLoadPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement element = (FrameworkElement)source;
        element.Loaded -= FrameworElementFocusHelperLoadedEvent;
        if ((bool)e.NewValue == true)
            element.Loaded += FrameworElementFocusHelperLoadedEvent;
    }

    public static void SetFocusOnLoad(DependencyObject element, bool value)
    {
        element.SetValue(FocusOnLoadProperty, value);
    }
    public static bool GetFocusOnLoad(DependencyObject element)
    {
        return (bool)element.GetValue(FocusOnLoadProperty);
    }

    public static void FrameworElementFocusHelperLoadedEvent(object sender, RoutedEventArgs e)
    {
        ((FrameworkElement)sender).Focus();
    }
}

Usage:

<DataGridTemplateColumn.CellEditingTemplate>
    <DataTemplate>
        <DatePicker SelectedDate="{Binding Date}" rt:FrameworkElementFocusHelper.FocusOnLoad="true"/>
    </DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
like image 926
learningcs Avatar asked Jan 27 '15 05:01

learningcs


People also ask

How to make DataGrid editable?

The user can enter edit mode in a cell by pressing F2 key or double tapping on a cell. Alternatively, you can set the DataGridColumn. IsReadOnly property to true to disable editing in specific columns of the DataGrid.

How do I edit a DataGrid in WPF?

You can set the properties CanUserAddRows to true to allow user to add rows. DataGrid is editable by default, where each column has an edit control which lets you edit its value. By default the DataGrid automatically generates columns for every property in your Model, so you don't even have to define it's columns.

How to make DataGrid not editable?

You can easily use IsReadOnly property on the DataGrid to make it entirely read only. or use IsReadOnly per Column basis to make them read only. The WPF DataGrid has an IsReadOnly property that you can set to True to ensure that users cannot edit your DataGrid 's cells.

What is DataGrid WPF?

A DataGrid is a control that displays data in a customizable grid. It provides a flexible way to display a collection of data in rows and columns. The hierarchical inheritance of DataGrid class is as follows −


2 Answers

I have one way that at least gets you into edit mode on a key press.

First here is an extension class I have which provides some methods to get hold of rows/columns programmatically (not all of which may be necessary in this case):

namespace MyApp.Extensions
{
    /// <summary>
    /// Helper methods for the WPF DataGrid.
    /// </summary>
    public static class DataGridExtensions
    {
        /// <summary>
        /// Gets a specific row from the data grid. If the DataGrid is virtualised the row will be scrolled into view.
        /// </summary>
        /// <param name="grid">The DataGrid.</param>
        /// <param name="rowIndex">Row number to get.</param>
        /// <returns></returns>
        public static DataGridRow GetRow(this DataGrid grid, int rowIndex)
        {
            var row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(rowIndex);
            if (row == null)
            {
                grid.UpdateLayout();
                grid.ScrollIntoView(grid.Items[rowIndex]);
                row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(rowIndex);
            }
            return row;
        }

        /// <summary>
        /// Get the selected row.
        /// </summary>
        /// <param name="grid">DataGridRow.</param>
        /// <returns>DataGridRow or null if no row selected.</returns>
        public static DataGridRow GetSelectedRow(this DataGrid grid)
        {
            return (grid.SelectedIndex) < 0 ? null : (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(grid.SelectedIndex);
        }

        /// <summary>
        /// Gets a specific cell from the DataGrid.
        /// </summary>
        /// <param name="grid">The DataGrid.</param>
        /// <param name="row">The row from which to get a cell from.</param>
        /// <param name="column">The cell index.</param>
        /// <returns>A DataGridCell.</returns>
        public static DataGridCell GetCell(this DataGrid grid, DataGridRow row, int column)
        {
            if (row == null) return null;

            var presenter = GetVisualChild<DataGridCellsPresenter>(row);
            if (presenter == null)
            {
                // Virtualised - scroll into view.
                grid.ScrollIntoView(row, grid.Columns[column]);
                presenter = GetVisualChild<DataGridCellsPresenter>(row);
            }

            return (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(column);
        }

        /// <summary>
        /// Gets a specific cell from the DataGrid.
        /// </summary>
        /// <param name="grid">The DataGrid.</param>
        /// <param name="row">The row index.</param>
        /// <param name="column">The cell index.</param>
        /// <returns>A DataGridCell.</returns>
        public static DataGridCell GetCell(this DataGrid grid, int row, int column)
        {
            var rowContainer = grid.GetRow(row);
            return grid.GetCell(rowContainer, column);
        }

        /// <summary>
        /// Gets the currently selected (focused) cell.
        /// </summary>
        /// <param name="grid">The DataGrid.</param>
        /// <returns>DataGridCell or null if no cell is currently selected.</returns>
        public static DataGridCell GetSelectedCell(this DataGrid grid)
        {
            var row = grid.GetSelectedRow();
            if (row != null)
            {
                for (int i = 0; i < grid.Columns.Count; i++)
                {
                    var cell = grid.GetCell(row, i);
                    if (cell.IsFocused)
                        return cell;
                }
            }
            return null;
        }

        /// <summary>
        /// Helper method to get a particular visual child.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="parent"></param>
        /// <returns></returns>
        private static T GetVisualChild<T>(Visual parent) where T : Visual
        {
            T child = default(T);
            int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
            for (int i = 0; i < numVisuals; i++)
            {
                var v = (Visual)VisualTreeHelper.GetChild(parent, i);
                child = v as T ?? GetVisualChild<T>(v);
                if (child != null)
                {
                    break;
                }
            }
            return child;
        }
    }
}

Now add a handler to the PreviewKeyDown event on the Datagrid.

<DataGrid ItemsSource="{Binding MyData}" PreviewKeyDown="MyDataGrid_OnPreviewKeyDown">

And here's the handler:

private void MyDataGrid_OnPreviewKeyDown(object sender, KeyEventArgs e)
{
    var dg = sender as DataGrid;

   // alter this condition for whatever valid keys you want - avoid arrows/tab, etc.
    if (dg != null && !dg.IsReadOnly && e.Key == Key.Enter)
    {
        var cell = dg.GetSelectedCell();
        if (cell != null && cell.Column is DataGridTemplateColumn)
        {
            cell.Focus();
            dg.BeginEdit();                        
            e.Handled = true;
        }
    }
}

A bit of faff, but appears to work. Probably not too difficult to pass the key press on to the edit control.

I did look into doing it another way by creating my own DataGridXYZColumn class but there is a major gotcha in that the method that handles keyboard input is marked as internal and is not overridable, so I was left with this method!

like image 178
Aled Hughes Avatar answered Nov 16 '22 02:11

Aled Hughes


The Aled Hughes's answer is good base - the key idea there was to change focus before processing event. However, there a few key points missing:

  1. Should be passing focus to the input control rather than cell
  2. Capture PreviewTextInput rather than PreviewKeyDown - input characters do not correspond 1:1 to key presses, plus currently there seems to be no way to to invoke PreviewKeyDown on a TextBox - it swallows first character

the following handler should closely mimic DataGridTextColumn behaviour, including alt keycodes (e.g. alt+6+4 = @) while ignoring pasting:

private static bool CanEditCell(DataGridCell cell)
{
    if (!(cell.Column is DataGridTemplateColumn col)) return false; //TemplateColumns only    
    //dont process noneditable or already editing cell
    return !(cell.IsEditing || cell.IsReadOnly); 
}

private static void DataGrid_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
    //ignore non-key input related events, e.g. ctrl+c/ctrl+v
    if (string.IsNullOrEmpty(e.Text)) return;

    if (e.Source is DataGrid dg &&
        e.OriginalSource is DataGridCell cell &&
        CanEditCell(cell))
    {
        dg.BeginEdit();
        //custom extension method, see
        var tb = cell.GetVisualChild<TextBox>();
        tb?.Focus(); //route current event into the input control
        tb?.SelectAll(); //overwrite contents
    }
}
like image 20
wondra Avatar answered Nov 16 '22 02:11

wondra