Question 1: WPF DataGrid - How to show a button in a row when the row is editing (detect edit mode)?
- revision 1 - Code for the question 1
- revision 2 - Add Button Visibility binding (solution)
Question 2: WPF DataGrid - How to cancel edit row after validation error?
- revision 3 - Code for the question 2
- revision 4 - Add Date2 column by DataGridTemplateColumn
- revision 5 - Cancel editing all cells in a row
- revision 6 - Other possibilities of finding cells containing errors (↓ read below)
- revision 7 - Must have - CellEditingTemplate
- revision 8 (current) (solution) - CurrentCell need to be restored
Useful links - binding, validation, DataGrid:
https://docs.microsoft.com/en-us/dotnet/framework/wpf/data/data-binding-overview
https://msdn.microsoft.com/en-us/magazine/ff714593.aspx
https://www.codeproject.com/Articles/30905/WPF-DataGrid-Practical-Examples
https://www.codeproject.com/Articles/863291/Validation-in-WPF
https://blog.magnusmontin.net/2013/08/26/data-validation-in-wpf/
https://www.wpfsharp.com/2012/02/03/how-to-disable-a-button-on-textbox-validationerrors-in-wpf/
WPF Examples: https://github.com/microsoft/WPF-Samples/tree/master/Data%20Binding/BindValidation
<!-- MainWindow.xaml -->
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="240" Width="380">
<Window.Resources>
<local:DateConverter x:Key="DateConverter"/>
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</Window.Resources>
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Button Content="? Add" Width="50" Margin="4" Click="AddButton_Click"/>
</StackPanel>
<DataGrid x:Name="datagrid" DockPanel.Dock="Bottom" ItemsSource="{Binding Items}" AutoGenerateColumns="False"
CanUserAddRows="False" SelectionMode="Single" SelectionUnit="FullRow" Margin="3">
<DataGrid.Columns>
<DataGridTextColumn Header="Something" Width="*" Binding="{Binding Something}" />
<DataGridTextColumn Header="Date" Width="100" Binding="{Binding Date, Converter={StaticResource DateConverter}, ValidatesOnDataErrors=True}" />
<DataGridTemplateColumn Header="Date2" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Date, Converter={StaticResource DateConverter}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding Date, Converter={StaticResource DateConverter}, ValidatesOnDataErrors=True}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Action" Width="Auto" IsReadOnly="True" CanUserResize="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<UniformGrid Rows="1">
<Button Content="?" ToolTip="Cancel changes" Click="CancelChangesButton_Click" HorizontalAlignment="Center"
Visibility="{Binding IsEditing, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}, Converter={StaticResource BoolToVisibilityConverter}}"/>
</UniformGrid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Window>
// MainWindow.xaml.cs
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
namespace WpfApp1
{
public partial class MainWindow : Window
{
public class SomethingItem
{
public string Something
{
get { return (string)GetValue(SomethingProperty); }
set { SetValue(SomethingProperty, value); }
}
public DateTime Date
{
get { return (DateTime)GetValue(DateProperty); }
set { SetValue(DateProperty, value); }
}
public static readonly DependencyProperty SomethingProperty =
DependencyProperty.Register("Something", typeof(string), typeof(SomethingItem));
public static readonly DependencyProperty DateProperty =
DependencyProperty.Register("Date", typeof(DateTime), typeof(SomethingItem));
}
public ObservableCollection<SomethingItem> Items { get; } = new ObservableCollection<SomethingItem>();
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void AddButton_Click(object sender, RoutedEventArgs e)
{
SomethingItem si = new SomethingItem
{
Something = "<write\u00A0something>",
Date = DateTime.Now.Date,
};
Items.Add(si);
// Move the focus to the new item
datagrid.SelectedValue = si;
datagrid.CurrentCell = new DataGridCellInfo(si, datagrid.Columns[0]);
Dispatcher.BeginInvoke((Action)(() => datagrid.BeginEdit()), DispatcherPriority.Background);
}
private void CancelChangesButton_Click(object sender, RoutedEventArgs e)
{
var cc = dataGrid.CurrentCell;
foreach (var col in datagrid.Columns)
{
datagrid.CurrentCell = new DataGridCellInfo(datagrid.CurrentItem, col);
datagrid.CancelEdit();
}
dataGrid.CurrentCell = cc;
}
}
public class DateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is DateTime d)
return d.ToString("d");
else
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (DateTime.TryParse((string)value, out DateTime dt))
return dt.Date;
else
return DependencyProperty.UnsetValue;
}
}
}
private void CancelChangesCellsHavingError()
{
SomethingItem item = datagrid.CurrentItem as SomethingItem;
DataGridRow row = (DataGridRow)datagrid.ItemContainerGenerator.ContainerFromItem(item);
if (Validation.GetHasError(row))
{
var cc = dataGrid.CurrentCell;
foreach (DataGridColumn col in datagrid.Columns)
{
DataGridCell cell = (DataGridCell)col.GetCellContent(item).Parent;
List<DependencyObject> errs = GetVisualChildrenHavingError(cell);
if (errs != null)
{
datagrid.CurrentCell = new DataGridCellInfo(item, col);
datagrid.CancelEdit(DataGridEditingUnit.Cell);
}
}
dataGrid.CurrentCell = cc;
}
}
/// <summary>
/// Returns all visual children that HasError. Return null if nothing is found.
/// </summary>
public static List<DependencyObject> GetVisualChildrenHavingError(DependencyObject parent)
{
List<DependencyObject> result = null;
GetVisualChildrenHavingError(parent, ref result);
return result;
}
private static void GetVisualChildrenHavingError(DependencyObject parent, ref List<DependencyObject> result)
{
for (int childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (Validation.GetHasError(childElement))
{
if (result == null)
result = new List<DependencyObject>();
result.Add(childElement);
}
GetVisualChildrenHavingError(childElement, ref result);
}
}
private void CancelChangesCellsHavingError_BindingGroup()
{
SomethingItem item = datagrid.CurrentItem as SomethingItem;
DataGridRow row = (DataGridRow)datagrid.ItemContainerGenerator.ContainerFromItem(item);
if (Validation.GetHasError(row))
{
var cc = dataGrid.CurrentCell;
foreach (BindingExpressionBase exp in row.BindingGroup.BindingExpressions.ToArray())
{
if (exp.HasError)
{
DataGridCell cell = FindVisualParent<DataGridCell>(exp.Target);
datagrid.CurrentCell = new DataGridCellInfo(item, GetGridColumnFromGridCell(cell, item));
datagrid.CancelEdit(DataGridEditingUnit.Cell);
}
}
dataGrid.CurrentCell = cc;
}
}
private DataGridColumn GetGridColumnFromGridCell(DataGridCell cell, object item)
{
return datagrid.Columns.FirstOrDefault(c => cell.Equals(c.GetCellContent(item).Parent));
}
public static TParent FindVisualParent<TParent>(DependencyObject child) where TParent : DependencyObject
{
while (child != null)
{
child = VisualTreeHelper.GetParent(child);
if (child is TParent parent)
return parent;
}
return null;
}
When I edit a cell and press cancel edit, I can't enter edit mode immediately again. I need to click another cell and come back.
CurrentCell
should be restored.