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 :)
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
6,559
Location
Chesapeake, VA
Programming Experience
10+
Last edited:

ChummyChum

Member
Joined
Sep 28, 2022
Messages
10
Programming Experience
1-3
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!
 

ChummyChum

Member
Joined
Sep 28, 2022
Messages
10
Programming Experience
1-3
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:

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
6,559
Location
Chesapeake, VA
Programming Experience
10+
What does your GetErrors() implementation look like?
 

ChummyChum

Member
Joined
Sep 28, 2022
Messages
10
Programming Experience
1-3
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; }
            }

        }
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
6,559
Location
Chesapeake, VA
Programming Experience
10+
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.
 

ChummyChum

Member
Joined
Sep 28, 2022
Messages
10
Programming Experience
1-3
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
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
6,559
Location
Chesapeake, VA
Programming Experience
10+
Anyway, the following blog post maybe useful:
 

ChummyChum

Member
Joined
Sep 28, 2022
Messages
10
Programming Experience
1-3
Anyway, the following blog post maybe useful:
a familiar article, thanks :) maybe I'l go over it a few more times :LOL:
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
6,559
Location
Chesapeake, VA
Programming Experience
10+

ChummyChum

Member
Joined
Sep 28, 2022
Messages
10
Programming Experience
1-3
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.
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
6,559
Location
Chesapeake, VA
Programming Experience
10+
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; }
    }
}
 

ChummyChum

Member
Joined
Sep 28, 2022
Messages
10
Programming Experience
1-3
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
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
6,559
Location
Chesapeake, VA
Programming Experience
10+
That's what I thought would happen as well, but as I discovered it does not. That's why my test code above goes and clears the errors on fields that are valid.

What is interesting, though, is if your bind to the entire view model, passing in a null property name for the ErrorsChanged event will actually call GetErrors() passing in null once, and then an empty string the second time. The issue though was returning an empty list did not clear all errors, and returning a non-empty list did not set errors on all.

Thinking about this more, it makes sense. If you had to implement this validation systems using INotifyDataError, how would you know which controls to mark as having errors or not having errors when you don't have a property name? How will you deal with UI elements that are dynamically added? Best to just let the programmer to deal with it.
 
Top Bottom