Single click edit in WPF DataGrid

Viewed 78789

I want the user to be able to put the cell into editing mode and highlight the row the cell is contained in with a single click. By default, this is double click.

How do I override or implement this?

13 Answers

i prefer this way based on Dušan Knežević suggestion. you click an that's it ))

<DataGrid.Resources>

    <Style TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
        <Style.Triggers>
                <MultiTrigger>
                    <MultiTrigger.Conditions>
                        <Condition Property="IsMouseOver"
                                   Value="True" />
                        <Condition Property="IsReadOnly"
                                   Value="False" />
                    </MultiTrigger.Conditions>
                    <MultiTrigger.Setters>
                        <Setter Property="IsEditing"
                                Value="True" />
                    </MultiTrigger.Setters>
                </MultiTrigger>
        </Style.Triggers>
    </Style>

</DataGrid.Resources>

I looking for editing cell on single click in MVVM and this is an other way to do it.

  1. Adding behavior in xaml

    <UserControl xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
                 xmlns:myBehavior="clr-namespace:My.Namespace.To.Behavior">
    
        <DataGrid>
            <i:Interaction.Behaviors>
                <myBehavior:EditCellOnSingleClickBehavior/>
            </i:Interaction.Behaviors>
        </DataGrid>
    </UserControl>
    
  2. The EditCellOnSingleClickBehavior class extend System.Windows.Interactivity.Behavior;

    public class EditCellOnSingleClick : Behavior<DataGrid>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.LoadingRow += this.OnLoadingRow;
            this.AssociatedObject.UnloadingRow += this.OnUnloading;
        }
    
        protected override void OnDetaching()
        {
            base.OnDetaching();
            this.AssociatedObject.LoadingRow -= this.OnLoadingRow;
            this.AssociatedObject.UnloadingRow -= this.OnUnloading;
        }
    
        private void OnLoadingRow(object sender, DataGridRowEventArgs e)
        {
            e.Row.GotFocus += this.OnGotFocus;
        }
    
        private void OnUnloading(object sender, DataGridRowEventArgs e)
        {
            e.Row.GotFocus -= this.OnGotFocus;
        }
    
        private void OnGotFocus(object sender, RoutedEventArgs e)
        {
            this.AssociatedObject.BeginEdit(e);
        }
    }
    

Voila !

I slightly edit solution from Dušan Knežević

<DataGrid.Resources>
   <Style x:Key="ddlStyle" TargetType="DataGridCell">
      <Style.Triggers>
         <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="IsEditing" Value="True" />
         </Trigger>
      </Style.Triggers>
    </Style>
</DataGrid.Resources>

and apply the style to my desire column

<DataGridComboBoxColumn CellStyle="{StaticResource ddlStyle}">

Several of these answers inspired me, as well as this blog post, yet each answer left something wanting. I combined what seemed the best parts of them and came up with this fairly elegant solution that seems to get the user experience exactly right.

This uses some C# 9 syntax, which works fine everywhere though you may need to set this in your project file:

<LangVersion>9</LangVersion>

First, we get down from 3 clicks to 2 with this:

Add this class to your project:

public static class WpfHelpers
{
    internal static void DataGridPreviewMouseLeftButtonDownEvent(object sender, RoutedEventArgs e)
    {
        // The original source for this was inspired by https://softwaremechanik.wordpress.com/2013/10/02/how-to-make-all-wpf-datagrid-cells-have-a-single-click-to-edit/
        DataGridCell? cell = e is MouseButtonEventArgs { OriginalSource: UIElement clickTarget } ? FindVisualParent<DataGridCell>(clickTarget) : null;
        if (cell is { IsEditing: false, IsReadOnly: false })
        {
            if (!cell.IsFocused)
            {
                cell.Focus();
            }

            if (FindVisualParent<DataGrid>(cell) is DataGrid dataGrid)
            {
                if (dataGrid.SelectionUnit != DataGridSelectionUnit.FullRow)
                {
                    if (!cell.IsSelected)
                    {
                        cell.IsSelected = true;
                    }
                }
                else
                {
                    if (FindVisualParent<DataGridRow>(cell) is DataGridRow { IsSelected: false } row)
                    {
                        row.IsSelected = true;
                    }
                }
            }
        }
    }

    internal static T? GetFirstChildByType<T>(DependencyObject prop)
        where T : DependencyObject
    {
        int count = VisualTreeHelper.GetChildrenCount(prop);
        for (int i = 0; i < count; i++)
        {
            if (VisualTreeHelper.GetChild(prop, i) is DependencyObject child)
            {
                T? typedChild = child as T ?? GetFirstChildByType<T>(child);
                if (typedChild is object)
                {
                    return typedChild;
                }
            }
        }

        return null;
    }

    private static T? FindVisualParent<T>(UIElement element)
        where T : UIElement
    {
        UIElement? parent = element;
        while (parent is object)
        {
            if (parent is T correctlyTyped)
            {
                return correctlyTyped;
            }

            parent = VisualTreeHelper.GetParent(parent) as UIElement;
        }

        return null;
    }
}

Add this to your App.xaml.cs file:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    EventManager.RegisterClassHandler(
        typeof(DataGrid),
        DataGrid.PreviewMouseLeftButtonDownEvent,
        new RoutedEventHandler(WpfHelpers.DataGridPreviewMouseLeftButtonDownEvent));
}

Then we get from 2 to 1 with this:

Add this to the code behind of the page containing the DataGrid:

private void TransactionDataGrid_PreparingCellForEdit(object sender, DataGridPreparingCellForEditEventArgs e)
{
    WpfHelpers.GetFirstChildByType<Control>(e.EditingElement)?.Focus();
}

And wire it up (in XAML): PreparingCellForEdit="TransactionDataGrid_PreparingCellForEdit"

I know that I am a bit late to the party but I had the same problem and came up with a different solution:

     public class DataGridTextBoxColumn : DataGridBoundColumn
 {
  public DataGridTextBoxColumn():base()
  {
  }

  protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
  {
   throw new NotImplementedException("Should not be used.");
  }

  protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
  {
   var control = new TextBox();
   control.Style = (Style)Application.Current.TryFindResource("textBoxStyle");
   control.FontSize = 14;
   control.VerticalContentAlignment = VerticalAlignment.Center;
   BindingOperations.SetBinding(control, TextBox.TextProperty, Binding);
    control.IsReadOnly = IsReadOnly;
   return control;
  }
 }

        <DataGrid Grid.Row="1" x:Name="exportData" Margin="15" VerticalAlignment="Stretch" ItemsSource="{Binding CSVExportData}" Style="{StaticResource dataGridStyle}">
        <DataGrid.Columns >
            <local:DataGridTextBoxColumn Header="Sample ID" Binding="{Binding SampleID}" IsReadOnly="True"></local:DataGridTextBoxColumn>
            <local:DataGridTextBoxColumn Header="Analysis Date" Binding="{Binding Date}" IsReadOnly="True"></local:DataGridTextBoxColumn>
            <local:DataGridTextBoxColumn Header="Test" Binding="{Binding Test}" IsReadOnly="True"></local:DataGridTextBoxColumn>
            <local:DataGridTextBoxColumn Header="Comment" Binding="{Binding Comment}"></local:DataGridTextBoxColumn>
        </DataGrid.Columns>
    </DataGrid>

As you can see I wrote my own DataGridTextColumn inheriting everything vom the DataGridBoundColumn. By overriding the GenerateElement Method and returning a Textbox control right there the Method for generating the Editing Element never gets called. In a different project I used this to implement a Datepicker column, so this should work for checkboxes and comboboxes as well.

This does not seem to impact the rest of the datagrids behaviours..At least I haven't noticed any side effects nor have I got any negative feedback so far.

