Related
I'm still fighting with manipulation of cell backgrounds so I'm asking a new question.
A user "H.B." wrote that I can actually set the cell style during the AutoGeneratingColumn event - Change DataGrid cell colour based on values. The problem is that I'm not sure how to do it.
What I want:
Set different background colours for each cell depending on its value. If the value is null I also want it not to be clickable (focusable I guess).
What I have / I'm trying to do:
private void mydatagrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
foreach (Cell cell in e.Column)
{
if (cell.Value < 1)
{
cell.Background = Color.Black;
cell.isFocusable = false;
}
else
{
cell.Background = Color.Pink;
}
}
}
This is just the pseudocode. Is something like this is possible during column auto-generation and if so, how can I edit my code so it will be valid?
I read about value convertors but I want to know if it's somehow possible programmatically, without writing XAML.
Please understand that I'm still a beginner to C#/WPF/DataGrid.
Solution part1:
I used the answer I accepted. Just put it into
<Window.Resources>
<local:ValueColorConverter x:Key="colorConverter"/>
<Style x:Key="DataGridCellStyle1" TargetType="{x:Type DataGridCell}">
<Setter Property="Padding" Value="5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<Border Padding="{TemplateBinding Padding}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
<Border.Background>
<MultiBinding Converter="{StaticResource colorConverter}">
<Binding RelativeSource="{RelativeSource AncestorType=DataGridCell}" Path="Content.Text"/>
<Binding RelativeSource="{RelativeSource AncestorType=DataGridCell}" Path="IsSelected"/>
</MultiBinding>
</Border.Background>
<ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
And made for it a MultiBinding convertor so I can also set the background color for selected cells.
Problem:
Now I only have to solve the problem of setting focus of empty cells. Any hints?
<Style.Triggers>
<Trigger Property="HasContent" Value="False">
<Setter Property="Focusable" Value="False"/>
</Trigger>
</Style.Triggers>
This doesn't work. I had empty strings in the empty cells, but they are filled with ´null´s so it should work, right? Or what am I doing wrong :| ?
Solution part 2:
So the code above won't work as long as the cell value is a ´TextBox´ so I decided to find another way to deal with it which can be found in my answer here: https://stackoverflow.com/a/16673602/2296407
Thanks for trying to help me :)
I can propose two different solutions for your question: the first is "code-behind-style" (which you are asking for but personally I think it is not right approach in WPF) and more WPF-style (which more tricky but keeps code-behind clean and utilizes styles, triggers and converters)
Solution 1. Event handling and code-behind logic for coloring
First of all, the approach you've chosen will not work directly - the AutoGeneratingColumn event is meant to be used for altering the entire column appearance, not on the cell-by-cell basis. So it can be used for, say, attaching the correct style to entire column basing on it's display index or bound property.
Generally speaking, for the first time the event is raised your datagrid will not have any rows (and consequently - cells) at all. If you really need to catch the event - consider your DataGrid.LoadingRow event instead. And you will not be able to get the cells that easy :)
So, what you do: handle the LoadingRow event, get the row (it has the Item property which holds (surprisingly :)) your bound item), get the bound item, make all needed calculations, get the cell you need to alter and finally alter the style of the target cell.
Here is the code (as item I use a sample object with the int "Value" property that I use for coloring).
XAML
<DataGrid Name="mygrid" ItemsSource="{Binding Items}" AutoGenerateColumns="True" LoadingRow="DataGrid_OnLoadingRow"/>
.CS
private void DataGrid_OnLoadingRow(object sender, DataGridRowEventArgs e)
{
Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => AlterRow(e)));
}
private void AlterRow(DataGridRowEventArgs e)
{
var cell = GetCell(mygrid, e.Row, 1);
if (cell == null) return;
var item = e.Row.Item as SampleObject;
if (item == null) return;
var value = item.Value;
if (value <= 1) cell.Background = Brushes.Red;
else if (value <= 2) cell.Background = Brushes.Yellow;
else cell.Background = Brushes.Green;
}
public static DataGridRow GetRow(DataGrid grid, int index)
{
var row = grid.ItemContainerGenerator.ContainerFromIndex(index) as DataGridRow;
if (row == null)
{
// may be virtualized, bring into view and try again
grid.ScrollIntoView(grid.Items[index]);
row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(index);
}
return row;
}
public 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;
}
public static DataGridCell GetCell(DataGrid host, DataGridRow row, int columnIndex)
{
if (row == null) return null;
var presenter = GetVisualChild<DataGridCellsPresenter>(row);
if (presenter == null) return null;
// try to get the cell but it may possibly be virtualized
var cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
if (cell == null)
{
// now try to bring into view and retreive the cell
host.ScrollIntoView(row, host.Columns[columnIndex]);
cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
}
return cell;
}
Solution 2. WPF-style
This solution uses code-behind only for value-to-color convertions (assuming that that logic of coloring is more complex than equality comparison - in that case you can use triggers and do not mess with converters).
What you do: set DataGrid.CellStyle property with style that contains a data trigger, which checks if the cell is within a desired column (basing on it's DisplayIndex) and if it is - applies background through a converter.
XAML
<DataGrid Name="mygrid" ItemsSource="{Binding Items}" AutoGenerateColumns="True">
<DataGrid.Resources>
<local:ValueColorConverter x:Key="colorconverter"/>
</DataGrid.Resources>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Column.DisplayIndex}" Value="1">
<Setter Property="Background" Value="{Binding RelativeSource={RelativeSource Self}, Path=Content.Text, Converter={StaticResource colorconverter}}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
</DataGrid>
.CS
public class ValueColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var str = value as string;
if (str == null) return null;
int intValue;
if (!int.TryParse(str, out intValue)) return null;
if (intValue <= 1) return Brushes.Red;
else if (intValue <= 2) return Brushes.Yellow;
else return Brushes.Green;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
UPD: If you need to color entire datagrid, XAML is much easier (no need to use triggers). Use the following CellStyle:
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="Background" Value="{Binding RelativeSource={RelativeSource Self}, Path=Content.Text, Converter={StaticResource colorconverter}}"/>
</Style>
</DataGrid.CellStyle>
What i meant is that you can set the CellStyle property of the column, you can not manipulate cells directly as they are not available in this event. The style can contain your conditional logic in the form of DataTriggers (will need a converter as you have "less-than" and not equals) and Setters.
Also if the logic is not specific to the columns you can set the style globally on the grid itself. The point of using the event would be to manipulate the column properties which you can not access otherwise.
I am not sure whether this property (Cell.Style) is available in your WPF Datagrid. Probably some alternative exists in your case. It has worked for WinForms datagrid.
cell.Style.BackColor = System.Drawing.Color.Black;
I have the following Attached Property:-
public partial class GridViewProperties
{
public static readonly DependencyProperty DoAutoSizeColumnsProperty =
DependencyProperty.RegisterAttached("DoAutoSizeColumns",
typeof(bool),
typeof(GridViewProperties),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault |
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsParentArrange |
FrameworkPropertyMetadataOptions.AffectsParentMeasure |
FrameworkPropertyMetadataOptions.AffectsRender,
DoAutoSizeColumnsChanged));
public static bool GetDoAutoSizeColumns(DependencyObject obj)
{
return (bool)obj.GetValue(DoAutoSizeColumnsProperty);
}
public static void SetDoAutoSizeColumns(DependencyObject obj, bool value)
{
obj.SetValue(DoAutoSizeColumnsProperty, value);
}
private static void DoAutoSizeColumnsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var gv = obj as GridView;
if (gv == null)
return;
if (e.NewValue != null && (bool)e.NewValue)
{
AutoSizeColumns(gv.Columns);
SetDoAutoSizeColumns(gv, false);
gv.InvalidateProperty(GridView.ColumnCollectionProperty);
}
}
private static void AutoSizeColumns(GridViewColumnCollection gvcc)
{
// same code as double clicking column gripper
foreach (var gvc in gvcc)
{
// if already set to auto, toggle it so that we can re-run width="auto"
//if (double.IsNaN(gvc.Width))
gvc.Width = gvc.ActualWidth;
// now do it
gvc.Width = double.NaN;
//gvc.InvalidateProperty(GridViewColumn.WidthProperty);
//gvc.ClearValue(GridViewColumn.WidthProperty);
}
}
}
I use it in XAML in the following fashion:
<Style x:Key="AutoColumnStyle" TargetType="ListView">
<Setter Property="View">
<Setter.Value>
<GridView infra:GridViewProperties.AutoSizeColumns="{Binding Path=DataContext.DoAutoSizeColumns, Source={x:Reference uc}}">
<GridViewColumn Width="auto" Header="Title" CellTemplate="{StaticResource Name}" />
<GridViewColumn Width="auto" Header="First" CellTemplate="{StaticResource First}"/>
<GridViewColumn Width="auto" Header="Last" CellTemplate="{StaticResource Last}"/>
</GridView>
</Setter.Value>
</Setter>
The abaove is in UserControl.Resources.
The rest of the XAML is:
<ListView ItemsSource="{Binding Names}" VerticalAlignment="Top" HorizontalAlignment="Left"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
SelectionMode="Single"
x:Name="ListViewContracts"
KeyboardNavigation.TabNavigation="Continue"
KeyboardNavigation.DirectionalNavigation="Cycle"
Style="{StaticResource AutoColumnStyle}"
>
<ListView.ItemContainerStyle >
<Style BasedOn="{StaticResource ListViewItemContainerStyle}" TargetType="ListViewItem">
<Setter Property="Focusable" Value="False" />
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
(I have tried this with no Width="auto" too).
Whenever I set DoAutoSizeColumns = true in my ViewModel I see everything work as expected in the attached property except what it is desgined for that is the gridview columns are not resized according to the largest item in that column (even though I see gv.Width toggled from and to double.Nan which is how resize is meant to work).
As you can see I have tried a number of variations in the attached property mostly commented out including adding in all the FrameworkPoprertyMetadataOptions and trying various InvalidatePoperty attempts but also UIPropertyMetadata too (and DynamicResource too).
What am I missing?
UPDATE
This attached property works in other GridViews the only difference I can see here is that - I need to switch GridViews in the ListView which was not indicated above -but the key difference is that I inject as a style rather than directly. (On second thoughts this may not be the case since the firs item in a column in these other GridViews is always the largest item).
It was to do with the injection of a style.
I had two different GridViews switched by a boolean and used a DataTrigger to inject the alternate one and AutoSizeColumns worked fine on that altererate GridView! So I made both styles be injected by a DataTrigger rather having the default set by just a setter as in the XAML above.
As to why this is case I would be interested in finding out. At least I have a fully MVVM way of autosizing columns which I have not seen in other SO answers.
(BTW I have other attached properties that do not need me to manually trigger the autosize, I just wrote this version to identify the problem).
I just want to display row numbers in the left-most column of my DataGrid. Is there some attribute to do this?
Keep in mind, this isn't a primary key for my table. I don't want these row numbers to move with their rows when a column is sorted. I basically want a running count. It doesn't even need to have a header.
One way is to add them in the LoadingRow event for the DataGrid
<DataGrid Name="DataGrid" LoadingRow="DataGrid_LoadingRow" ...
void DataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
e.Row.Header = (e.Row.GetIndex()).ToString();
}
When items are added or removed from the source list then the numbers can get out of sync for a while. For a fix to this, see the attached behavior here:
WPF 4 DataGrid: Getting the Row Number into the RowHeader
Useable like this
<DataGrid ItemsSource="{Binding ...}"
behaviors:DataGridBehavior.DisplayRowNumber="True">
Adding a short info about Fredrik Hedblad answer.
<DataGrid Name="DataGrid" LoadingRow="DataGrid_LoadingRow" ...
void DataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
e.Row.Header = (e.Row.GetIndex()+1).ToString();
}
...If you want to start numbering from 1
If your data grid has its ItemsSource bound to a collection, bind the AlternationCount property of your data grid to either the the count property of your collection, or to the Items.Count property of your DataGrid as follows:
<DataGrid ItemsSource="{Binding MyObservableCollection}" AlternationCount="{Binding MyObservableCollection.Count}" />
Or:
<DataGrid ItemsSource="{Binding MyObservableCollection}" AlternationCount="{Binding Items.Count, RelativeSource={RelativeSource Self}" />
Either should work.
Then, assuming you're using a DataGridTextColumn for your leftmost column you do the following in your DataGrid.Columns definition:
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding AlternationIndex, RelativeSource={RelativeSource AncestorType=DataGridRow}}"
</DataGrid.Columns>
If you don't want to start at 0, you can add a converter to your binding to increment the index.
It's an old question, but I would like to share something. I had a similar problem, all I needed was a simple RowHeader numeration of rows and Fredrik Hedblad's answer was almost complete for my problem.
While this is great:
<DataGrid Name="DataGrid" LoadingRow="DataGrid_LoadingRow" ...
void DataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
e.Row.Header = (e.Row.GetIndex()).ToString();
}
my headers messed up when removing and adding items. If you have buttons responsible for that just add dataGrid.Items.Refresh(); under the 'deleting' code as in my case:
private void removeButton_Click(object sender, RoutedEventArgs e)
{
// delete items
dataGrid.Items.Refresh();
}
That solved desyncronized numeration for me, because refreshing items calls DataGrig_LoadingRow again.
And just to add to the discussion on this... (I spent too much time finding this out!).
You'll need to set the EnableRowVirtualization to False on the datagrid to prevent errors in the row sequencing:
EnableRowVirtualization="False"
The EnableRowVirtualization property is set to true by default. When the EnableRowVirtualization property is set to true, the DataGrid does not instantiate a DataGridRow object for each data item in the bound data source. Instead, the DataGrid creates DataGridRow objects only when they are needed, and reuses them as much as it can. MSDN Reference here
Just another answer to provide almost copy&paste example (not to be encouraged) for new people or people in a rush, inspired by answers inside this post by #GrantA and #Johan Larsson ( + many other people who answered to the numerous posts on that subject)
You may not want to add the enumeration inside a column
You do not need to re-create your own Attached Property
<UserControl...
<Grid>
<DataGrid ItemsSource="{Binding MainData.ProjColl}"
AutoGenerateColumns="False"
AlternationCount="{ Binding MainData.ProjColl.Count}" >
<DataGrid.Columns>
<!--Columns given here for example-->
...
<DataGridTextColumn Header="Project Name"
Binding="{Binding CMProjectItemDirName}"
IsReadOnly="True"/>
...
<DataGridTextColumn Header="Sources Dir"
Binding="{Binding CMSourceDir.DirStr}"/>
...
</DataGrid.Columns>
<!--The problem of the day-->
<DataGrid.RowHeaderStyle>
<Style TargetType="{x:Type DataGridRowHeader}">
<Setter Property="Content"
Value="{Binding Path=(ItemsControl.AlternationIndex),
RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
</Style>
</DataGrid.RowHeaderStyle>
</DataGrid>
</Grid>
</UserControl>
Note the parenthesis () around (ItemsControl.AlternationIndex) as warned in Fredrik Hedblad answer in Check if Row as odd number
After some Tests withRowHeaderStyle, the repaired and extended sample from NGI:
<DataGrid EnableRowVirtualization="false" ItemsSource="{Binding ResultView}" AlternationCount="{Binding ResultView.Count}" RowHeaderWidth="10">
<DataGrid.RowHeaderStyle>
<Style TargetType="{x:Type DataGridRowHeader}">
<Setter Property="Content" Value="{Binding Path=AlternationIndex, RelativeSource={RelativeSource AncestorType=DataGridRow}}" />
</Style>
</DataGrid.RowHeaderStyle>
</DataGrid>
Using attached properties, full source here.
using System.Windows;
using System.Windows.Controls;
public static class Index
{
private static readonly DependencyPropertyKey OfPropertyKey = DependencyProperty.RegisterAttachedReadOnly(
"Of",
typeof(int),
typeof(Index),
new PropertyMetadata(-1));
public static readonly DependencyProperty OfProperty = OfPropertyKey.DependencyProperty;
public static readonly DependencyProperty InProperty = DependencyProperty.RegisterAttached(
"In",
typeof(DataGrid),
typeof(Index),
new PropertyMetadata(default(DataGrid), OnInChanged));
public static void SetOf(this DataGridRow element, int value)
{
element.SetValue(OfPropertyKey, value);
}
public static int GetOf(this DataGridRow element)
{
return (int)element.GetValue(OfProperty);
}
public static void SetIn(this DataGridRow element, DataGrid value)
{
element.SetValue(InProperty, value);
}
public static DataGrid GetIn(this DataGridRow element)
{
return (DataGrid)element.GetValue(InProperty);
}
private static void OnInChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var row = (DataGridRow)d;
row.SetOf(row.GetIndex());
}
}
Xaml:
<DataGrid ItemsSource="{Binding Data}">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="dataGrid2D:Index.In"
Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
</Style>
</DataGrid.RowStyle>
<DataGrid.RowHeaderStyle>
<Style TargetType="{x:Type DataGridRowHeader}">
<Setter Property="Content"
Value="{Binding Path=(dataGrid2D:Index.Of),
RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}}" />
</Style>
</DataGrid.RowHeaderStyle>
</DataGrid>
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?
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 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;
}
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;
}
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.
}
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.
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>
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}">
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.
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;
}
}
}
<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;
}
is there a way to mimic the behavior of ctrl+click which keeps previously selected rows selected and just adds more selected items?
by default, when clicking on each row, all previously selected rows get de-selected.
one way to achieve this, would be to override SelectionChanged event, and re-select removed rows.
void TestGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) {
foreach (var i in e.RemovedItems)
TestGrid.SelectedItems.Add(i);
}
This is not ideal however, because in some situations i would want to de-select rows (such as when clicking a toggle button in one of the columns).
This is not pretty, but it works if you can live with selecting multiple rows by dragging not working.
private void dataGrid_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
var dep = (DependencyObject)e.OriginalSource;
// iteratively traverse the visual tree
while ((dep != null) &&
!(dep is DataGridRow))
{
dep = VisualTreeHelper.GetParent(dep);
}
if (dep == null)
return;
if (dep is DataGridRow)
{
var row = dep as DataGridRow;
row.IsSelected = !row.IsSelected;
e.Handled = true;
}
}
Set the SelectionMode to DataGridSelectionMode.Extended
here's a solution that worked for me.
i removed all properties that set details visiblity (to keep everything at default)
than added the following style
<Style x:Key="VisibilityStyle" TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Visible}" Value="False">
<Setter Property="DetailsVisibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Visible}" Value="True">
<Setter Property="DetailsVisibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
assigned this resource to RowStyle
in my underlying data object, i added Visible property, and Implemented INotifyPropertyChanged interface.
now whenever i want to show/hide details, i simply manipulate the Visible property on my underlying object. this can happen from column button handler, to anywhere else in my code. works great