使用MVVM进行适当的验证

61

警告:非常长且详细的文章。

好的,当使用MVVM时,在WPF中进行验证。我现在已经阅读了很多内容,查看了许多SO问题,并尝试了许多方法,但是在某些时候,一切都感觉有点不可靠,我真的不确定如何以正确的方式™进行操作。

理想情况下,我希望所有验证都在视图模型中使用IDataErrorInfo进行处理;所以我就这样做了。然而,有不同的方面使得这个解决方案对于整个验证主题来说并不完全。

情况

让我们来看看以下简单的表单。正如您所见,它并不花哨。我们只有两个文本框,每个文本框绑定到视图模型中的一个stringint属性。此外,我们还有一个按钮,该按钮绑定到一个ICommand

Simple form with only a string and integer input

所以对于验证,我们现在有两个选择:
  1. 每当文本框的值更改时自动运行验证。这样,当用户输入无效内容时,他会立即得到响应。
    • 我们可以进一步采取措施,在出现任何错误时禁用按钮。
  2. 或者我们只在按下按钮时显式地运行验证,然后显示所有适用的错误。显然,我们不能在此处在错误时禁用按钮。
理想情况下,我希望实现第一种选择。对于已激活ValidatesOnDataErrors的普通数据绑定,这是默认行为。因此,当文本更改时,绑定更新源并触发该属性的IDataErrorInfo验证;错误将返回视图。目前为止还好。

视图模型中的验证状态

有趣的部分在于让视图模型或者说在这个例子中的按钮知道是否存在任何错误。使用 IDataErrorInfo 的方式主要是将错误报告回传给视图,因此视图可以轻松看到是否存在任何错误,并显示它们,甚至可以使用 Validation.Errors 显示注释。此外,验证总是查看单个属性。
因此,使视图模型知道是否存在任何错误或验证成功是很棘手的。常见的解决方案是简单地触发视图模型本身所有属性的 IDataErrorInfo 验证。通常使用单独的 IsValid 属性来完成这项工作。好处是这也可以轻松用于禁用命令。缺点是这可能会过于频繁地对所有属性运行验证,但大多数验证应该足够简单,不会影响性能。另一个解决方案是记住使用验证产生错误的属性,并仅检查那些属性,但这似乎有些过于复杂和不必要。
这意味着这可能很好地发挥作用。 IDataErrorInfo 为所有属性提供验证,我们可以在视图模型本身中使用该接口来运行整个对象的验证。引入问题:视图模型使用其属性的实际类型。因此,在我们的示例中,整数属性是一个实际的int。然而,视图中使用的文本框本质上仅支持文本。因此,当绑定到视图模型中的int时,数据绑定引擎将自动执行类型转换,或者至少会尝试。如果您可以在专门用于数字的文本框中输入文本,则很有可能不总是存在有效数字:因此,数据绑定引擎将无法进行转换并抛出FormatException异常。

Data binding engine throws an exception and that’s displayed in the view

在视图方面,我们可以轻松地看到。绑定引擎中的异常会被WPF自动捕获并显示为错误 - 甚至不需要启用Binding.ValidatesOnExceptions,这对于在setter中抛出的异常是必需的。但是,错误消息确实具有通用文本,因此可能会出现问题。我通过使用Binding.UpdateSourceExceptionFilter处理程序解决了这个问题,检查被抛出的异常并查看源属性,然后生成一个不太通用的错误消息。所有这些都封装在我的自己的Binding标记扩展中,所以我可以拥有所有我需要的默认值。
所以视图很好。用户犯了一个错误,看到一些错误反馈并可以纠正它。然而,视图模型则丢失了。由于绑定引擎抛出了异常,因此源永远没有更新。因此,视图模型仍然处于旧值状态,这与向用户显示的内容不符,并且IDataErrorInfo验证显然不适用。

更糟的是,视图模型无法很好地知道这一点。至少我还没有找到一个好的解决方案。可能的做法是让视图向视图模型报告错误。这可以通过将Validation.HasError属性数据绑定回视图模型(直接不可能)来完成,以便视图模型可以首先检查视图的状态。

另一种选择是将在Binding.UpdateSourceExceptionFilter中处理的异常转发给视图模型,以便它也能得到通知。视图模型甚至可以为绑定提供一些接口来报告这些事情,从而允许自定义错误消息,而不是只有通用的按类型错误消息。但这会在视图和视图模型之间创建更强的耦合,而我通常是要避免的。

