问题 强制INotifyDataErrorInfo验证


我完全按照以下链接中的描述实现了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));
}

现在这一切都运行正常,但它只会在我完成后立即生效 在我的TextBox中输入内容。我想要一些方法来按需验证,甚至没有触摸文本框,比如按一下按钮。

我已尝试为我的所有属性提升PropertyChanged,如中所述 这个 问题,但它没有检测到错误。我不知何故需要调用我的属性设置器,以便可以检测到错误。我正在寻找一个MVVM解决方案。


1244
2018-01-07 21:38


起源

你为什么不简单地调用NotifyErrorsChanged方法?这将引发ErrorsChanged事件,如果它们具有ValidatesOnNotifyDataErrors = True,则所有绑定控件都应对其作出反应。 - Stipo
我已经尝试过,它什么也没做,我假设那是因为当时_errors字典是空的。 - kskyriacou
按需验证的目的是什么?当您的任何属性发生更改时,您的模型将立即自行验证。手动验证只会产生相同的结果,因为模型已经自行验证。 - TreeTree
然后在类的构造函数中将property设置为null,因为类的初始状态无效。 - Stipo
问题是您在setter中执行验证(检查值是null还是空格)。我建议你把支票提取到一个单独的方法(比方说, ValidateFullName()),然后您将能够通过简单调用此方法重新验证该值 - 它将重新评估当前值是否为 FullName 有效,设置适当的验证信息并提高 ErrorsChanged 如有必要。 - Grx70


答案:


你使用的INotifyDataErrorInfo实现有点瑕疵恕我直言。它依赖于附加到对象的状态(列表)中保存的错误。存储状态的问题有时在移动的世界中,您没有机会在需要时更新它。这是另一个MVVM实现,它不依赖于存储状态,而是动态计算错误状态。

由于您需要将验证代码放在中央GetErrors方法中(您可以创建从此中心方法调用的每个属性验证方法),而不是在属性设置器中,因此处理的方式有所不同。

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示例方法应该可以工作(我已经测试了它,例如像_name的成员值,如果没有通过公共setter就会被更改)。


11
2018-01-11 13:20



这样做会在我的所有容器周围放置红色错误框,就像我的堆栈面板一样 这个例子。我只在文本框上使用验证,甚至不在此stackpanel中。此外,此方法会在创建模型后立即强制进行验证。我想验证 只要 当用户更改属性时 要么 一经请求。 - kskyriacou
你问的是不清楚和/或不完整的。你的问题中没有提到stackpanel。您可以按照自己的方式更改验证样式,例如: nbdtech.com/Blog/archive/2010/07/05/... - Simon Mourier
我不是要改变验证方式,我所说的就是在使用你的代码后,我的所有容器现在都用红线突出显示。 stackpanels只是一个例子。 - kskyriacou
事实容器用红线突出显示是在WPF中设计的:如果你的MVVM /数据绑定对象无效,那么它的样式使用WPF的默认无效验证样式,这就是为什么我指出了一种方法来玩这个。它实际上证明我的示例代码可以像WPF所期望的那样工作。 - Simon Mourier
我刚刚从“错误字典”方法切换到此处描述的“按需读取错误”方法。我没有上述问题。它工作得很好,使代码更清晰,并消除了保持错误字典新鲜的时间问题。使验证逻辑在绑定引擎需要时准确运行更有意义。好东西 - nmarler


最好的办法是使用中继命令界面。看看这个:

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
}

您可以在视图模型中声明此relay命令,如:

public RelayCommand SaveCommand { get; private set; }

现在,除了注册你的 SaveCommand 同 OnSave 和a CanSave 方法,因为你延伸 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));
    }

使用此设置,以及您的viewmodel是否都延伸 INotifyPropertyChanged 和 INotifyDataErrorInfo (或者从这两个扩展的基类),当你将一个按钮绑定到 SaveCommand 如上所述,如果存在验证错误,WPF框架将自动禁用它。

希望这可以帮助。


1
2018-01-08 08:49



我熟悉了 RelayCommand 而且我已经在使用类似的东西了。我的验证是在模型上完成的,所以我的viewmodel没有实现 INotifyDataErrorInfo。另外,我遇​​到的问题是我的属性设置器中的验证是在我想要它时被调用的,所以我没有看到你的答案如何解决这个问题。我将按照上面评论中提到的@ Grx70的方式行事。 - kskyriacou