强制执行 INotifyDataErrorInfo 验证

16

我已经按照以下链接中描述的方式准确实现了INotifyDataErrorInfo:

http://blog.micic.ch/net/easy-mvvm-example-with-inotifypropertychanged-and-inotifydataerrorinfo

我的模型中有一个绑定到字符串属性的TextBox

XAML

<TextBox Text="{Binding FullName,
                        ValidatesOnNotifyDataErrors=True,
                        NotifyOnValidationError=True,
                        UpdateSourceTrigger=PropertyChanged}" />

模型

private string _fullName;
public string FullName
{
    get { return _fullName; }
    set
    {
        // Set raises OnPropertyChanged
        Set(ref _fullName, value);

        if (string.IsNullOrWhiteSpace(_fullName))
            AddError(nameof(FullName), "Name required");
        else
            RemoveError(nameof(FullName));                
    }
}

INotifyDataError接口

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

public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

// get errors by property
public IEnumerable GetErrors(string propertyName)
{
    if (_errors.ContainsKey(propertyName))
        return _errors[propertyName];
    return null;
}

public bool HasErrors => _errors.Count > 0;

// object is valid
public bool IsValid => !HasErrors;

public void AddError(string propertyName, string error)
{
    // Add error to list
    _errors[propertyName] = new List<string>() { error };
    NotifyErrorsChanged(propertyName);
}

public void RemoveError(string propertyName)
{
    // remove error
    if (_errors.ContainsKey(propertyName))
        _errors.Remove(propertyName);
    NotifyErrorsChanged(propertyName);
}

public void NotifyErrorsChanged(string propertyName)
{
    // Notify
    if (ErrorsChanged != null)
       ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}

现在一切都运作良好,但只有在我在文本框中输入内容时才进行验证。我希望有一种方法可以按需验证,而无需触摸文本框,例如在点击按钮时进行验证。

我已经尝试了为我的所有属性引发 PropertyChanged 的方法,如问题所述,但它无法检测到错误。我需要某种方式来调用属性 setter 以便可以检测到错误。我正在寻找一个MVVM解决方案。


为什么不直接调用NotifyErrorsChanged方法呢?这将引发ErrorsChanged事件,如果绑定的控件具有ValidatesOnNotifyDataErrors=True,则所有控件都应该对其做出反应。 - Stipo
我已经尝试过了,但没有任何作用,我认为这是因为_errors字典在那个时候是空的。 - kkyr
什么是按需验证的目的?当模型的任何属性被更改时,您的模型将立即进行自我验证。手动验证只会产生相同的结果,因为模型已经通过自身进行了验证。 - TreeTree
1
然后在你的类的构造函数中将属性设置为null,因为你的类的初始状态是无效的。 - Stipo
2
问题在于您在setter中执行验证(检查值是否为null或空格)。我建议您将检查提取到单独的方法中(例如,ValidateFullName()),然后您就可以通过对该方法的简单调用重新验证该值 - 它将重新评估FullName的当前值是否有效,设置适当的验证信息并在必要时引发ErrorsChanged - Grx70
显示剩余7条评论
2个回答

24

在我看来,您所使用的INotifyDataErrorInfo实现有些缺陷。它依赖于附加到对象上的状态(一个列表)中保留的错误。存储状态的问题是,在一个不断变化的世界中,有时您没有机会在想要的时候更新它。这里有另一种MVVM实现,它不依赖于存储状态,而是实时计算错误状态。

处理方式有些不同,需要将验证代码放在中央的GetErrors方法中(您可以从该中央方法调用针对每个属性的验证方法),而不是属性setter中。

public class ModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
{
    public event PropertyChangedEventHandler PropertyChanged;
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public bool HasErrors
    {
        get
        {
            return GetErrors(null).OfType<object>().Any();
        }
    }

    public virtual void ForceValidation()
    {
        OnPropertyChanged(null);
    }

    public virtual IEnumerable GetErrors([CallerMemberName] string propertyName = null)
    {
        return Enumerable.Empty<object>();
    }

    protected void OnErrorsChanged([CallerMemberName] string propertyName = null)
    {
        OnErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }

    protected virtual void OnErrorsChanged(object sender, DataErrorsChangedEventArgs e)
    {
        var handler = ErrorsChanged;
        if (handler != null)
        {
            handler(sender, e);
        }
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        OnPropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(sender, e);
        }
    }
}

这里有两个示例类,演示如何使用它:

public class Customer : ModelBase
{
    private string _name;

    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged();
            }
        }
    }

    public override IEnumerable GetErrors([CallerMemberName] string propertyName = null)
    {
        if (string.IsNullOrEmpty(propertyName) || propertyName == nameof(Name))
        {
            if (string.IsNullOrWhiteSpace(_name))
                yield return "Name cannot be empty.";
        }
    }
}

public class CustomerWithAge : Customer
{
    private int _age;
    public int Age
    {
        get
        {
            return _age;
        }
        set
        {
            if (_age != value)
            {
                _age = value;
                OnPropertyChanged();
            }
        }
    }