另一个“解决方案”是摆脱所有键入的属性,使用普通的 string 属性,并在视图模型中进行转换。这显然将所有验证移动到视图模型中,但也意味着数据绑定引擎通常会处理的事情会产生令人难以置信的重复。此外,它还将改变视图模型的语义。对我来说,视图是为视图模型构建的,而不是反过来——当然,视图模型的设计取决于我们想象视图要做什么,但仍然有一般的自由来控制视图如何完成任务。因此,视图模型定义了一个 int 属性,因为有一个数字;现在,视图可以使用文本框(允许所有这些问题),或者使用本地与数字一起工作的东西。因此,不,将属性类型更改为 string 对我来说不是一个选项。
最后,这是视图的问题。视图(及其数据绑定引擎)负责为视图模型提供适当的值以便处理。但在这种情况下,似乎没有好的方法告诉视图模型应该使旧的属性值无效。

BindingGroups

Binding groups是我尝试解决这个问题的一种方式。Binding groups具有将所有验证(包括IDataErrorInfo和抛出的异常)分组的能力。如果在视图模型中可用,它们甚至可以使用CommitEdit来检查所有这些验证源的验证状态。

默认情况下,绑定组实现了上述第二种选择。它们使绑定显式更新,从而添加了一个额外的未提交状态。因此,当单击按钮时,命令可以提交这些更改,触发源更新和所有验证,并获得一个成功的单一结果。因此,该命令的操作可能是:

 if (bindingGroup.CommitEdit())
     SaveEverything();
CommitEdit只有在所有验证都成功时才会返回true。它将考虑IDataErrorInfo并检查绑定异常。这似乎是选择2的完美解决方案。唯一有点麻烦的是管理带有绑定的绑定组,但我已经构建了一个大部分处理此事的工具(related)。
如果绑定中存在绑定组,则该绑定将默认为显式的UpdateSourceTrigger。要使用绑定组实现上面的选择1,我们基本上必须更改触发器。由于我已经有一个自定义的绑定扩展,所以这是相当简单的,我只需将其设置为LostFocus即可。
现在,只要文本字段更改,绑定仍将更新。如果源可以更新(绑定引擎不抛出异常),则 IDataErrorInfo 将像往常一样运行。如果无法更新,则视图仍能看到它。如果我们点击按钮,底层命令可以调用 CommitEdit(尽管不需要提交任何内容)并获取总验证结果以查看是否可以继续。
这种方式可能无法轻松地禁用按钮,至少不能从视图模型中禁用。反复检查验证状态并不是一个好主意,而且当绑定引擎抛出异常时视图模型也没有通知(此时应该禁用按钮)——或者当它消失时再次启用按钮。我们仍然可以使用 Validation.HasError 在视图中添加触发器来禁用按钮,因此这并非不可能。
解决方案呢?
总的来说,这似乎是完美的解决方案。但我的问题是什么呢?老实说,我不是很确定。绑定组是一个复杂的东西,通常在较小的组中使用,可能在单个视图中有多个绑定组。为了确保我的验证,使用一个大的绑定组来覆盖整个视图,感觉就像滥用了它。我一直在想,肯定有更好的方法来解决这个情况,因为我肯定不是唯一遇到这些问题的人。到目前为止,我还没有看到很多人在MVVM中使用绑定组进行验证,所以这感觉很奇怪。

那么,在WPF中使用MVVM进行验证并能够检查绑定引擎异常的正确方式是什么?


我的解决方案(/hack)

首先,感谢您的输入!正如我上面所写的那样,我已经在使用 IDataErrorInfo 来进行数据验证,我个人认为这是最舒适的工具来完成验证工作。我正在使用类似于 Sheridan 在下面回答中建议的实用程序,因此维护也很好。

最终,我的问题归结为绑定异常问题,即视图模型仅在发生异常时才知道它。虽然我可以通过如上所述的绑定组来处理这个问题,但我还是决定不这样做,因为我对此感到不太舒服。那么我做了什么呢?

正如我上面提到的那样,我通过监听绑定的 UpdateSourceExceptionFilter 来在视图端检测绑定异常。在那里,我可以从绑定表达式的 DataItem 中获取到视图模型的引用。然后,我有一个接口 IReceivesBindingErrorInformation,它将视图模型注册为可能接收有关绑定错误信息的接收器。然后我使用它来将绑定路径和异常传递给视图模型:

