Jai
Jai

Reputation: 8363

Cross property validation using INotifyDataErrorInfo not working

I'm trying to implement validation in my MVVM project, with validation done at Model. Below is a minimal example:

View:

Window x:Class="Test.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:Test"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:VM x:Key="VM"/>
    </Window.Resources>
    <Grid DataContext="{StaticResource VM}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Content="Number: "/>
        <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Number, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
                 ToolTip="{Binding Path=(Validation.Errors)/ErrorContent, RelativeSource={RelativeSource Mode=Self}}"/>
        <Label Grid.Row="1" Grid.Column="0" Content="Limit: "/>
        <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Limit, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
                 ToolTip="{Binding Path=(Validation.Errors)/ErrorContent, RelativeSource={RelativeSource Mode=Self}}"/>
    </Grid>
</Window>

ViewModel:

public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
    private Model _model = new Model();

    public int Number
    {
        get { return this._model.Number; }
        set
        {
            if (value != this._model.Number)
                this._model.Number = value;
        }
    }

    public int Limit
    {
        get { return this._model.Limit; }
        set
        {
            if (value != this._model.Limit)
                this._model.Limit = value;
        }
    }

    public ViewModel()
    {
        this._model.PropertyChanged += (sender, args) => this.PropertyChanged?.Invoke(sender, args);
        this._model.ErrorsChanged += (sender, args) => this.ErrorsChanged?.Invoke(sender, args);
    }

    public bool HasErrors => this._model.HasErrors;

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    public event PropertyChangedEventHandler PropertyChanged;

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

Model:

public class Model : INotifyPropertyChanged, INotifyDataErrorInfo
{
    private int _number;
    public int Number
    {
        get => _number;
        set
        {
            if (_number != value)
            {
                _number = value;
                OnPropertyChanged();
                //ValidateNumber();
            }
        }
    }

    private int _limit;
    public int Limit
    {
        get => _limit;
        set
        {
            if (_limit != value)
            {
                _limit = value;
                OnPropertyChanged();
                ValidateNumber();
            }
        }
    }

    private void ValidateNumber()
    {
        var propertyName = nameof(Number);
        if (Number > Limit)
            _errors[propertyName] = new[] { "Number cannot be larger than Limit." };
        else
        {
            if (_errors.ContainsKey(propertyName))
                _errors.Remove(propertyName);
        }

        OnErrorChanged(propertyName);
    }

    private Dictionary<string, IEnumerable<string>> _errors = new Dictionary<string, IEnumerable<string>>();

    public bool HasErrors => _errors.Values.Any(err => err != null);

    public event PropertyChangedEventHandler PropertyChanged;
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public IEnumerable GetErrors(string propertyName) => _errors.ContainsKey(propertyName) ? _errors[propertyName] : Enumerable.Empty<object>();

    private void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    private void OnErrorChanged([CallerMemberName] string propertyName = null) => ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}

From the example, the Number property is supposed to be validated whenever Limit is changed, and validation errors for Number are supposed to be shown on the View. However, the validation method is observed to execute but no validation errors are shown for the Number field on the View.

From this article, it seems like INotifyDataErrorInfo is supposed to support such cross-property validation.

Upvotes: 1

Views: 584

Answers (1)

Jai
Jai

Reputation: 8363

It seems like the binding engine expects that the sender of an INotifyDataErrorInfo.ErrorsChanged event to be the same object that the binding source resides in.

Changing

this._model.ErrorsChanged += (sender, args) => this.ErrorsChanged?.Invoke(sender, args);

To

this._model.ErrorsChanged += (sender, args) => this.ErrorsChanged?.Invoke(this, args);

fixes the issue.

Any experts who knows why this is designed this way? (I spent one whole day just to troubleshoot this)

Upvotes: 1

Related Questions