Skip to content

Instantly share code, notes, and snippets.

@marbel82
Last active March 5, 2020 10:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marbel82/38435f3cd8a8a3b9e373a1e62d901f2b to your computer and use it in GitHub Desktop.
Save marbel82/38435f3cd8a8a3b9e373a1e62d901f2b to your computer and use it in GitHub Desktop.
WPF DataGrid: Show CancelEdit button when row is in edit mode + Cancel row edition

Window picture with DataGrid with Cancel edit button

Question 1: WPF DataGrid - How to show a button in a row when the row is editing (detect edit mode)?

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


DataGrid with cancel changes button

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

How to find cells with an error (search for visual children)?

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

How to find cells with an error (BindingGroup.BindingExpressions)?

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;
}
@marbel82
Copy link
Author

marbel82 commented Mar 5, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment