3

我正在尝试实现 INotifyDataErrorInfo 并且我的模型有一些自定义类型,这些类型需要根据它们的使用进行不同的验证。我不确定如何实施此验证。

我尝试在下面创建一个简单的示例,以显示我要完成的工作。我不是在寻找有关更改模型的建议,因为我的实际模型要复杂得多。

简单示例

我的示例模型适用于将有演示者和嘉宾的媒体活动。安排媒体活动时,用户将输入姓名、最少和最多演示者以及最少和最多来宾。通常,媒体必须至少有1名主持人且不超过5名,并且必须至少有10名嘉宾且不超过50名。

我有以下课程,取自在线示例,用作我的模型类的基础。

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace NotifyDataErrorInfo
{
    public class ValidatableModel : INotifyDataErrorInfo, INotifyPropertyChanged
    {
        public ConcurrentDictionary<string, List<string>> _errors = new ConcurrentDictionary<string, List<string>>();

        public event PropertyChangedEventHandler PropertyChanged;

        public void RaisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;

            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }

            ValidateAsync();
        }

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public void OnErrorsChanged(string propertyName)
        {
            var handler = ErrorsChanged;

            if (handler != null)
            {
                handler(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        public IEnumerable GetErrors(string propertyName)
        {
            if (propertyName == null) return null;

            List<string> errorsForName;
            _errors.TryGetValue(propertyName, out errorsForName);

            return errorsForName;
        }

        public bool HasErrors
        {
            get
            {
                return _errors.Any(kv => kv.Value != null && kv.Value.Count > 0);
            }
        }

        public Task ValidateAsync()
        {
            return Task.Run(() => Validate());
        }

        private object _lock = new object();
        public void Validate()
        {
            lock (_lock)
            {
                var validationContext = new ValidationContext(this, null, null);
                var validationResults = new List<ValidationResult>();

                Validator.TryValidateObject(this, validationContext, validationResults, true);

                foreach (var kv in _errors.ToList())
                {
                    if (validationResults.All(r => r.MemberNames.All(m => m != kv.Key)))
                    {
                        List<string> outLi;
                        _errors.TryRemove(kv.Key, out outLi);
                        OnErrorsChanged(kv.Key);
                    }
                }

                var q = from r in validationResults
                        from m in r.MemberNames
                        group r by m into g
                        select g;

                foreach (var prop in q)
                {
                    var messages = prop.Select(r => r.ErrorMessage).ToList();

                    if (_errors.ContainsKey(prop.Key))
                    {
                        List<string> outLi;
                        _errors.TryRemove(prop.Key, out outLi);
                    }

                    _errors.TryAdd(prop.Key, messages);
                    OnErrorsChanged(prop.Key);
                }
            }
        }
    }
}

因为我在两个地方使用了最小值和最大值,所以我创建了以下类来存储最小值和最大值。这是我的例子中过于简单的部分,但应该明白这一点。

namespace NotifyDataErrorInfo
{
    public class MinMaxValues : ValidatableModel
    {
        private int min;
        private int max;

        public int Min
        {
            get
            {
                return min;
            }

            set
            {
                if (!min.Equals(value))
                {
                    min = value;
                    RaisePropertyChanged(nameof(Min));
                    OnErrorsChanged(nameof(Min));
                }
            }
        }

        public int Max
        {
            get
            {
                return max;
            }

            set
            {
                if (!max.Equals(value))
                {
                    max = value;
                    RaisePropertyChanged(nameof(Max));
                    OnErrorsChanged(nameof(Max));
                }
            }
        }

        public MinMaxValues()
        {
            Min = 0;
            Max = 0;
        }
    }
}

这是我的 MediaEvent 类,您可以看到它为 MinMaxPresenters 和 MinMaxGuests 使用了 MinMaxValues 类。

using System.ComponentModel.DataAnnotations;

namespace NotifyDataErrorInfo
{
    public class MediaEvent: ValidatableModel
    {
        private string name;
        private MinMaxValues minMaxPresenters;
        private MinMaxValues minMaxGuests;

        public MediaEvent()
        {
            name = string.Empty;
            minMaxPresenters = new MinMaxValues();
            minMaxGuests = new MinMaxValues();

            this.Validate();
            this.minMaxPresenters.Validate();
            this.minMaxGuests.Validate();            }
        }

        [Required]
        [StringLength(10, MinimumLength = 5)]
        public string Name
        {
            get
            {
                return name;
            }

            set
            {
                if(!name.Equals(value))
                {
                    name = value;
                    RaisePropertyChanged(nameof(Name));
                }
            }
        }

        public MinMaxValues MinMaxPresenters
        {
            get
            {
                return minMaxPresenters;
            }

            set
            {
                if (!minMaxPresenters.Equals(value))
                {
                    minMaxPresenters = value;
                    RaisePropertyChanged(nameof(MinMaxPresenters));
                }
            }
        }

        public MinMaxValues MinMaxGuests
        {
            get
            {
                return minMaxGuests;
            }

            set
            {
                if (!minMaxGuests.Equals(value))
                {
                    minMaxGuests = value;
                    RaisePropertyChanged(nameof(MinMaxGuests));
                }
            }
        }
    }
}

