INotifyDataErrorInfo implementation for several values, check that there are no errors in Dictionary (WPF MVVM)

mrw 201 Reputation points
2020-12-22T22:08:25.157+00:00

I have been trying to implement INotifyDataErrorInfo and everything seems to be working except SaveSettings() and especially bool HasErrors. Can somebody please tell me why and how to fix it?

I mean that SaveSettings is always triggered, even if there are errors in TextBoxes. It should be triggered only if there are no errors.

ViewModel.cs:

using System;
using System.Collections;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

namespace SimpleValidator.ViewModels
{
    public class ViewModel : BaseViewModel, INotifyDataErrorInfo
    {
        private readonly PropertyErrors errors;

        public ICommand SaveCommand => new RelayCommand(param => this.SaveSettings());

        public ViewModel()
        {
            this.errors = new PropertyErrors(this, this.OnErrorsChanged);

            this.TimeLimit = "12:23";

            this.TimeRecordInterval = "11";
        }

        private void SaveSettings()
        {
            if (!HasErrors)
                MessageBox.Show("Save");
        }

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        private string _timeLimit;
        public string TimeLimit
        {
            get { return this._timeLimit; }
            set
            {
                if (value == this._timeLimit)
                {
                    return;
                }

                this._timeLimit = value;
                this.errors.Clear(nameof(this.TimeLimit));
                OnPropertyChanged();
            }
        }

        private string _timeRecordInterval;
        public string TimeRecordInterval
        {
            get { return this._timeRecordInterval; }
            set
            {
                if (value == this._timeRecordInterval)
                {
                    return;
                }

                this._timeRecordInterval = value;
                this.errors.Clear(nameof(this.TimeRecordInterval));
                OnPropertyChanged();
            }
        }

        public bool HasErrors => this.errors.HasErrors;

        public IEnumerable GetErrors(string propertyName) => this.errors.GetErrors(propertyName);

        protected virtual void OnErrorsChanged(DataErrorsChangedEventArgs e)
        {
            this.ErrorsChanged?.Invoke(this, e);
        }
    }
}
Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,671 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,247 questions
XAML
XAML
A language based on Extensible Markup Language (XML) that enables developers to specify a hierarchy of objects with a set of properties and logic.
765 questions
{count} votes

Accepted answer
  1. Peter Fleischer (former MVP) 19,231 Reputation points
    2020-12-23T11:21:47.033+00:00

    Hi,
    in your code you validate in 2 levels: UI and data object (in your case in ViewModel). Validation in UI level your code don't inform ViewModel. If you want inform ViewModel you can:

    Set Validation.Error and NotifyOnValidationError="True"

    <Label>Interval</Label>
    <TextBox x:Name="TimeRecordInterval" Validation.Error="Validation_Error"
               Grid.Row="0"
               Grid.Column="1">
      <TextBox.Text>
        <Binding Path="TimeRecordInterval" NotifyOnValidationError="True"
                       UpdateSourceTrigger="PropertyChanged">
          <Binding.ValidationRules>
            <viewmodels:NumberValidationRule numberValidation="number" />
          </Binding.ValidationRules>
        </Binding>
          </TextBox.Text>
    ...
    

    Include Event method:

    using SimpleValidator.ViewModels;
    using System.Windows;
    using System.Windows.Controls;
    
    namespace SimpleValidator
    {
      /// <summary>
      /// Interaction logic for MainWindow.xaml
      /// </summary>
      public partial class MainWindow : Window
      {
        private ViewModel ViewModel { get; set; }
    
        public MainWindow()
        {
          ViewModel = new ViewModel();
          DataContext = ViewModel;
          InitializeComponent();
        }
    
        private void Validation_Error(object sender, System.Windows.Controls.ValidationErrorEventArgs e)
        {
          string name = ((TextBox)sender)?.Name;
          ViewModel.SetTimeRecordInterval(name, e);
        }
      }
    }
    

    and in ViewModel:

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    
    namespace SimpleValidator.ViewModels
    {
      public class ViewModel : BaseViewModel, INotifyDataErrorInfo
      {
        private readonly PropertyErrors errors;
    
        public ICommand SaveCommand => new RelayCommand(param => this.SaveSettings(), 
          (obj) => !this.HasErrors && this.UiErrorMessages.Count==0);
    
        private readonly Dictionary<string, string> UiErrorMessages = new Dictionary<string, string>();
    
        public void SetTimeRecordInterval(string ctrlName, ValidationErrorEventArgs e)
        {
          if (e.Action == ValidationErrorEventAction.Added) this.UiErrorMessages[ctrlName] = e.Error.ErrorContent.ToString();
          if (e.Action == ValidationErrorEventAction.Removed) this.UiErrorMessages.Remove(ctrlName);
          OnPropertyChanged(nameof(SaveCommand));
        }
    
        private void SaveSettings()
        {
          if (!HasErrors && UiErrorMessages.Count == 0)
            MessageBox.Show("Save");
        }
    
    1 person found this answer helpful.
    0 comments No comments

0 additional answers

Sort by: Most helpful