Search code examples
c#.net-coreidataerrorinfoinotifydataerrorinfo

Replacing IDataErrorInfo with INotifyDataErrorInfo


I've a class named Person with two properties FirstName, LastName, two Constructors, one ICommand and usual stuffs required for INotifyPropertyChanged and IDataErrorInfo:

class Person : ObservableCollection<Person>, INotifyPropertyChanged, IDataErrorInfo
{
    string firstName, lastName;

    #region Properties
    [Required(ErrorMessage = "First Name is Required")]
    [RegularExpression("test", ErrorMessage = "It's to be test")]
    public string FirstName {
        get => firstName;
        set { firstName = value; OnPropertyChanged(); }
    }

    [Required]
    [RegularExpression("test", ErrorMessage = "It also has to be test")]
    public string LastName {
        get => lastName;
        set { lastName = value; OnPropertyChanged(); }
    }
    #endregion Properties
   
    #region Constructors
    public Person(){
        AddToList = new Command(CanAdd, Add);
    }

    public Person(string fName, string lName){
        FirstName = fName;
        LastName = lName;
    }
    #endregion Constructors

    #region Command
    public ICommand AddToList { get; set; }
    bool CanAdd(object para) => Validator.TryValidateObject(this, new ValidationContext(this), null, true);
    void Add(object para){
        Add(new Person(FirstName, LastName));
        FirstName = LastName = null;
    }
    #endregion Command

    #region INotifyPropertyChanged
    public new event PropertyChangedEventHandler PropertyChanged;
    void OnPropertyChanged([CallerMemberName] string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    #endregion INotifyPropertyChanged

    #region IDataErrorInfo
    public string Error => null;
    public string this[string columnName] {
        get {
            var ValidationResults = new List<ValidationResult>();
            if (Validator.TryValidateProperty(
                    GetType().GetProperty(columnName).GetValue(this),
                    new ValidationContext(this) { MemberName = columnName },
                    ValidationResults
                )) return null;

            return ValidationResults.First().ErrorMessage;
        }
    }
    #endregion IDataErrorInfo
}

in xaml I've two TextBox bound to FirstName and LastName of Person, two Label for validation error message and a Button, bound to the ICommand, to add Person in the following ListView:

<Window ...>
    <Window.Resources>
        <local:Person x:Key="Person"/>
    </Window.Resources>

    <Grid DataContext="{StaticResource Person}">
        <StackPanel>
            <TextBox x:Name="Fname" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
            <Label Content="{Binding (Validation.Errors)[0].ErrorContent, ElementName=Fname}"/>

            <TextBox x:Name="Lname" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"/>
            <Label Content="{Binding (Validation.Errors).CurrentItem.ErrorContent, ElementName=Lname}"/>
        
            <Button Content="Click" Command="{Binding AddToList}" />
        
            <ListView x:Name="lv" ItemsSource="{Binding}">
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="First Name" Width="200" 
                            DisplayMemberBinding="{Binding FirstName}"/>
                        <GridViewColumn Header="Last Name" Width="200" 
                            DisplayMemberBinding="{Binding LastName}" />
                    </GridView>
                </ListView.View>
            </ListView>
        </StackPanel>
    </Grid>
</Window>

It works, I get error message if First and Last names are not valid and the Button remains disabled as long as any of the First and Last names is invalid. I want to replace only the IDataErrorInfo part with INotifyDataErrorInfo. What changes do I have to make in Person class and xaml to keep the same functionality?


Solution

  • The framework calls GetErrors each time you raise the ErrorsChanged event. Since there is an HasErrors property that should return true whenever there are any validation errors, it makes sense to validate in the property setters and cache the validation errors in a Dictionary<string, List<ValidationResult>>.

    Please refer to the following sample implementation:

    class Person : ObservableCollection<Person>, INotifyPropertyChanged, INotifyDataErrorInfo
    {
        string firstName, lastName;
    
        #region Properties
        [Required(ErrorMessage = "First Name is Required")]
        [RegularExpression("test", ErrorMessage = "It's to be test")]
        public string FirstName
        {
            get => firstName;
            set { firstName = value; OnPropertyChanged(); Validate(); }
        }
    
        [Required]
        [RegularExpression("test", ErrorMessage = "It also has to be test")]
        public string LastName
        {
            get => lastName;
            set { lastName = value; OnPropertyChanged(); Validate(); }
        }
        #endregion Properties
    
        #region Constructors
        public Person()
        {
            AddToList = new Command(CanAdd, Add);
            Validate(nameof(FirstName));
            Validate(nameof(LastName));
        }
    
        public Person(string fName, string lName)
        {
            FirstName = fName;
            LastName = lName;
        }
        #endregion Constructors
    
        #region Command
        public ICommand AddToList { get; set; }
        bool CanAdd(object para) => _validationResults.Count == 0;
        void Add(object para)
        {
            base.Add(new Person(FirstName, LastName));
            FirstName = LastName = null;
        }
        #endregion Command
    
        #region INotifyPropertyChanged
        public new event PropertyChangedEventHandler PropertyChanged;
        void OnPropertyChanged([CallerMemberName] string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        #endregion INotifyPropertyChanged
    
        #region INotifyDataErrorInfo
        private readonly Dictionary<string, List<ValidationResult>> _validationResults = new Dictionary<string, List<ValidationResult>>();
    
        public bool HasErrors => _validationResults.Count > 0;
    
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    
        public System.Collections.IEnumerable GetErrors(string propertyName)
        {
            if (_validationResults.TryGetValue(propertyName, out List<ValidationResult> validationResults))
                return new string[1] { validationResults.First().ErrorMessage };
            return null;
        }
    
        private void Validate([CallerMemberName]string propertyName = "")
        {
            var ValidationResults = new List<ValidationResult>();
            if (Validator.TryValidateProperty(typeof(Person).GetProperty(propertyName).GetValue(this),
                    new ValidationContext(this) { MemberName = propertyName }, ValidationResults))
            {
                _validationResults.Remove(propertyName);
            }
            else
            {
                _validationResults[propertyName] = ValidationResults;
            }
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }
        #endregion
    }