Search code examples
c#wpfxamldatagrididataerrorinfo

WPF DataGrid TextBoxes retaining previous values when there are multiple validation errors


I have created a DataGrid with two columns that both use TextBoxs for editing the properties of a ViewModel. When both columns have validation errors, and the property values are changed from the ViewModel, entering edit mode in one of the cells retains the previously edited value.

Here is a short example:

View

<Window ...>
    <Window.DataContext>
        <ViewModels:MainPresenter />
    </Window.DataContext>

    <DockPanel>
        <Button Command="{Binding ResetValuesCommand}"
                Margin="5" DockPanel.Dock="Top">Reset Values</Button>

        <DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False" Margin="5">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Value 1"
                    Binding="{Binding Value1, ValidatesOnDataErrors=True}" />
                <DataGridTextColumn Header="Value 2"
                    Binding="{Binding Value2, ValidatesOnDataErrors=True}" />
            </DataGrid.Columns>
        </DataGrid>
    </DockPanel>
</Window>

ViewModel

public class MainPresenter : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public IEnumerable<ItemPresenter> Items { get; }
        = new ObservableCollection<ItemPresenter> {new ItemPresenter()};

    public ICommand ResetValuesCommand => new ResetCommand(Items);

    private class ResetCommand : ICommand
    {
        private readonly IEnumerable<ItemPresenter> _items;

        public ResetCommand(IEnumerable<ItemPresenter> items) { _items = items; }

        public void Execute(object parameter) => _items.ToList().ForEach(i => i.Reset());

        public bool CanExecute(object parameter) => true;

        public event EventHandler CanExecuteChanged { add { } remove { } }
    }
}

public class ItemPresenter : INotifyPropertyChanged, IDataErrorInfo
{
    public event PropertyChangedEventHandler PropertyChanged;

    public string Value1 { get; set; } = "A";

    public string Value2 { get; set; } = "B";

    public string this[string columnName] => "ERROR";

    public string Error => "ERROR";

    public void Reset()
    {
        Value1 = "A";
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value1)));
        Value2 = "B";
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value2)));
    }
}

Steps to Reproduce

When running the app, both columns are highlighted as being invalid.

  • Double-click on the cell in 'Value 1' column (currently having value "A") and change it, e.g. to "Z";
  • Press enter (or equivalent) to commit the change;
  • Press the 'Reset Values' button (causing the cell we have just edited to change to "A");
  • Double-click on the cell in 'Value 1' column again.

The cell in 'Value 1' column changes to edit mode and the value is shown as "Z" again.

One thing to note: this only occurs when other columns have validation errors. If this is the only column with a validation error, then the TextBox in edit mode will correctly show "A" when entering edit mode.

Partial Fix

Oddly, explicitly setting the Mode in the bindings to TwoWay (presumably the default, as that's the apparent behaviour) fixes the problem.

However, if I want some custom cell templates (and replace the DataGridTextColumns with DataGridTemplateColumnss) but still use a TextBox for editing:

<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False" Margin="5">
    <DataGrid.Columns>
        <DataGridTemplateColumn Header="Value 1">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate DataType="{x:Type ViewModels:ItemPresenter}">
                    <TextBlock Text="{Binding Value1, ValidatesOnDataErrors=True}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate DataType="{x:Type ViewModels:ItemPresenter}">
                    <TextBox Text="{Binding Value1, ValidatesOnDataErrors=True}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>

        <DataGridTemplateColumn Header="Value 2">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate DataType="{x:Type ViewModels:ItemPresenter}">
                    <TextBlock Text="{Binding Value2, ValidatesOnDataErrors=True}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate DataType="{x:Type ViewModels:ItemPresenter}">
                    <TextBox Text="{Binding Value2, ValidatesOnDataErrors=True}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

I encounter the same problem but explicitly setting the binding modes to TwoWay does not fix it.

Am I doing something wrong somewhere, that I've overlooked? Alternatively, does anyone have a workaround for this?


Solution

  • I've found a workaround.

    If I change to using INotifyDataErrorInfo (only available in .NET 4.5 and above), then it works as expected.

    ViewModel

    public class ItemPresenter : INotifyPropertyChanged, INotifyDataErrorInfo
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    
        public string Value1 { get; set; } = "A";
    
        public string Value2 { get; set; } = "B";
    
        public IEnumerable GetErrors(string propertyName) => new[] { "ERROR" };
    
        public bool HasErrors => true;
    
        public void Reset()
        {
            Value1 = "A";
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value1)));
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value1)));
            Value2 = "B";
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value2)));
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value2)));
        }
    }
    

    View

    <DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False" Margin="5">
        <DataGrid.Columns>
            <DataGridTemplateColumn Header="Value 1">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate DataType="{x:Type ViewModels:ItemPresenter}">
                        <TextBlock Text="{Binding Value1}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
                <DataGridTemplateColumn.CellEditingTemplate>
                    <DataTemplate DataType="{x:Type ViewModels:ItemPresenter}">
                        <TextBox Text="{Binding Value1}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellEditingTemplate>
            </DataGridTemplateColumn>
            <DataGridTemplateColumn Header="Value 2">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate DataType="{x:Type ViewModels:ItemPresenter}">
                        <TextBlock Text="{Binding Value2}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
                <DataGridTemplateColumn.CellEditingTemplate>
                    <DataTemplate DataType="{x:Type ViewModels:ItemPresenter}">
                        <TextBox Text="{Binding Value2}" />
                    </DataTemplate>
                </DataGridTemplateColumn.CellEditingTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>
    </DataGrid>