在 WPF DataGrid 中单击编辑

我希望用户能够将单元格置于编辑模式,并通过单击突出显示单元格所包含的行。默认情况下,这是双击。

我如何重写或实现它?

79573 次浏览

From: http://wpf.codeplex.com/wikipage?title=Single-Click%20Editing

XAML:

<!-- SINGLE CLICK EDITING -->
<Style TargetType="{x:Type dg:DataGridCell}">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="DataGridCell_PreviewMouseLeftButtonDown"></EventSetter>
</Style>

CODE-BEHIND:

//
// SINGLE CLICK EDITING
//
private void DataGridCell_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
DataGridCell cell = sender as DataGridCell;
if (cell != null && !cell.IsEditing && !cell.IsReadOnly)
{
if (!cell.IsFocused)
{
cell.Focus();
}
DataGrid dataGrid = FindVisualParent<DataGrid>(cell);
if (dataGrid != null)
{
if (dataGrid.SelectionUnit != DataGridSelectionUnit.FullRow)
{
if (!cell.IsSelected)
cell.IsSelected = true;
}
else
{
DataGridRow row = FindVisualParent<DataGridRow>(cell);
if (row != null && !row.IsSelected)
{
row.IsSelected = true;
}
}
}
}
}


static T FindVisualParent<T>(UIElement element) where T : UIElement
{
UIElement parent = element;
while (parent != null)
{
T correctlyTyped = parent as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}


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


return null;
}

Here is how I resolved this issue:

<DataGrid DataGridCell.Selected="DataGridCell_Selected"
ItemsSource="{Binding Source={StaticResource itemView}}">
<DataGrid.Columns>
<DataGridTextColumn Header="Nom" Binding="{Binding Path=Name}"/>
<DataGridTextColumn Header="Age" Binding="{Binding Path=Age}"/>
</DataGrid.Columns>
</DataGrid>

This DataGrid is bound to a CollectionViewSource (Containing dummy Person objects).

The magic happens there : DataGridCell.Selected="DataGridCell_Selected".

I simply hook the Selected Event of the DataGrid cell, and call BeginEdit() on the DataGrid.

Here is the code behind for the event handler :

private void DataGridCell_Selected(object sender, RoutedEventArgs e)
{
// Lookup for the source to be DataGridCell
if (e.OriginalSource.GetType() == typeof(DataGridCell))
{
// Starts the Edit on the row;
DataGrid grd = (DataGrid)sender;
grd.BeginEdit(e);
}
}

The solution from http://wpf.codeplex.com/wikipage?title=Single-Click%20Editing worked great for me, but I enabled it for every DataGrid using a Style defined in a ResourceDictionary. To use handlers in resource dictionaries you need to add a code-behind file to it. Here's how you do it:

This is a DataGridStyles.xaml Resource Dictionary:

    <ResourceDictionary x:Class="YourNamespace.DataGridStyles"
x:ClassModifier="public"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="DataGrid">
<!-- Your DataGrid style definition goes here -->


<!-- Cell style -->
<Setter Property="CellStyle">
<Setter.Value>
<Style TargetType="DataGridCell">
<!-- Your DataGrid Cell style definition goes here -->
<!-- Single Click Editing -->
<EventSetter Event="PreviewMouseLeftButtonDown"
Handler="DataGridCell_PreviewMouseLeftButtonDown" />
</Style>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>


Note the x:Class attribute in the root element. Create a class file. In this example it'd be DataGridStyles.xaml.cs. Put this code inside:

using System.Windows.Controls;
using System.Windows;
using System.Windows.Input;