Update

A simple solution if you are fine with that your cell stays a textbox (no distinguishing between edit and non-edit mode). This way single click editing works out of the box. This works with other elements like combobox and buttons as well. Otherwise use the solution below the update.

<DataGridTemplateColumn Header="My Column header">
   <DataGridTemplateColumn.CellTemplate>
      <DataTemplate>
         <TextBox Text="{Binding MyProperty } />
      </DataTemplate>
   </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

Update end

Rant

I tried everything i found here and on google and even tried out creating my own versions. But every answer/solution worked mainly for textbox columns but didnt work with all other elements (checkboxes, comboboxes, buttons columns), or even broke those other element columns or had some other side effects. Thanks microsoft for making datagrid behave that ugly way and force us to create those hacks. Because of that I decided to make a version which can be applied with a style to a textbox column directly without affecting other columns.

Features

  • No Code behind. MVVM friendly.
  • It works when clicking on different textbox cells in the same or different rows.
  • TAB and ENTER keys work.
  • It doesnt affect other columns.

Sources

I used this solution and @m-y's answer and modified them to be an attached behavior. http://wpf-tutorial-net.blogspot.com/2016/05/wpf-datagrid-edit-cell-on-single-click.html

How to use it

Add this Style. The BasedOn is important when you use some fancy styles for your datagrid and you dont wanna lose them.

<Window.Resources>
    <Style x:Key="SingleClickEditStyle" TargetType="{x:Type DataGridCell}" BasedOn="{StaticResource {x:Type DataGridCell}}">
        <Setter Property="local:DataGridTextBoxSingleClickEditBehavior.Enable" Value="True" />
    </Style>
</Window.Resources>

Apply the style with CellStyle to each of your DataGridTextColumns like this:

<DataGrid ItemsSource="{Binding MyData}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn Header="My Header" Binding="{Binding Comment}" CellStyle="{StaticResource SingleClickEditStyle}" />         
    </DataGrid.Columns>
</DataGrid>

And now add this class to the same namespace as your MainViewModel (or a different Namespace. But then you will need to use a other namespace prefix than local). Welcome to the ugly boilerplate code world of attached behaviors.

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace YourMainViewModelNameSpace
{
    public static class DataGridTextBoxSingleClickEditBehavior
    {
        public static readonly DependencyProperty EnableProperty = DependencyProperty.RegisterAttached(
            "Enable",
            typeof(bool),
            typeof(DataGridTextBoxSingleClickEditBehavior),
            new FrameworkPropertyMetadata(false, OnEnableChanged));


        public static bool GetEnable(FrameworkElement frameworkElement)
        {
            return (bool) frameworkElement.GetValue(EnableProperty);
        }


        public static void SetEnable(FrameworkElement frameworkElement, bool value)
        {
            frameworkElement.SetValue(EnableProperty, value);
        }


        private static void OnEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is DataGridCell dataGridCell)
                dataGridCell.PreviewMouseLeftButtonDown += DataGridCell_PreviewMouseLeftButtonDown;
        }


        private static void DataGridCell_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            EditCell(sender as DataGridCell, e);
        }

        private static void EditCell(DataGridCell dataGridCell, RoutedEventArgs e)
        {
            if (dataGridCell == null || dataGridCell.IsEditing || dataGridCell.IsReadOnly)
                return;

            if (dataGridCell.IsFocused == false)
                dataGridCell.Focus();

            var dataGrid = FindVisualParent<DataGrid>(dataGridCell);
            dataGrid?.BeginEdit(e);
        }


        private static T FindVisualParent<T>(UIElement element) where T : UIElement
        {
            var parent = VisualTreeHelper.GetParent(element) as UIElement;

            while (parent != null)
            {
                if (parent is T parentWithCorrectType)
                    return parentWithCorrectType;

                parent = VisualTreeHelper.GetParent(parent) as UIElement;
            }

            return null;
        }
    }
}
Related