    public override IEnumerable GetErrors([CallerMemberName] string propertyName = null)
    {
        foreach (var obj in base.GetErrors(propertyName))
        {
            yield return obj;
        }

        if (string.IsNullOrEmpty(propertyName) || propertyName == nameof(Age))
        {
            if (_age <= 0)
                yield return "Age is invalid.";
        }
    }
}

只需使用这样简单的 XAML 代码,它就像魔法一样运行:

<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}" />

(如果您不使用UpdateSourceTrigger,则它仅在失去焦点时才起作用。)

使用这个MVVM基类,您不必强制进行任何验证。但是,如果您需要它,我已经在ModelBase中添加了一个ForceValidation示例方法,它应该可以工作(例如,我已经使用过_member value_测试了它,而这个值已经被更改,没有通过公共设置器)。


你所提出的问题不够清晰或者不完整。在你的问题中没有提到StackPanel。你可以按照自己的需求更改验证样式,例如:http://www.nbdtech.com/Blog/archive/2010/07/05/wpf-adorners-part-3-ndash-adorners-and-validation.aspx。 - Simon Mourier
在WPF中,容器被红色线条突出显示是有意设计的:如果您的MVVM/数据绑定对象无效,则其样式将使用WPF的默认无效验证样式,这就是为什么我指向了您玩弄它的方法。实际上,这证明了我的示例代码按照WPF的预期工作。 - Simon Mourier
实际上,绑定到对象的特定控件应该被红线突出显示,而不是与该控件毫无关系的容器。 - kkyr
1
我刚刚从“错误字典”方法转换到了这里描述的“按需获取错误”方法。我没有遇到上述描述的任何问题。它完美地工作,生成的代码更加清晰,并消除了保持错误字典更新的时间难题。对于验证逻辑来说,让它在绑定引擎需要时运行更有意义。很棒的东西。 - nmarler
1
@SimonMourier,在上面的例子中,您甚至没有触发ErrorsChanged事件一次。我确信这个事件对于通知视图中的绑定“现在请调用GetErrors(),因为有些东西已经改变”是非常重要的。您能否编辑您的回答,解释一下这一点? - Sinatr
显示剩余5条评论

1

你最好使用一个继电器命令接口。看看这个:

public class RelayCommand : ICommand
{
    Action _TargetExecuteMethod;
    Func<bool> _TargetCanExecuteMethod;

    public RelayCommand(Action executeMethod)
    {
        _TargetExecuteMethod = executeMethod;
    }

    public RelayCommand(Action executeMethod, Func<bool> canExecuteMethod)
    {
        _TargetExecuteMethod = executeMethod;
        _TargetCanExecuteMethod = canExecuteMethod;
    }

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged(this, EventArgs.Empty);
    }
    #region ICommand Members

    bool ICommand.CanExecute(object parameter)
    {
        if (_TargetCanExecuteMethod != null)
        {
            return _TargetCanExecuteMethod();
        }
        if (_TargetExecuteMethod != null)
        {
            return true;
        }
        return false;
    }

    public event EventHandler CanExecuteChanged = delegate { };

    void ICommand.Execute(object parameter)
    {
        if (_TargetExecuteMethod != null)
        {
            _TargetExecuteMethod();
        }
    }
    #endregion
}

你需要在视图模型中声明这个继电器命令,例如:
public RelayCommand SaveCommand { get; private set; }

现在,除了将您的SaveCommand注册到OnSaveCanSave方法之外,由于您扩展自INotifyDataErrorInfo,因此您还可以在构造函数中注册到ErrorsChanged
public YourViewModel()
{
    SaveCommand = new RelayCommand(OnSave, CanSave);
    ErrorsChanged += RaiseCanExecuteChanged;
}

而且你需要以下方法:

private void RaiseCanExecuteChanged(object sender, EventArgs e)
{
        SaveCommand.RaiseCanExecuteChanged();
}

public bool CanSave()
{
    return !this.HasErrors;
}

private void OnSave()
{
    //Your save logic here.
}

此外,每次调用PropertyChanged之后,您都可以调用此验证方法:
    private void ValidateProperty<T>(string propertyName, T value)
    {
        var results = new List<ValidationResult>();
        ValidationContext context = new ValidationContext(this);
        context.MemberName = propertyName;
        Validator.TryValidateProperty(value, context, results);

        if (results.Any())
        {
            _errors[propertyName] = results.Select(c => c.ErrorMessage).ToList();
        }
        else
        {
            _errors.Remove(propertyName);
        }

        ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }

使用此设置,如果您的视图模型同时扩展自INotifyPropertyChangedINotifyDataErrorInfo(或从扩展这两个的基类),当您将按钮绑定到上面的SaveCommand时,WPF框架会自动禁用它,如果存在验证错误。希望这可以帮助您。

我熟悉RelayCommand并且已经在使用类似的东西。我的验证是在模型上完成的,因此我的视图模型没有实现INotifyDataErrorInfo。另外,我遇到的问题是我的验证是在属性设置器中完成的,但当我想要调用它时,它并没有被调用,所以我不明白你的答案如何解决这个问题。我将采用@Grx70在他上面的评论中提到的方法。 - kkyr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接