警告:非常长而详细的帖子。
好的,使用 MVVM 时在 WPF 中进行验证。我现在已经阅读了很多东西,查看了许多 SO 问题,并尝试了许多方法,但是在某些时候一切都感觉有些 hacky,我真的不知道如何以正确的方式做到这一点™。
IDataErrorInfo
理想情况下,我希望使用;在视图模型中进行所有验证。所以这就是我所做的。然而,有不同的方面使该解决方案不是整个验证主题的完整解决方案。
情况
让我们采用以下简单的形式。如您所见,这没什么花哨的。我们只有两个文本框,它们分别绑定到视图模型中的一个string
和int
属性。此外,我们有一个绑定到ICommand
.
因此,对于验证,我们现在有两个选择:
- 只要文本框的值发生变化,我们就可以自动运行验证。因此,当用户输入无效内容时,他会立即得到响应。
- 当出现任何错误时,我们可以进一步禁用按钮。
- 或者我们可以仅在按下按钮时显式运行验证,然后显示所有错误(如果适用)。显然我们不能在这里禁用错误按钮。
理想情况下,我想实现选择 1。对于激活的普通数据绑定,ValidatesOnDataErrors
这是默认行为。因此,当文本更改时,绑定会更新源并触发IDataErrorInfo
对该属性的验证;错误被报告回视图。到目前为止,一切都很好。
视图模型中的验证状态
有趣的是让视图模型或本例中的按钮知道是否有任何错误。这种方式IDataErrorInfo
有效,主要是将错误报告给视图。因此视图可以很容易地查看是否有任何错误,显示它们,甚至使用Validation.Errors
. 此外,验证总是在查看单个属性时发生。
所以让视图模型知道什么时候有任何错误,或者验证是否成功,是很棘手的。一个常见的解决方案是简单地触发IDataErrorInfo
视图模型本身中所有属性的验证。这通常使用单独的IsValid
属性来完成。好处是这也可以很容易地用于禁用命令。缺点是这可能会过于频繁地对所有属性进行验证,但大多数验证应该足够简单,不会损害性能。另一种解决方案是使用验证来记住哪些属性产生了错误并只检查那些,但这在大多数情况下似乎有点过于复杂和不必要。
最重要的是,这可以正常工作。IDataErrorInfo
为所有属性提供验证,我们可以简单地在视图模型本身中使用该接口来为整个对象运行验证。问题介绍:
绑定异常
视图模型对其属性使用实际类型。所以在我们的例子中,整数属性是一个实际的int
. 然而,视图中使用的文本框本身只支持text。所以当绑定到int
视图模型中时,数据绑定引擎会自动执行类型转换——或者至少它会尝试。如果您可以在用于数字的文本框中输入文本,则内部并不总是有效数字的可能性很高:因此数据绑定引擎将无法转换并抛出FormatException
.
在视图方面,我们可以很容易地看到这一点。来自绑定引擎的异常会被 WPF 自动捕获并显示为错误——甚至不需要启用Binding.ValidatesOnExceptions
setter 中抛出的异常所需要的。错误消息确实有一个通用文本,所以这可能是一个问题。我已经通过使用Binding.UpdateSourceExceptionFilter
处理程序为自己解决了这个问题,检查抛出的异常并查看源属性,然后生成一个不太通用的错误消息。所有这些都封装到我自己的 Binding 标记扩展中,所以我可以拥有我需要的所有默认值。
所以景色还不错。用户犯了一个错误,看到一些错误反馈并可以纠正它。然而,视图模型丢失了。由于绑定引擎抛出异常,源从未更新。所以视图模型仍然是旧值,这不是向用户显示的内容,并且IDataErrorInfo
验证显然不适用。
更糟糕的是,视图模型没有很好的方法知道这一点。至少,我还没有找到一个好的解决方案。可能的做法是让视图向视图模型报告出现错误。这可以通过将Validation.HasError
属性数据绑定回视图模型(这不可能直接)来完成,因此视图模型可以首先检查视图的状态。
另一种选择是将处理的异常中继Binding.UpdateSourceExceptionFilter
到视图模型,因此它也会收到通知。视图模型甚至可以为绑定提供一些接口来报告这些事情,允许自定义错误消息而不是通用的每个类型的错误消息。但这会在视图和视图模型之间产生更强的耦合,而我通常希望避免这种耦合。
另一个“解决方案”是摆脱所有类型化的属性,使用普通string
属性并在视图模型中进行转换。这显然会将所有验证转移到视图模型,但也意味着数据绑定引擎通常会处理大量重复的事情。此外,它会改变视图模型的语义。对我来说,视图是为视图模型构建的,而不是相反——当然,视图模型的设计取决于我们想象视图要做什么,但是视图如何做到这一点仍然有一般的自由。所以视图模型定义了一个int
属性,因为有一个数字;视图现在可以使用文本框(允许所有这些问题),或者使用本机与数字一起使用的东西。所以不,将属性的类型更改为string
不是我的选择。
归根结底,这是观点的问题。视图(及其数据绑定引擎)负责为视图模型提供适当的值以供使用。但是在这种情况下,似乎没有好的方法告诉视图模型它应该使旧的属性值无效。
绑定组
绑定组是我试图解决这个问题的一种方法。绑定组能够对所有验证进行分组,包括IDataErrorInfo
和抛出的异常。如果视图模型可用,它们甚至可以检查所有这些验证源的验证状态,例如使用CommitEdit
.
默认情况下,绑定组实现上面的选项 2。它们使绑定显式更新,本质上添加了一个额外的未提交状态。因此,当单击按钮时,该命令可以提交这些更改,触发源更新和所有验证,并在成功时获得单个结果。所以命令的动作可能是这样的:
if (bindingGroup.CommitEdit())
SaveEverything();
CommitEdit
只有在所有验证成功时才会返回 true 。它将IDataErrorInfo
考虑并检查绑定异常。这似乎是选择 2 的完美解决方案。唯一有点麻烦的是使用绑定管理绑定组,但我已经为自己构建了一些主要处理此问题的东西(相关)。
如果绑定存在绑定组,则绑定将默认为显式UpdateSourceTrigger
. 要使用绑定组实现上述选项 1,我们基本上必须更改触发器。因为无论如何我都有一个自定义绑定扩展,这相当简单,我只是将它设置LostFocus
为所有。
所以现在,只要文本字段发生变化,绑定仍然会更新。如果源可以更新(绑定引擎不抛出异常),那么IDataErrorInfo
将照常运行。如果它无法更新,视图仍然可以看到它。如果我们点击我们的按钮,底层命令可以调用CommitEdit
(尽管不需要提交任何东西)并获取总验证结果,看看它是否可以继续。
我们可能无法通过这种方式轻松禁用该按钮。至少不是来自视图模型。一遍又一遍地检查验证并不是一个好主意,只是为了更新命令状态,并且当绑定引擎异常抛出时(这应该禁用按钮)或当它消失时,视图模型不会得到通知再次启用该按钮。我们仍然可以添加一个触发器来禁用视图中的按钮,Validation.HasError
所以这不是不可能的。
解决方案?
所以总的来说,这似乎是一个完美的解决方案。不过,我有什么问题?老实说,我并不完全确定。绑定组是一个复杂的东西,似乎通常在较小的组中使用,可能在单个视图中有多个绑定组。通过为整个视图使用一个大的绑定组来确保我的验证,感觉好像我在滥用它。我只是一直在想,必须有更好的方法来解决整个情况,因为我肯定不会是唯一一个遇到这些问题的人。到目前为止,我还没有真正看到很多人使用绑定组来验证 MVVM,所以感觉很奇怪。
那么,在能够检查绑定引擎异常的同时,使用 MVVM 在 WPF 中进行验证的正确方法到底是什么?
我的解决方案(/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
重新验证一个属性时,我就知道绑定有效,我可以从哈希集中清除该属性。
然后,在视图模型中,我可以检查哈希集是否包含任何项目并中止任何需要完全验证数据的操作。由于从视图到视图模型的耦合,它可能不是最好的解决方案,但使用该接口至少不是问题。