namespace YourNamespace
{
partial class DataGridStyles : ResourceDictionary
{


public DataGridStyles()
{
InitializeComponent();
}


// The code from the myermian's answer goes here.
}

The answer from Micael Bergeron was a good start for me to find a solution thats working for me. To allow single-click editing also for Cells in the same row thats already in edit mode i had to adjust it a bit. Using SelectionUnit Cell was no option for me.

Instead of using the DataGridCell.Selected Event which is only fired for the first time a row's cell is clicked, i used the DataGridCell.GotFocus Event.

<DataGrid DataGridCell.GotFocus="DataGrid_CellGotFocus" />

If you do so you will have always the correct cell focused and in edit mode, but no control in the cell will be focused, this i solved like this

private void DataGrid_CellGotFocus(object sender, RoutedEventArgs e)
{
// Lookup for the source to be DataGridCell
if (e.OriginalSource.GetType() == typeof(DataGridCell))
{
// Starts the Edit on the row;
DataGrid grd = (DataGrid)sender;
grd.BeginEdit(e);


Control control = GetFirstChildByType<Control>(e.OriginalSource as DataGridCell);
if (control != null)
{
control.Focus();
}
}
}


private T GetFirstChildByType<T>(DependencyObject prop) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(prop); i++)
{
DependencyObject child = VisualTreeHelper.GetChild((prop), i) as DependencyObject;
if (child == null)
continue;


T castedProp = child as T;
if (castedProp != null)
return castedProp;


castedProp = GetFirstChildByType<T>(child);


if (castedProp != null)
return castedProp;
}
return null;
}

There are two issues with user2134678's answer. One is very minor and has no functional effect. The other is fairly significant.

The first issueis that the GotFocus is actually being called against the DataGrid, not the DataGridCell in practice. The DataGridCell qualifier in the XAML is redundant.

The main problem I found with the answer is that the Enter key behavior is broken. Enter should move you to the next cell below the current cell in normal DataGrid behavior. However, what actually happens behind the scenes is GotFocus event will be called twice. Once upon the current cell losing focus, and once upon the new cell gaining focus. But as long as BeginEdit is called on that first cell, the next cell will never be activated. The upshot is that you have one-click editing, but anyone who isn't literally clicking on the grid is going to be inconvenienced, and a user-interface designer should not assume that all users are using mouses. (Keyboard users can sort of get around it by using Tab, but that still means they're jumping through hoops that they shouldn't need to.)

So the solution to this problem? Handle event KeyDown for the cell and if the Key is the Enter key, set a flag that stops BeginEdit from firing on the first cell. Now the Enter key behaves as it should.

To begin with, add the following Style to your DataGrid:

<DataGrid.Resources>
<Style TargetType="{x:Type DataGridCell}" x:Key="SingleClickEditingCellStyle">
<EventSetter Event="KeyDown" Handler="DataGridCell_KeyDown" />
</Style>
</DataGrid.Resources>

Apply that style to "CellStyle" property the columns for which you want to enable one-click.

Then in the code behind you have the following in your GotFocus handler (note that I'm using VB here because that's what our "one-click data grid requesting" client wanted as the development language):

Private _endEditing As Boolean = False


Private Sub DataGrid_GotFocus(ByVal sender As Object, ByVal e As RoutedEventArgs)
If Me._endEditing Then
Me._endEditing = False
Return
End If


Dim cell = TryCast(e.OriginalSource, DataGridCell)


If cell Is Nothing Then
Return
End If


If cell.IsReadOnly Then
Return
End If


DirectCast(sender, DataGrid).BeginEdit(e)
.
.
.

Then you add your handler for the KeyDown event:

Private Sub DataGridCell_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs)
If e.Key = Key.Enter Then
Me._endEditing = True
End If
End Sub

Now you have a DataGrid that hasn't changed any fundamental behavior of the out-of-the-box implementation and yet supports single-click editing.

 <DataGridComboBoxColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="cal:Message.Attach"
Value="[Event MouseLeftButtonUp] = [Action ReachThisMethod($source)]"/>
</Style>
</DataGridComboBoxColumn.CellStyle>
 public void ReachThisMethod(object sender)
{
((System.Windows.Controls.DataGridCell)(sender)).IsEditing = true;


}

I solved it by adding a trigger that sets IsEditing property of the DataGridCell to True when the mouse is over it. It solved most of my problems. It works with comboboxes too.

<Style TargetType="DataGridCell">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="IsEditing" Value="True" />
</Trigger>
</Style.Triggers>
</Style>

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 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;
}
}
}

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"