gpt4 book ai didi

c# - 使用 MVVM 进行正确验证

转载 作者:行者123 更新时间:2023-12-02 08:33:09 25 4
gpt4 key购买 nike

警告:很长很详细的帖子。

好的,使用 MVVM 时在 WPF 中进行验证。我现在读了很多东西,看了很多 SO 问题,并尝试了很多方法,但在某些时候,一切都感觉有些 hacky,我真的不知道如何以正确的方式做到这一点™。

理想情况下,我希望使用 IDataErrorInfo 在 View 模型中进行所有验证;所以这就是我所做的。然而,有不同的方面使该解决方案不是整个验证主题的完整解决方案。

情况

让我们采用以下简单的形式。如您所见,这没什么特别的。我们只有两个文本框,它们分别绑定(bind)到 View 模型中的 stringint 属性。此外,我们有一个绑定(bind)到 ICommand 的按钮。

Simple form with only a string and integer input

所以对于验证我们现在有两个选择:

  • 当文本框的值发生变化时,我们可以自动运行验证。因此,当用户输入无效的内容时,他会得到即时响应。
  • 当出现任何错误时,我们可以更进一步禁用该按钮。
  • 或者我们可以仅在按下按钮时显式运行验证,然后在适用时显示所有错误。显然我们不能在这里禁用错误按钮。

  • 理想情况下,我想实现选择 1。对于激活 ValidatesOnDataErrors 的普通数据绑定(bind),这是默认行为。因此,当文本更改时,绑定(bind)会更新源并触发该属性的 IDataErrorInfo 验证;错误被报告回 View 。到现在为止还挺好。

    View 模型中的验证状态

    有趣的一点是让 View 模型或本例中的按钮知道是否有任何错误。 IDataErrorInfo的工作方式,主要是向 View 报告错误。因此 View 可以轻松查看是否有任何错误,显示它们,甚至使用 Validation.Errors 显示注释。此外,验证总是在查看单个属性时发生。

    所以让 View 模型知道什么时候有任何错误,或者验证是否成功,是很棘手的。一个常见的解决方案是简单地触发 View 模型本身所有属性的 IDataErrorInfo 验证。这通常使用单独的 IsValid 属性来完成。好处是这也可以很容易地用于禁用命令。缺点是这可能会过于频繁地对所有属性运行验证,但大多数验证应该足够简单,不会损害性能。另一种解决方案是使用验证记住哪些属性产生了错误并只检查这些属性,但这在大多数情况下似乎有点过于复杂且不必要。

    最重要的是,这可以正常工作。 IDataErrorInfo 提供对所有属性的验证,我们可以简单地在 View 模型本身中使用该接口(interface)来运行整个对象的验证。问题介绍:

    绑定(bind)异常

    View 模型为其属性使用实际类型。所以在我们的例子中,整数属性是一个实际的 int 。然而, View 中使用的文本框本身只支持文本。因此,当绑定(bind)到 View 模型中的 int 时,数据绑定(bind)引擎将自动执行类型转换——或者至少它会尝试。如果您可以在用于数字的文本框中输入文本,则内部并不总是有效数字的可能性很高:因此数据绑定(bind)引擎将无法转换并抛出 FormatException

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

    在 View 方面,我们可以很容易地看到这一点。来自绑定(bind)引擎的异常会被 WPF 自动捕获并显示为错误——甚至不需要启用 Binding.ValidatesOnExceptions ,这对于在 setter 中抛出的异常是必需的。错误消息确实有一个通用文本,所以这可能是一个问题。我通过使用 Binding.UpdateSourceExceptionFilter 处理程序为自己解决了这个问题,检查抛出的异常并查看源属性,然后生成一个不太通用的错误消息。所有这些都封装到我自己的 Binding 标记扩展中,所以我可以拥有我需要的所有默认值。

    所以景色还不错。用户犯了一个错误,看到一些错误反馈并可以纠正它。然而, View 模型丢失了。由于绑定(bind)引擎抛出异常,源从未更新。所以 View 模型仍然是旧值,这不是显示给用户的, IDataErrorInfo 验证显然不适用。

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

    另一种选择是将 Binding.UpdateSourceExceptionFilter 中处理的异常中继到 View 模型,因此它也会收到通知。 View 模型甚至可以为绑定(bind)提供一些接口(interface)来报告这些事情,允许自定义错误消息而不是通用的每个类型的错误消息。但这会创建从 View 到 View 模型的更强耦合,我通常希望避免这种情况。

    另一个“解决方案”是去掉所有类型的属性,使用普通的 string 属性并在 View 模型中进行转换。这显然会将所有验证转移到 View 模型,但也意味着数据绑定(bind)引擎通常会处理大量重复的事情。此外,它会改变 View 模型的语义。对我来说, View 是为 View 模型构建的,而不是相反——当然 View 模型的设计取决于我们想象 View 要做什么,但 View 如何做到这一点仍然存在普遍的自由。所以 View 模型定义了一个 int 属性,因为有一个数字; View 现在可以使用文本框(允许所有这些问题),或者使用 native 处理数字的东西。所以不,将属性类型更改为 string 对我来说不是一个选择。

    归根结底,这是 View 的问题。 View (及其数据绑定(bind)引擎)负责为 View 模型提供适当的值以供使用。但是在这种情况下,似乎没有好的方法告诉 View 模型它应该使旧的属性值无效。

    绑定(bind)组

    Binding groups 是我试图解决这个问题的一种方法。绑定(bind)组能够对所有验证进行分组,包括 IDataErrorInfo 和抛出的异常。如果 View 模型可用,它们甚至可以检查所有这些验证源的验证状态,例如使用 CommitEdit

    默认情况下,绑定(bind)组实现上面的选项 2。它们使绑定(bind)显式更新,本质上添加了一个额外的未提交状态。因此,当单击按钮时,该命令可以提交这些更改,触发源更新和所有验证,如果成功则获得单个结果。所以命令的 Action 可能是这样的:
     if (bindingGroup.CommitEdit())
    SaveEverything();
    CommitEdit 只有在所有验证都成功时才会返回 true。它将考虑 IDataErrorInfo 并检查绑定(bind)异常。这似乎是选项 2 的完美解决方案。唯一有点麻烦的是使用绑定(bind)管理绑定(bind)组,但我已经为自己构建了一些主要解决这个问题的东西 ( related)。

    如果绑定(bind)存在绑定(bind)组,则绑定(bind)将默认为显式 UpdateSourceTrigger 。要使用绑定(bind)组实现上面的选择 1,我们基本上必须更改触发器。因为我有一个自定义绑定(bind)扩展,所以这很简单,我只是将它设置为 LostFocus

    所以现在,只要文本字段发生变化,绑定(bind)仍然会更新。如果源可以更新(绑定(bind)引擎不会抛出任何异常),那么 IDataErrorInfo 将照常运行。如果无法更新, View 仍然可以看到它。如果我们点击我们的按钮,底层命令可以调用 CommitEdit(虽然不需要提交任何东西)并获得总验证结果,看看它是否可以继续。

    我们可能无法通过这种方式轻松禁用该按钮。至少不是来自 View 模型。一遍又一遍地检查验证并不是一个好主意,只是为了更新命令状态,并且当绑定(bind)引擎异常被抛出时(这应该禁用按钮)或当它消失时不会通知 View 模型再次启用按钮。我们仍然可以使用 Validation.HasError 添加一个触发器来禁用 View 中的按钮,所以这并非不可能。

    解决方案?

    所以总的来说,这似乎是完美的解决方案。我的问题是什么?老实说,我并不完全确定。绑定(bind)组是一个复杂的东西,似乎通常在较小的组中使用,可能在单个 View 中具有多个绑定(bind)组。通过为整个 View 使用一个大绑定(bind)组来确保我的验证,感觉好像我在滥用它。我一直在想,必须有更好的方法来解决整个情况,因为肯定不会只有我一个人遇到这些问题。到目前为止,我还没有真正看到很多人使用绑定(bind)组来使用 MVVM 进行验证,所以感觉很奇怪。

    那么,在能够检查绑定(bind)引擎异常的同时,使用 MVVM 在 WPF 中进行验证的正确方法究竟是什么?

    我的解决方案(/hack)

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

    最后,我的问题归结为绑定(bind)异常问题, View 模型不知道它何时发生。虽然我可以使用上面详述的绑定(bind)组来处理这个问题,但我仍然决定反对它,因为我对它感到不太舒服。那我做了什么?

    正如我上面提到的,我通过监听绑定(bind)的 UpdateSourceExceptionFilter 来检测 View 端的绑定(bind)异常。在那里,我可以从绑定(bind)表达式的 DataItem 获得对 View 模型的引用。然后我有一个接口(interface) IReceivesBindingErrorInformation,它将 View 模型注册为可能的接收器,以获取有关绑定(bind)错误的信息。然后我使用它来将绑定(bind)路径和异常传递给 View 模型:
    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
    // ...
    }

    在 View 模型中,每当我收到有关路径绑定(bind)表达式的通知时,我都会记得:
    HashSet<string> bindingErrors = new HashSet<string>();
    void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception)
    {
    bindingErrors.Add(path);
    }

    每当 IDataErrorInfo 重新验证属性时,我就知道绑定(bind)有效,我可以从哈希集中清除该属性。

    在 View 模型中,我可以检查散列集是否包含任何项目并中止任何需要完全验证数据的操作。由于从 View 到 View 模型的耦合,它可能不是最好的解决方案,但使用该接口(interface)至少是一个问题。

    最佳答案

    Warning: Long answer also



    我使用 IDataErrorInfo 接口(interface)进行验证,但我已根据需要对其进行了自定义。我想你会发现它也解决了你的一些问题。与您的问题的一个区别是我在我的基本数据类型类中实现了它。

    正如您所指出的,这个接口(interface)一次只处理一个属性,但显然在这个时代,这是不好的。所以我只是添加了一个集合属性来代替:
    protected ObservableCollection<string> errors = new ObservableCollection<string>();

    public virtual ObservableCollection<string> Errors
    {
    get { return errors; }
    }

    为了解决您无法显示外部错误的问题(在您的情况下来自 View ,但在我的情况下来自 View 模型),我只是添加了另一个集合属性:
    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; }
    }

    这使我能够使用自定义 Grid.Visibility 将其绑定(bind)到 BoolToVisibilityConverter ,例如。显示一个 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 方法是自定义的 extension 方法,并且“按照 jar 头上所说的去做”。请注意它将如何依次调用我想要验证的每个属性并从中编译一个集合,而忽略重复的错误。

    使用 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 接口(interface),其中包含一些附加功能。您可以从 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 并且在 FooObject 中使用了 Binding ,你可以这样做:
    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;
    }
    }

    这样您就可以满足您的要求,但是您将需要添加很多额外的代码。

    关于c# - 使用 MVVM 进行正确验证,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19498485/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com