这是我的 MainWindow 的 XAML

<Window 
    x:Class="NotifyDataErrorInfo.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:NotifyDataErrorInfo"
    mc:Ignorable="d"
    Title="MainWindow"
    Height="209" Width="525"
    ResizeMode="NoResize">

    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>

    <Grid Margin="5">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="42*"/>
            <RowDefinition Height="43*"/>
            <RowDefinition Height="42*"/>
            <RowDefinition Height="43*"/>
        </Grid.RowDefinitions>

        <Label
            Content="Meeting Name: "
            Grid.Row="0" Grid.Column="0"/>

        <TextBox
            Text="{Binding Name}"
            Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="3"/>

        <Label
            Content="Min Presenters: "
            Grid.Row="1" Grid.Column="0"/>

        <TextBox
            Text="{Binding MinMaxPresenters.Min}"
            Grid.Row="1" Grid.Column="1"/>

        <Label
            Content="Max Presenters: "
            Grid.Row="1" Grid.Column="2"/>

        <TextBox
            Text="{Binding MinMaxPresenters.Max}"
            Grid.Row="1" Grid.Column="3"/>

        <Label
            Content="Min Guests: "
            Grid.Row="2" Grid.Column="0"/>

        <TextBox
            Text="{Binding MinMaxGuests.Min}"
            Grid.Row="2" Grid.Column="1"/>

        <Label
            Content="Max Guests: "
            Grid.Row="2" Grid.Column="2"/>

        <TextBox
            Text="{Binding MinMaxGuests.Max}"
            Grid.Row="2" Grid.Column="3"/>

        <Button
            x:Name="TestButton"
            Content="TEST"
            Click="TestButton_Click"
            Grid.Row="3" Grid.Column="3"/>
    </Grid>
</Window>

在 App.xaml.cs 中使用

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    var mainWindow = new MainWindow();
    var mediaEvent = new MediaEvent();

    mainWindow.DataContext = mediaEvent;
    mainWindow.Show();
}

在 MediaEvent 类中,我使用 [Required] 和 [StringLength(10, MinimumLength = 5)] 属性修饰了 Name 属性。这些按预期工作。当输入的名称短于 5 个字符或长于 10 个字符时,我可以在名称文本框周围看到一个红色框,表明存在错误。

什么我想不通

现在我不确定如何验证 MinMaxPresenters.Min、MinMaxPresenters.Max、MinMaxGuests.Min 和 MinMaxGuests.Max

如果我用 [Range(1, 5)] 之类的东西装饰 MinMaxValues 类中的 Min 属性,我可以确认验证正在发生并且 UI 会相应地更新。

问题是验证适用于演示者和来宾的最小值。我需要为演示者和来宾验证不同的最小值。

