INotifyDataErrorInfo WPF implementation

ChummyChum

Member
Joined
Sep 28, 2022
Messages
10
Programming Experience
1-3
Data validation in WPF seems too complicated, I am slowly getting the hang of it, but still need some help from you guys when using INotifyDataErrorInfo.

So, my problem is that I don't want my validation occur on property change, that is why I've set it up that all my property checks are done when a user clicks 'Save' button.

When the button is clicked I check all my properties and do 'AddError()' if something is not right. It works, the screen lights up in red, error messages appear etc.

BUT I am stuck on how to raise ErrorChanged for all of the properties when I clear it.

So when a user clicks 'Save' button again I want to clear all of the errors dictionary.clear() and then raise the errorschanged event BUT for all of the properties.

In all of the tutorials about INotifyDataErrorInfo there is:

C#:
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
and I get that but I want to raise Errors changed for the whole form, dont want to raise individually.

Is something like that possible? Personally I am a fan of form validation on user click and not on property changed.

I hope I described my ask clearly :)
 
Last edited:
Wow, I missed that line when I was reading that documentation earlier.
Jesus.
C#:
null or Empty if the error is object-level.
Thank you man!
 
Heh, tried it and its not working :(
It seems that the view is not getting that the errors changed. I am passing it empty instead of property name. (If I pass it propertyName it works correctly)
The Error message still stays on the screen even the errors dictionary is cleared out.

C#:
 private void RaiseErrorChanged(string propertyName)
        {
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }
        public void ClearAllErrors()
        {
            _formGreske.Clear();
            RaiseErrorChanged(string.Empty); //za resetiranje svih property samo proslijedi empty/null string
        }
1664443128180.png


C#:
<StackPanel >
                        <Label Style="{StaticResource LabelTitles}"
                           Content="Broj transakcije"></Label>
                        <TextBox Style="{StaticResource InputBox}"
                                 Text="{Binding NoviUnos.BrojTransakcije,
                            ValidatesOnNotifyDataErrors=True}">
                        </TextBox>
                    </StackPanel>


EDIT:
When the value is changed inside the textbox and on clicking 'SAVE' the IEnumerable GetErrors() method of the INotify interface fires up and remembers the previous error, that is why it is staying on the view. I dont understand why that method is firing up before everything else.
 
Last edited:
What does your GetErrors() implementation look like?
 
What does your GetErrors() implementation look like?
C#:
 public IEnumerable GetErrors(string propertyName)
        {
            if (propertyName == null)
            {
                return null;
            }
            else
            {
                if (_formGreske.ContainsKey(propertyName))
                {
                    return _formGreske[propertyName];
                }
                else { return null; }
            }

        }
 
When the value is changed inside the textbox and on clicking 'SAVE' the IEnumerable GetErrors() method of the INotify interface fires up and remembers the previous error, that is why it is staying on the view. I dont understand why that method is firing up before everything else.
I have vague recollections that validation on specific controls are initiated when focus is lost. So when the user clicks on the "Save" button, the focus is lost on your textbox, validation is performed, and then your button event command is executed.
 
Its something about updating the view and I think GetErrors() has its fingers in it.

I am validating if Len <4 then I am throwing an error, so this:
1664456210295.png

throws an error correctly.

But if enter 4 number the control passes but the view is not updating:
1664456261555.png


AND if I enter 5 numbers and click 'Save' then it updates:
1664456286690.png
 
Anyway, the following blog post maybe useful:
 
Anyway, the following blog post maybe useful:
a familiar article, thanks :) maybe I'l go over it a few more times :LOL:
 
Magnus article is saying like you proposed:
If the UpdateSourceTrigger is set to LostFocus, which is the default for the Text property of the TextBox control, the text you type into the TextBox does not update the source property until the control loses focus which happens when you click away from it
so If I put
C#:
UpdateSourceTrigger=PropertyChanged
the validation occurs like in all of the tutorials around. As soon as the property changes the validation runs and the error disappears.

I think that I can't get what I want with this interface. Since I am not AddingErrors() when I set {} the property, the view can't call GetErrors() and thus it does not know that my errors are cleared.

Only if there would be a way to force the view to call getErrors even if the property is not change or out of focus.
 
Basically you want to do the "Cross Property errors" when the save button is pressed.

Anyway, the following code works for me:
MainWindowViewModel.cs:
using Microsoft.VisualBasic;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace TestWpfValidation
{
    internal class MainViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
    {
        Person _person;

        public ICommand SaveCommand { get; init; }

        public string Name
        {
            get => _person.Name;
            set
            {
                if (_person.Name != value)
                {
                    _person.Name = value;
                    FirePropertyChange();
                }
            }
        }

        public int Age
        {
            get => _person.Age;
            set
            {
                if (_person.Age != value)
                {
                    _person.Age = value;
                    FirePropertyChange();
                }
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        void FirePropertyChange([CallerMemberName] string? name = null)
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

        public MainViewModel(Person person)
        {
            _person = person;
            SaveCommand = new SaveCommand(this);
        }

        public void DoSave()
        {
            if (Name.Length <= 4)
                AddError("Name too short", "Name");
            else
                ClearError("Name");

            if (Age <= 0)
                AddError("Age too low", "Age");
            else
                ClearError("Age");
        }

        Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();

        public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;

        public bool HasErrors => _errors.Values.SelectMany(e => e).Count() > 0;

        void AddError(string error, [CallerMemberName] string? name = null)
            => ActOnError(name, l => l.Add(error));

        void ClearError([CallerMemberName] string? name = null)
            => ActOnError(name, l => l.Clear());

        void ActOnError(string? name, Action<List<string>> action)
        {
            if (string.IsNullOrWhiteSpace(name))
                return;
            if (!_errors.ContainsKey(name))
                _errors.Add(name, new List<string>());
            action(_errors[name]);
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(name));
        }

        public IEnumerable GetErrors(string? propertyName)
        {
            var errors = new List<string>();
            if (!string.IsNullOrWhiteSpace(propertyName) &&
                _errors.ContainsKey(propertyName))
            {
                errors = _errors[propertyName];
            }
            return errors;
        }
    }

    class SaveCommand : ICommand
    {
        MainViewModel _mainViewModel;
        public event EventHandler? CanExecuteChanged;

        public SaveCommand(MainViewModel mainViewModel) => _mainViewModel = mainViewModel;
        public bool CanExecute(object? parameter) => true;
        public void Execute(object? parameter) => _mainViewModel.DoSave();
    }
}

MainWindow.xaml:
<Window x:Class="TestWpfValidation.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:TestWpfValidation"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <Label>Name</Label>
        <TextBox Text="{Binding Name, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}"></TextBox>
        <Label>Age</Label>
        <TextBox Text="{Binding Age, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}"></TextBox>
        <Button Command="{Binding SaveCommand}">Save</Button>
    </StackPanel>
</Window>


MainWindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TestWpfValidation
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainViewModel(new Person());
        }
    }
}

Person.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestWpfValidation
{
    internal class Person
    {
        public string Name { get; set; } = String.Empty;
        public int Age { get; set; }
    }
}
 
OK, the setup like that so basiclly what every other tutorial is instructing, it works, and I understand how and why.
But what if you want to clear errors for Name and Age at the beginning of the method? That is what I am trying to do.

So like this:
C#:
 public void DoSave()
        {
             ClearError(string.empty);
            if (Name.Length <= 4)
                AddError("Name too short", "Name");

            if (Age <= 0)
                AddError("Age too low", "Age");
        }

I thought that by passing empty string
C#:
 ClearError(string.empty);
it will clear out everything, but in my case it is not happening
 
Back
Top Bottom