object OnUpdateSourceExceptionFilter(object bindExpression, Exception exception)
{
    BindingExpression expr = (bindExpression as BindingExpression);
    if (expr.DataItem is IReceivesBindingErrorInformation)
    {
        ((IReceivesBindingErrorInformation)expr.DataItem).ReceiveBindingErrorInformation(expr.ParentBinding.Path.Path, exception);
    }

    // check for FormatException and produce a nicer error
    // ...
 }

在视图模型中,当我收到关于路径绑定表达式的通知时,我会记住它。
HashSet<string> bindingErrors = new HashSet<string>();
void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception)
{
    bindingErrors.Add(path);
}

每当 IDataErrorInfo 重新验证一个属性时,我知道绑定起作用了,并且可以从哈希集中清除该属性。
在视图模型中,我可以检查哈希集是否包含任何项,并中止需要完全验证数据的任何操作。由于视图与视图模型之间的耦合,这可能不是最好的解决方案,但至少使用该接口会稍微减少一些问题。

18
如果您已经看到这里了,非常感谢您的阅读! - poke
这就是为什么我不喜欢WPF内置的验证,而是基于附加属性和委托在视图和ViewModel方面创建了自己的验证。 - Federico Berasategui
5
顺便说一下,当涉及到格式验证(比如在数字文本框中输入'a')时,我更倾向于使用MaskedTextBox或类似的东西,甚至不让用户这样做。 - Federico Berasategui
你如何将 OnUpdateSourceExceptionFilter 附加到每个绑定表达式?希望不是手动的。 - Jordan
@Jordan 我创建了自己的 Binding 子类型,然后在所有地方都使用它,而不是默认的那个。例如,使用 XML 命名空间 f,绑定将如下所示:{f:Binding MyPath} - poke
显示剩余2条评论
5个回答

17

警告:答案有点长

我使用 IDataErrorInfo 接口进行验证,但我已经根据自己的需求进行了定制。我认为你会发现它也可以解决你的一些问题。与你的问题不同之处在于,我在我的基础数据类型类中实现它。

正如你所指出的,这个接口只处理一个属性,但显然在今天这个时代,这是不够的。因此,我只是添加了一个集合属性来代替:

protected ObservableCollection<string> errors = new ObservableCollection<string>();

public virtual ObservableCollection<string> Errors
{
    get { return errors; }
}
为了解决您无法显示外部错误的问题(在您的情况下是来自视图,但在我的情况下是来自视图模型),我只需添加另一个集合属性:
protected ObservableCollection<string> externalErrors = new ObservableCollection<string>();

public ObservableCollection<string> ExternalErrors
{
    get { return externalErrors; }
}

我有一个HasError属性,它查看我的集合:

public virtual bool HasError
{
    get { return Errors != null && Errors.Count > 0; }
}
这使我能够使用自定义的BoolToVisibilityConverter将其绑定到Grid.Visibility,例如,在Grid中显示包含集合控件的错误时。 它还允许我将Brush更改为Red以突出显示错误(使用另一个Converter),但我想你已经明白了。
然后,在每种数据类型或模型类中,我重写Errors属性并实现Item索引器(在此示例中进行了简化)。
public override ObservableCollection<string> Errors
{
    get
    {
        errors = new ObservableCollection<string>();
        errors.AddUniqueIfNotEmpty(this["Name"]);
        errors.AddUniqueIfNotEmpty(this["EmailAddresses"]);
        errors.AddUniqueIfNotEmpty(this["SomeOtherProperty"]);
        errors.AddRange(ExternalErrors);
        return errors;
    }
}

public override string this[string propertyName]
{
    get
    {
        string error = string.Empty;
        if (propertyName == "Name" && Name.IsNullOrEmpty()) error = "You must enter the Name field.";
        else if (propertyName == "EmailAddresses" && EmailAddresses.Count == 0) error = "You must enter at least one e-mail address into the Email address(es) field.";
        else if (propertyName == "SomeOtherProperty" && SomeOtherProperty.IsNullOrEmpty()) error = "You must enter the SomeOtherProperty field.";
        return error;
    }
}

AddUniqueIfNotEmpty方法是一个自定义的扩展方法,它“说到做到”。请注意,它会依次调用我要验证的每个属性并从中编译一个集合,忽略重复的错误。

使用ExternalErrors集合,我可以验证在数据类中无法验证的内容:

private void ValidateUniqueName(Genre genre)
{
    string errorMessage = "The genre name must be unique";
    if (!IsGenreNameUnique(genre))
    {
        if (!genre.ExternalErrors.Contains(errorMessage)) genre.ExternalErrors.Add(errorMessage);
    }
    else genre.ExternalErrors.Remove(errorMessage);
}