我试过的

在 MediaEvent 中,我加入了 minMaxPresenters 的 PropertyChanged 事件。在那个事件处理程序中,我尝试根据演示者的规则(范围 = 1 到 5)验证 Min 和 Max 值。如果验证失败,我尝试添加到 _errors 集合。

在我的构造函数中,我添加了

minMaxPresenters.PropertyChanged += MinMaxPresenters_PropertyChanged;

然后创建了以下

private void MinMaxPresenters_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    if (e.PropertyName == "Min")
    {
        if (minMaxPresenters.Min < 1)
        {
            _errors.TryAdd("MinMaxPresenters.Min", new List<string> { "A media event requires at least 1 presenter" });
            OnErrorsChanged("MinMaxPresenters.Min");
        }
    }
    else if (e.PropertyName == "Max")
    {
        if (minMaxPresenters.Max <= minMaxPresenters.Min)
        {
            _errors.TryAdd("MinMaxPresenters.Max", new List<string> { "The max presenters must be greater than the min" });
            OnErrorsChanged("MinMaxPresenters.Max");
        }
        else if (minMaxPresenters.Max > 5)
        {
            _errors.TryAdd("MinMaxPresenters.Max", new List<string> { "A media event can't have more than 5 presenters" });
            OnErrorsChanged("MinMaxPresenters.Max");
        }
    }
}

当我输入超出演示者范围的最小值和最大值时,我可以看到我的错误被添加到模型中的 _errors 集合中,但我的视图并未表明存在任何错误。

我接近了吗?我对这一切都错了吗?

我还需要根据其他属性值验证值,因此需要进行自定义验证并通过代码添加错误。一个例子是验证上面的最大值。演示者的最大值需要小于 5,但也必须大于为最小值输入的值。

编辑

您可以忽略 MainWindow 中的按钮。只需单击并中断后面的代码,这样我就可以看到集合中有什么错误。

此外,如果有人评论公开 _errors,这只是尝试添加错误的快速方法。理想情况下,我会创建 AddError 和 RemoveError 方法。

4

1 回答 1

1

你的问题在这里

_errors.TryAdd("MinMaxPresenters.Min", new List<string> 
      { "A media event requires at least 1 presenter" });

您正在将错误添加到父对象,但 WPF 绑定会在属性链中的最后一个对象上查找错误。验证和 WPF 是一个令人头疼的问题。使用您的模型,您应该这样做

MinMaxPresenters._errors.TryAdd("Min", new List<string>
      { "A media event requires at least 1 presenter" });

然后错误将被 UI 拾取。

在我开发的框架中,我能够执行您最初尝试的操作,但我会解析错误字符串“MinMaxPresenters.Min”,然后查找名称为“MinMaxPresenters”的属性并自动将验证错误转发给子对象。

我的AddErrors实现是

    public void AddErrors(string path, IEnumerable<Exception> errors, bool nest = true)
    {
        var exceptions = errors as IList<Exception> ?? errors.ToList();

        var nestedPath = path.Split('.').ToList();
        if (nestedPath.Count > 1 && nest)
        {
            var tail = string.Join(".", nestedPath.Skip(1));
            // Try and get a child property as Maybe<INotifyDataExceptionInfo> 
            // and if it exists pass the error
            // downwards after stripping off the first part of
            // the path.
            var notifyDataExceptionInfo = this.TryGet<INotifyDataExceptionInfo,INotifyDataExceptionInfo>(nestedPath[0]);
            if(notifyDataExceptionInfo.IsSome)
                notifyDataExceptionInfo.Value.AddErrors(tail, exceptions);
        }

        _Errors.RemoveKey(path);
        foreach (var error in exceptions)
        {
            _Errors.Add(path, error);
        }

        RaiseErrorEvents(path);

    }

** TryGet 是一种通过引用获取属性值的方法

** 完整的实现可以在这个位置找到。

于 2017-01-25T06:36:34.967 回答