警告:非常长且详细的文章。
好的,当使用MVVM时,在WPF中进行验证。我现在已经阅读了很多内容,查看了许多SO问题,并尝试了许多方法,但是在某些时候,一切都感觉有点不可靠,我真的不确定如何以正确的方式™进行操作。
理想情况下,我希望所有验证都在视图模型中使用IDataErrorInfo
进行处理;所以我就这样做了。然而,有不同的方面使得这个解决方案对于整个验证主题来说并不完全。
情况
让我们来看看以下简单的表单。正如您所见,它并不花哨。我们只有两个文本框,每个文本框绑定到视图模型中的一个string
和int
属性。此外,我们还有一个按钮,该按钮绑定到一个ICommand
。
- 每当文本框的值更改时自动运行验证。这样,当用户输入无效内容时,他会立即得到响应。
- 我们可以进一步采取措施,在出现任何错误时禁用按钮。
- 或者我们只在按下按钮时显式地运行验证,然后显示所有适用的错误。显然,我们不能在此处在错误时禁用按钮。
ValidatesOnDataErrors
的普通数据绑定,这是默认行为。因此,当文本更改时,绑定更新源并触发该属性的IDataErrorInfo
验证;错误将返回视图。目前为止还好。
视图模型中的验证状态
有趣的部分在于让视图模型或者说在这个例子中的按钮知道是否存在任何错误。使用IDataErrorInfo
的方式主要是将错误报告回传给视图,因此视图可以轻松看到是否存在任何错误,并显示它们,甚至可以使用 Validation.Errors
显示注释。此外,验证总是查看单个属性。因此,使视图模型知道是否存在任何错误或验证成功是很棘手的。常见的解决方案是简单地触发视图模型本身所有属性的
IDataErrorInfo
验证。通常使用单独的 IsValid
属性来完成这项工作。好处是这也可以轻松用于禁用命令。缺点是这可能会过于频繁地对所有属性运行验证,但大多数验证应该足够简单,不会影响性能。另一个解决方案是记住使用验证产生错误的属性,并仅检查那些属性,但这似乎有些过于复杂和不必要。这意味着这可能很好地发挥作用。
IDataErrorInfo
为所有属性提供验证,我们可以在视图模型本身中使用该接口来运行整个对象的验证。引入问题:视图模型使用其属性的实际类型。因此,在我们的示例中,整数属性是一个实际的int
。然而,视图中使用的文本框本质上仅支持文本。因此,当绑定到视图模型中的int
时,数据绑定引擎将自动执行类型转换,或者至少会尝试。如果您可以在专门用于数字的文本框中输入文本,则很有可能不总是存在有效数字:因此,数据绑定引擎将无法进行转换并抛出FormatException
异常。
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
重新验证一个属性时,我知道绑定起作用了,并且可以从哈希集中清除该属性。在视图模型中,我可以检查哈希集是否包含任何项,并中止需要完全验证数据的任何操作。由于视图与视图模型之间的耦合,这可能不是最好的解决方案,但至少使用该接口会稍微减少一些问题。
OnUpdateSourceExceptionFilter
附加到每个绑定表达式?希望不是手动的。 - JordanBinding
子类型,然后在所有地方都使用它,而不是默认的那个。例如,使用 XML 命名空间f
,绑定将如下所示:{f:Binding MyPath}
。 - poke