针对用户在int字段中输入字母的情况,我倾向于使用自定义的IsNumeric AttachedProperty来限制TextBox的输入,以避免此类错误的发生。总的来说,我对WPF中的验证能力非常满意,一点都不觉得缺少。

最后提醒一下,现在有一个INotifyDataErrorInfo接口,其中包含了一些新增的功能。您可以从MSDN上的INotifyDataErrorInfo Interface页面了解更多信息。


更新>>>

是的,ExternalErrors属性只允许我添加与数据对象相关的错误...抱歉,我的示例不完整...如果我向您展示IsGenreNameUnique方法,您将会看到它使用Linq在集合中的所有Genre数据项上进行操作,以确定这个对象的名称是否唯一:

private bool IsGenreNameUnique(Genre genre)
{
    return Genres.Where(d => d.Name != string.Empty && d.Name == genre.Name).Count() == 1;
}

关于你的 int/string 问题,我只能想到一种情况会在你的数据类中导致出现那些错误,就是你将所有属性声明为object,但这样你需要进行大量类型转换。也许你可以像这样复制一下你的属性:

public object FooObject { get; set; } // Implement INotifyPropertyChanged

public int Foo
{
    get { return FooObject.GetType() == typeof(int) ? int.Parse(FooObject) : -1; }
}

如果在代码中使用了 Foo,而在 Binding 中使用了 FooObject,那么你可以这样做:

然后如果代码中使用了 Foo 而绑定中使用了 FooObject,你可以这样做:
public override string this[string propertyName]
{
    get
    {
        string error = string.Empty;
        if (propertyName == "FooObject" && FooObject.GetType() != typeof(int)) 
            error = "Please enter a whole number for the Foo field.";
        ...
        return error;
    }
}
那样你就能够满足你的需求,但是你会需要添加很多额外的代码。

1
+1 这很接近我使用的方式,而且相对来说不会很痛苦。但是,我真的建议使用一个基类,使用例如Dictionary<string,List<Func<bool>> 实现,即对于每个属性都有多个“验证函数”被调用。为了让所有东西都正常工作,您只需从基类派生并调用一个流畅的方法this.AddValidationFor(m => m.Name ).When(() => Name.IsNullOrEmpty()).ErrorIs(“您必须输入名称字段。”); - stijn
@stijn,你说这个挺有意思的,因为我实际上使用了一个扩展类来帮助我的验证,并且为我处理了所有标准验证错误条件和错误消息,所以我要编写的代码*少了很多。我在此答案中省略了它,因为已经够长了。不过,我确实喜欢你的想法,可能会进一步调查...谢谢。 - Sheridan
谢谢你的回答。我确实使用了一些简化的方法来管理我的IDataErrorInfo验证,它们也被实现在实际的数据对象中(虽然是委托)。我不确定我是否正确理解了你的“ExternalErrors”。这些是数据对象的错误,受到一些不包含在对象本身中的值的影响吗?关于int/string问题,我想你是对的,尽管这仍然不能真正满足我。我想我最终会使用某种类型的hack来报告绑定错误。 - poke
谢谢你的编辑。如上所述,我不想使用无类型属性。我现在已经决定了一个解决方案,并在我的问题底部进一步解释了。尽管如此,我仍然会接受你的答案,因为那个见解非常有用,非常感谢! - poke

3

在我看来,问题在于验证过程发生在太多的地方。我也希望将所有的验证逻辑写在ViewModel中,但是那些数字绑定让我的ViewModel变得混乱。

我通过创建一个永远不会失败的绑定来解决这个问题。显然,如果一个绑定总是成功的话,那么类型本身必须优雅地处理错误条件。

可失败值类型

我首先创建了一个通用类型,它可以优雅地支持失败的转换:

public struct Failable<T>
{
    public T Value { get; private set; }
    public string Text { get; private set; }
    public bool IsValid { get; private set; }

    public Failable(T value)
    {
        Value = value;

        try
        {
            var converter = TypeDescriptor.GetConverter(typeof(T));
            Text = converter.ConvertToString(value);
            IsValid = true;
        }
        catch
        {
            Text = String.Empty;
            IsValid = false;
        }
    }

    public Failable(string text)
    {
        Text = text;

        try
        {
            var converter = TypeDescriptor.GetConverter(typeof(T));
            Value = (T)converter.ConvertFromString(text);
            IsValid = true;
        }
        catch
        {
            Value = default(T);
            IsValid = false;
        }
    }
}

请注意,即使由于无效的输入字符串(第二个构造函数)而导致类型初始化失败,它也会悄悄地存储无效状态以及无效文本。这是为了支持绑定的回路即使输入错误也可以支持

通用值转换器

可以使用上述类型编写通用值转换器。
public class StringToFailableConverter<T> : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value.GetType() != typeof(Failable<T>))
            throw new InvalidOperationException("Invalid value type.");

        if (targetType != typeof(string))
            throw new InvalidOperationException("Invalid target type.");

        var rawValue = (Failable<T>)value;
        return rawValue.Text;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value.GetType() != typeof(string))
            throw new InvalidOperationException("Invalid value type.");

        if (targetType != typeof(Failable<T>))
            throw new InvalidOperationException("Invalid target type.");

        return new Failable<T>(value as string);
    }
}

XAML便捷转换器

在XAML中创建和使用泛型实例很麻烦,因此让我们创建常用转换器的静态实例:

public static class Failable
{
    public static StringToFailableConverter<Int32> Int32Converter { get; private set; }
    public static StringToFailableConverter<double> DoubleConverter { get; private set; }

    static Failable()
    {
        Int32Converter = new StringToFailableConverter<Int32>();
        DoubleConverter = new StringToFailableConverter<Double>();
    }
}

其他值类型可以很容易地进行扩展。

用法

使用方法非常简单,只需要将类型从int更改为Failable<int>

视图模型

public Failable<int> NumberValue
{
    //Custom logic along with validation
    //using IsValid property
}

XAML

<TextBox Text="{Binding NumberValue,Converter={x:Static local:Failable.Int32Converter}}"/>

这样,您可以通过检查IsValid属性,在ViewModel中使用相同的验证机制(IDataErrorInfoINotifyDataErrorInfo或其他任何内容)。如果IsValid为true,则可以直接使用Value


这是一个有趣的方法,我从未考虑过。感谢您的见解,这在将来肯定会派上用场! :) - poke

2
好的,我相信我已经找到了你要寻找的答案...
这不容易解释 - 但是...
一旦解释清楚,就非常容易理解...
我认为将MVVM视为"标准"或至少尝试成为标准最准确/“认证”。

但在我们开始之前...您需要改变一个关于MVVM的概念:

"此外,它还将改变视图模型的语义。对我来说, 视图是为视图模型而构建的,而不是反过来的 - 当然,设计 视图模型取决于我们想象视图要做什么, 但是视图如何执行操作仍然有一般自由"

那段话是你问题的根源.. - 为什么?

因为你声称View-Model没有调整自己以适应View的角色..
这在很多方面都是错误的 - 我会向您简单证明。

如果您有一个属性,例如:

public Visibility MyPresenter { get...

如果不是为View服务,那么Visibility是什么?
类型本身和将分配给该属性的名称肯定是为View而设定的。

根据我的经验,在MVVM中有两种可区分的View-Models类别:

  • Presenter View Model - 用于连接按钮、菜单、选项卡等....
  • Entity View Model - 用于连接将实体数据带到屏幕的控件。

这些是两个不同的 - 完全不同的关注点。

现在来看看解决方案:

public abstract class ViewModelBase : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;

   public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
   {
      if (PropertyChanged != null)
         PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
   }
}


public class VmSomeEntity : ViewModelBase, INotifyDataErrorInfo
{
    //This one is part of INotifyDataErrorInfo interface which I will not use,
    //perhaps in more complicated scenarios it could be used to let some other VM know validation changed.
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; 

    //will hold the errors found in validation.
    public Dictionary<string, string> ValidationErrors = new Dictionary<string, string>();

    //the actual value - notice it is 'int' and not 'string'..
    private int storageCapacityInBytes;

    //this is just to keep things sane - otherwise the view will not be able to send whatever the user throw at it.
    //we want to consume what the user throw at us and validate it - right? :)
    private string storageCapacityInBytesWrapper;

    //This is a property to be served by the View.. important to understand the tactic used inside!
    public string StorageCapacityInBytes
    {
       get { return storageCapacityInBytesWrapper ?? storageCapacityInBytes.ToString(); }
       set
       {
          int result;
          var isValid = int.TryParse(value, out result);
          if (isValid)
          {
             storageCapacityInBytes = result;
             storageCapacityInBytesWrapper = null;
             RaisePropertyChanged();
          }
          else
             storageCapacityInBytesWrapper = value;         

          HandleValidationError(isValid, "StorageCapacityInBytes", "Not a number.");
       }
    }

    //Manager for the dictionary
    private void HandleValidationError(bool isValid, string propertyName, string validationErrorDescription)
    {
        if (!string.IsNullOrEmpty(propertyName))
        {
            if (isValid)
            {
                if (ValidationErrors.ContainsKey(propertyName))
                    ValidationErrors.Remove(propertyName);
            }
            else
            {
                if (!ValidationErrors.ContainsKey(propertyName))
                    ValidationErrors.Add(propertyName, validationErrorDescription);
                else
                    ValidationErrors[propertyName] = validationErrorDescription;
            }
        }
    }

    // this is another part of the interface - will be called automatically
    public IEnumerable GetErrors(string propertyName)
    {
        return ValidationErrors.ContainsKey(propertyName)
            ? ValidationErrors[propertyName]
            : null;
    }

    // same here, another part of the interface - will be called automatically
    public bool HasErrors
    {
        get
        {
            return ValidationErrors.Count > 0;
        }
    }
}

现在,在您的代码的某个位置 - 您的按钮命令“CanExecute”方法可以在其实现中调用VmEntity.HasErrors。

从现在开始,愿您的代码在验证方面平安无事 :)


1
缺点是这可能会过于频繁地对所有属性进行验证,但大多数验证应该足够简单,不会影响性能。另一种解决方案是记住使用验证产生错误的属性并仅检查那些属性,但这似乎有点过于复杂和不必要。 您不需要跟踪哪些属性存在错误;您只需要知道错误是否存在。视图模型可以维护错误列表(也用于显示错误摘要),而IsValid属性只需反映列表是否有任何内容即可。每次调用IsValid时,您不需要每次都检查所有内容,只要确保错误摘要是最新的,并且每次更改后都会刷新IsValid
最后,这是一个视图问题。视图(及其数据绑定引擎)负责为视图模型提供适当的值以便处理。但在这种情况下,似乎没有好的方法告诉视图模型应该使旧属性值无效。
您可以监听与绑定到视图模型的容器内的错误:
container.AddHandler(Validation.ErrorEvent, Container_Error);

...

void Container_Error(object sender, ValidationErrorEventArgs e) {
    ...
}

这会在错误添加或移除时通知您,您可以通过 e.Error.Exception 是否存在来识别绑定异常,因此您的视图可以维护绑定异常列表并通知视图模型。但是,任何解决此问题的方案都将是一个 hack,因为视图没有正确发挥其作用,即给用户提供一种读取和更新视图模型结构的手段。这应该被视为一个临时解决方案,直到您正确地向用户呈现某种“整数框”而不是文本框。

“你不需要追踪哪些属性有错误” - 如果我不想在每次更改其中一个属性时重新验证每个属性,那么我就必须这样做。否则,我无法说“属性A之前是无效的,现在已经更改并且是有效的。” - poke
1
@poke 是的,能够按属性查找错误列表并有选择地更新会很有帮助...不过我认为这是微小的优化。无论如何,我认为标准使用 IDataErrorInfo 能够给你最大的灵活性,因为你可以缓存 Error / Item / IsValid 的结果,并让视图模型决定何时以及如何更新它们。 - nmclean

1
这里提供了一种简化的方法,如果你不想实现大量额外的代码...情景是你的视图模型中有一个int属性(也可以是decimal或其他非字符串类型),并且在视图中将一个文本框绑定到它上面。你在视图模型中进行验证,在属性的setter中触发验证。在视图中,用户输入123abc,视图逻辑会在视图中突出显示错误,但无法设置属性,因为值是错误的类型。setter从未被调用。最简单的解决方案是将视图模型中的int属性更改为字符串属性,并将值从模型中转换为字符串,然后再转换回来。这允许错误文本命中属性的setter,然后您的验证代码可以检查数据并根据需要拒绝它。在我看来,WPF中的验证是有问题的,正如人们之前尝试解决问题的复杂(和巧妙)方式所示。对我来说,我不想添加大量额外的代码或实现自己的类型类来使文本框验证生效,因此基于字符串的属性是我可以接受的,即使它感觉有点笨拙。
微软应该考虑修复这个问题,使得绑定到int或decimal属性的文本框中无效的用户输入可以以一种优雅的方式传达给视图模型。例如,他们应该能够为XAML控件创建一个新的绑定属性,将视图逻辑验证错误传达到视图模型的属性中。
感谢和尊重那些提供了详细答案的其他人。

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