适当的验证与MVVM [英] Proper validation with MVVM

查看:320
本文介绍了适当的验证与MVVM的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

警告:非常漫长而细致岗位

好吧,使用MVVM验证时,在WPF。我读过很多东西现在,看着许多SO问题,并试图的许多的办法,但一切都感觉在某些时候有点哈克,我真的不知道该怎么做的右方法的™。

在理想情况下,我想有使用视图模型中的所有验证发生<一个href=\"http://msdn.microsoft.com/en-us/library/system.componentmodel.idataerrorinfo.aspx\"><$c$c>IDataErrorInfo;所以这就是我所做的。然而有不同的方面,使这个解决方案是整个验证话题不是一个完整的解决方案。

情况

让我们以下面的简单形式。正如你所看到的,这是没有任何幻想。我们只是其绑定到字符串 INT 属性中的每个视图模型两个文本框。此外,我们有一个绑定到一个按钮的ICommand

因此​​,对于验证我们现在有两个选择:


  1. 我们可以自动运行验证每当一个文本框的值更改。这样,用户得到即时响应,当他进入事无效。

    • 我们可以进一步的时候有任何错误借此一步禁用按钮。


  2. 或者,我们可以运行验证只有明确地按下按钮时pressed,则显示所有的错误(如果适用)。显然,我们不能在这里禁用错误的按钮。

在理想情况下,我想实现选择题1.对于正常的数据绑定与活化的<一个href=\"http://msdn.microsoft.com/en-us/library/system.windows.data.binding.validatesondataerrors.aspx\"><$c$c>ValidatesOnDataErrors这是默认的行为。因此,当文本发生变化时,绑定更新的来源,并触发该属性的 IDataErrorInfo的确认;错误报告回视图。到目前为止好。

在视图模型验证状态

感兴趣的是让视图模型,或在这种情况下按钮,知道是否有任何错误。该方法 IDataErrorInfo的的作品,它主要是有报告错误回视图。这样的观点可以很容易地看到,如果有任何错误,显示它们甚至使用显示批注<一个href=\"http://msdn.microsoft.com/en-us/library/system.windows.controls.validation.errors.aspx\"><$c$c>Validation.Errors.此外,验证总是发生在寻找的一个属性。

所以具有视图模型知道什么时候有任何错误,或者如果验证成功,是棘手的。常见的解决方案是简单地触发视图模型本身的所有属性的 IDataErrorInfo的验证。这是使用一个单独的的IsValid 属性经常做。这样做的好处是,这也可以方便地用于禁用命令。其缺点是,这可能在所有属性上运行验证有点过于频繁,但大多数的验证应该是根本不够不伤性能。另一个解决方案是要记住使用验证和只检查那些特性产生的错误,但是这似乎有点过于复杂和不必要的大部分时间。

底线是,这可能正常工作。 IDataErrorInfo的提供的所有属性的验证,我们可以简单地使用视图模型本身的接口有太多整个对象运行验证。介绍的问题:

绑定例外

视图模型使用的实际类型的属性。所以在我们的例子中,整数属性是一个真实的 INT 。在视图中使用的文本框本身不过只支持的文本的。因此,在视图模型绑定到 INT 时,数据绑定引擎会自动进行类型转换,或者至少它会尝试。如果你能意味着数字的文本框中输入文本时,几率很高,不会有永远是里面的有效号码:所以数据绑定引擎将无法转换,并抛出一个 FormatException

在视图方面,我们可以很容易地看到这一点。从绑定引擎异常由WPF自动捕获并显示为错误 - 竟然没有必要启用<一个href=\"http://msdn.microsoft.com/en-us/library/system.windows.data.binding.validatesonexceptions.aspx\"><$c$c>Binding.ValidatesOnExceptions这将需要在设置方法抛出异常。错误消息做有一个通用的文本的,所以这可能是一个问题。我以解决了这个给自己一个<一个href=\"http://msdn.microsoft.com/en-us/library/system.windows.data.binding.updatesourceexceptionfilter.aspx\"><$c$c>Binding.UpdateSourceExceptionFilter处理程序,检查抛出的异常,并查看源属性,然后产生了比较特殊的错误讯息。所有这些封装远到我自己的绑定标记扩展,所以我可以有我需要的所有默认值。

所以认为是好的。用户作出了错误,看到一些差错反馈,并且可以纠正它。视图模型不过的丢失的。作为结合发动机抛出异常,源从未更新。因此视图模型仍然在旧的价值,这是不是什么东西被显示给用户,而 IDataErrorInfo的验证显然不适用。

更糟糕的是,有一个视图模型知道这没有什么好办法。至少,我还没有发现这方面的一个很好的解决方案呢。什么是可能是有查看报告回视图模型,有一个错误。这可以通过数据绑定的<一个完成href=\"http://msdn.microsoft.com/en-us/library/system.windows.controls.validation.haserror.aspx\"><$c$c>Validation.HasError财产退还视图模型(这是不能直接),因此视图模型可以先检查该视图的状态。

另一种选择是,以继电器 Binding.UpdateSourceExceptionFilter 处理,视图模型的异常,因此它会通知它。视图模型甚至可以提供一些接口的绑定报告这些东西,允许自定义错误消息,而不是一般每个类型的。但是,这会从视图到视图模型,我一般要避免创建一个强大的耦合。

另外一个解决方案将摆脱​​所有类型的属性,使用普通的字符串属性和做视图模型转换代替。这显然​​将移动所有验证到视图模式,但也意味着事情重复的令人难以置信的大量数据绑定引擎通常需要照顾。此外,它会改变视图模型的语义。对我来说,一个观​​点是专为视图模型,而不是反向的过程视图模型的设计取决于我们想象的观点做的,但还是有自由的普遍的观点是如何做到这一点。因此视图模型定义了一个 INT 属性,因为有一个数字;视图现在可以使用文本框(允许所有这些问题),或使用的东西,本身对数字作品。所以,不,改变类型属性为字符串不是我的选择。

在结束时,这是鉴于这样的问题。视图(及其数据绑定引擎)负责给予视图模型适当的值的工作。但是,在这种情况下,似乎是要告诉它应该废止旧属性值的视图模型没有很好的办法。

BindingGroups

绑定是我试图解决这个单向组。结合集团有能力把所有的验证,包括 IDataErrorInfo的和抛出的异常。如果提供给视图模型,他们甚至有一个平均使用检查验证状态的所有的这些验证源,例如<一个href=\"http://msdn.microsoft.com/en-us/library/system.windows.data.bindinggroup.commitedit.aspx\"><$c$c>CommitEdit.

默认情况下,结合集团从上面实施选择2。它们使绑定显式更新,从根本上增加一个额外的未提交的状态。所以点击按钮时,该命令的提交的这些变化,触发源更新和所有验证,并得到一个结果,如果它成功了。因此,该命令的操作可能是这样的:

 如果(bindingGroup.CommitEdit())
     SaveEverything();

commitEdit的如果的所有的验证成功才会返回true。这将需要 IDataErrorInfo的考虑在内,还要检查结合例外。这似乎是选择2.这是一个有点麻烦与绑定管理绑定组唯一一个完美的解决方案,但我已经建立了自己的东西,大多注意到了这一问题(的相关

如果一个结合基团是有约束力的present,绑定将默认为一个明确的<一个href=\"http://msdn.microsoft.com/en-us/library/system.windows.data.updatesourcetrigger.aspx\"><$c$c>UpdateSourceTrigger.从上面使用绑定组实施选择1,我们基本上改变触发。因为我有一个自定义绑定的扩展,无论如何,这是相当简单的,我只是将它设置为引发LostFocus 所有

所以,现在,绑定仍然会更新每当文本字段的变化。如果源可能被更新(绑定引擎抛出也不例外),那么 IDataErrorInfo的将照常运行。如果它不能被认为是更新还是能看到它。如果我们点击我们的按钮,底层的命令是调用 commitEdit的(虽然没有什么需要提交),并得到总的验证结果,看它是否能够继续。

我们可能无法轻松地禁用按钮这种方式。从视图模型至少不会。检查过验证和过是不是真的是一个好主意只是为了更新命令状态,而当绑定引擎异常被抛出无论如何(这应该禁用按钮,然后) - 或当它消失到不通知视图模型再次启用该按钮。我们仍然可以添加一个触发器使用禁用视图按钮在<一个href=\"http://msdn.microsoft.com/en-us/library/system.windows.controls.validation.haserror.aspx\"><$c$c>Validation.HasError所以这不是不可能的。

解决方案?

所以,总体来说,这似乎是完美的解决方案。什么是我与它的问题有关系吗?说实话,我不能完全肯定。结合基团是,似乎在更小的组被通常使用的,可能具有在单个视图中多个结合基的复杂的事情。通过使用一个大的绑定组为全视图只是为了确保我的验证,那感觉就好像我在滥用它。而我只是一直在想,一定有解决这个大局更好的办法,因为我肯定不能有这些问题的唯一的一个。到目前为止我还没有看到很多人使用绑定组与MVVM验证的好,所以我就觉得奇怪。

那么,究竟是做验证在WPF与MVVM同时能够检查绑定引擎异常的正确方法?


我的解决方案(/黑客)

首先,感谢您的输入!正如我上面写的,我使用 IDataErrorInfo的已经做我的数据验证和我个人认为它是最舒适的实用程序执行的验证工作。我使用类似于谢里丹在下面他的回答提出的实用程序,所以保持精细的作品了。

在最后,我的问题归结到绑定的异常问题,这里的视图模型只是不知道什么时候发生。虽然我可以用结合组处理这上面详述,我还是决定不这么做,因为我只是不觉得所有的舒服。所以我做了什么呢?

正如我上面提到的,我通过听一个绑定的 UpdateSourceExceptionFilter 检测到的观点端绑定异常。在那里,我可以从绑定前pression的<一个视图模型的引用href=\"http://msdn.microsoft.com/en-us/library/system.windows.data.bindingex$p$pssion.dataitem.aspx\"><$c$c>DataItem.然后,我有哪些寄存器视图模型作为一个可能接收大约绑定错误信息的界面 IReceivesBindingErrorInformation 。然后,我用它来传递绑定路径和例外视图模型:

 对象OnUpdateSourceExceptionFilter(对象bindEx pression,例外的例外)
{
    BindingEx pression EXPR =(bindEx pression作为BindingEx pression);
    如果(expr.DataItem是IReceivesBindingErrorInformation)
    {
        ((IReceivesBindingErrorInformation)expr.DataItem).ReceiveBindingErrorInformation(expr.ParentBinding.Path.Path,例外);
    }    //检查Fo​​rmatException并产生更好的错误
    // ...
 }

在视图模型,然后我记得,每当我收到通知的路径的绑定前pression:

 的HashSet&LT;串GT; bindingErrors =新的HashSet&LT;串GT;();
无效IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(字符串路径,例外的例外)
{
    bindingErrors.Add(路径);
}

和每当 IDataErrorInfo的重新验证一个属性,我知道绑定工作,我可以清除散集的属性。

在视图模型然后我可以检查散列集合包含任何项目并中止要求数据被完全验证的任何行动。它可能不是由于从视图的视图模型耦合的最好的解决方案,但使用该界面它至少稍差的问题。


解决方案

  

警告:龙回答也


我用 IDataErrorInfo的接口,用于验证,但我把它定制我的需要。我想,你会发现,它解决了一些你的问题了。一个区别你的问题是,我实现它在我的基本数据类型类。

正如你指出,这只是接口与一个地产交易的时间,但很明显,在这个时代,这是没有好。所以,我只是增加了一个集合属性,而不是使用方法:

 受保护的ObservableCollection&LT;串GT;错误=新的ObservableCollection&LT;串GT;();公共虚拟的ObservableCollection&LT;串GT;错误
{
    {返回错误; }
}

要解决您不能够显示外部错误的问题(在你的情况下,从视图,但在从视图模型矿),我只是添加了另一个集合属性:

 受保护的ObservableCollection&LT;串GT; externalErrors =新的ObservableCollection&LT;串GT;();公众的ObservableCollection&LT;串GT; ExternalErrors
{
    {返回externalErrors; }
}

我有看我收藏的 HasError 属性:

 公共虚拟BOOL HasError
{
    {返回错误= NULL&放大器;!&安培; Errors.Count&GT; 0; }
}

这使我使用自定义的 BoolToVisibilityConverter ,例如这个绑定到 Grid.Visibility 。展现电网与集合控件中显示错误时,有没有。这也让我改变红色高亮显示一个错误(使用另一个转换器),但我猜你的想法。

然后在每个数据类型或模型类,我重写错误财产和贯彻项目索引(在本实施例中简化的):

 公众覆盖的ObservableCollection&LT;串GT;错误
{
    得到
    {
        错误=新的ObservableCollection&LT;串GT;();
        errors.AddUniqueIfNotEmpty(本[名称]);
        errors.AddUniqueIfNotEmpty(本[EmailAddresses]);
        errors.AddUniqueIfNotEmpty(本[SomeOtherProperty]);
        errors.AddRange(ExternalErrors);
        返回错误;
    }
}公共重写字符串此[字符串propertyName的]
{
    得到
    {
        字符串错误=的String.Empty;
        如果(propertyName的==名称&放大器;&安培; Name.IsNullOrEmpty())错误=您必须输入名称字段。
        否则,如果(propertyName的==EmailAddresses&放大器;&安培; EmailAddresses.Count == 0)错误=您必须至少输入一个电子邮件地址进入邮箱地址(ES)场;
        否则,如果(propertyName的==SomeOtherProperty&放大器;&安培; SomeOtherProperty.IsNullOrEmpty())错误=您必须输入SomeOtherProperty领域。
        返回错误;
    }
}

AddUniqueIfNotEmpty 方法是一个自定义的扩展方法和'做什么是上锡说。注意它会怎么称呼,我想反过来验证并从中编译集合,忽略重复的错误,每个属性。

使用 ExternalErrors 收藏,我可以确认的事情,我不能在数据类验证:

 私人无效ValidateUniqueName(流派的流派)
{
    字符串的errorMessage =音乐流派的名称必须是唯一的;
    如果(!IsGenreNameUnique(流派))
    {
        如果genre.ExternalErrors.Add(的errorMessage)(genre.ExternalErrors.Contains(的errorMessage)!);
    }
    其他genre.ExternalErrors.Remove(的errorMessage);
}

要满足您对其中一个用户输入一个字母字符变成 INT 场上的局势来看,我倾向于使用自定义的则IsNumeric AttachedProperty 文本框,例如。我不要让他们做出这些类型的错误。我总觉得这是更好地阻止它,而不是让它发生,然后解决它。

总的来说,我在我的WPF验证能力真的很开心,我不希望离开的。

要与和完整性结束了,我觉得我应该提醒你一个事实,现在有一个 INotifyDataErrorInfo 接口,其中包括一些这方面的附加功能。你可以找到从<更href=\"http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifydataerrorinfo.aspx\"><$c$c>INotifyDataErrorInfo接口页面上MSDN。


更新>>>

是的, ExternalErrors 属性只是让我补充,涉及到的数据对象从对象外部错误...对不起,我的例子是不完全的...如果我表现出你 IsGenreNameUnique 方法,你会看到它使用的LinQ 上的所有的集合中的类型数据项,以确定该对象的名称是否唯一与否:

 私人布尔IsGenreNameUnique(流派的流派)
{
    返回Genres.Where(D =&GT; d.Name =&的String.Empty放大器;!&安培; d.Name == genre.Name).Count之间的()== 1;
}

至于你的 INT / 字符串的问题,我可以看到的唯一途径,你得到的的在你的数据类错误是,如果你声明所有的属性对象,但你不得不一个可怕的很多铸造要做。也许你会喜欢这个双倍的性能:

 公共对象FooObject {搞定;组; } //实现INotifyPropertyChanged公众诠释美孚
{
    {返回FooObject.GetType()== typeof运算(INT)? int.Parse(FooObject):-1; }
}

那么,如果在code的使用和 FooObject 绑定,你可以这样做:

 公众覆盖字符串此[字符串propertyName的]
{
    得到
    {
        字符串错误=的String.Empty;
        如果(propertyName的==FooObject&放大器;&安培; FooObject.GetType()= typeof运算(INT)!)
            错误=请输入整数为富场。
        ...
        返回错误;
    }
}

这样,你可能会满足您的要求,但你有很多额外的code以添加。

Warning: Very long and detailed post.

Okay, validation in WPF when using MVVM. I’ve read many things now, looked at many SO questions, and tried many approaches, but everything feels somewhat hacky at some point and I’m really not sure how to do it the right way™.

Ideally, I want to have all validation happen in the view model using IDataErrorInfo; so that’s what I did. There are however different aspects that make this solution be not a complete solution for the whole validation topic.

The situation

Let’s take the following simple form. As you can see, it’s nothing fancy. We just have a two textboxes which bind to a string and int property in the view model each. Furthermore we have a button that is bound to an ICommand.

So for the validation we now have a two choices:

  1. We can run the validation automatically whenever the value of a text box changes. As such the user gets an instant response when he entered something invalid.
    • We can take this one step further to disable the button when there are any errors.
  2. Or we can run the validation only explicitly when the button is pressed, then showing all errors if applicable. Obviously we can’t disable the button on errors here.

Ideally, I want to implement choice 1. For normal data bindings with activated ValidatesOnDataErrors this is default behavior. So when the text changes, the binding updates the source and triggers the IDataErrorInfo validation for that property; errors are reported back the view. So far so good.

Validation status in the view model

The interesting bit is to let the view model, or the button in this case, know if there are any errors. The way IDataErrorInfo works, it is mainly there to report errors back to the view. So the view can easily see if there are any errors, display them and even show annotations using Validation.Errors. Furthermore, validation always happens looking at a single property.

So having the view model know when there are any errors, or if the validation succeeded, is tricky. A common solution is to simply trigger the IDataErrorInfo validation for all properties in the view model itself. This is often done using a separate IsValid property. The benefit is that this can also be easily used for disabling the command. The drawback is that this might run the validation on all properties a bit too often, but most validations should be simply enough to not hurt the performance. Another solution would be to remember which properties produced errors using the validation and only check those, but that seems a bit overcomplicated and unnecessary for most times.

The bottom line is that this could work fine. IDataErrorInfo provides the validation for all properties, and we can simply use that interface in the view model itself to run the validation there too for the whole object. Introducing the problem:

Binding exceptions

The view model uses actual types for its properties. So in our example, the integer property is an actual int. The text box used in the view however natively only supports text. So when binding to the int in the view model, the data binding engine will automatically perform type conversions—or at least it will try. If you can enter text in a text box meant for numbers, the chances are high that there won’t always be valid numbers inside: So the data binding engine will fail to convert and throw a FormatException.

On the view side, we can easily see that. Exceptions from the binding engine are automatically caught by WPF and are displayed as errors—there isn’t even a need to enable Binding.ValidatesOnExceptions which would be required for exceptions thrown in the setter. The error messages do have a generic text though, so that could be a problem. I have solved this for myself by using a Binding.UpdateSourceExceptionFilter handler, inspecting the exception being thrown and looking at the source property and then generating a less generic error message instead. All that capsulated away into my own Binding markup extension, so I can have all the defaults I need.

So the view is fine. The user makes an error, sees some error feedback and can correct it. The view model however is lost. As the binding engine threw the exception, the source was never updated. So the view model is still on the old value, which isn’t what’s being displayed to the user, and the IDataErrorInfo validation obviously doesn’t apply.

What’s worse, there is no good way for the view model to know this. At least, I haven’t found a good solution for this yet. What would be possible is to have the view report back to the view model that there was an error. This could be done by data binding the Validation.HasError property back to the view model (which isn’t possible directly), so the view model could check the view’s state first.

Another option would be to relay the exception handled in Binding.UpdateSourceExceptionFilter to the view model, so it would be notified of it as well. The view model could even provide some interface for the binding to report these things, allowing for custom error messages instead of generic per-type ones. But that would create a stronger coupling from the view to the view model, which I generally want to avoid.

Another "solution" would be to get rid of all typed properties, use plain string properties and do the conversion in the view model instead. This obviously would move all validation to the view model, but also mean an incredible amount of duplication of things the data binding engine usually takes care of. Furthermore it would change the semantics of the view model. For me, a view is built for the view model and not the reverse—of course the design of the view model depends on what we imagine the view to do, but there’s still general freedom how the view does that. So the view model defines an int property because there is a number; the view can now use a text box (allowing all these problems), or use something that natively works with numbers. So no, changing the types of the properties to string is not an option for me.

In the end, this is a problem of the view. The view (and its data binding engine) is responsible for giving the view model proper values to work with. But in this case, there seems to be no good way to tell the view model that it should invalidate the old property value.

BindingGroups

Binding groups are one way I tried to tackle this. Binding groups have the ability to group all validations, including IDataErrorInfo and thrown exceptions. If available to the view model, they even have a mean to check the validation status for all of those validation sources, for example using CommitEdit.

By default, binding groups implement choice 2 from above. They make the bindings update explicitly, essentially adding an additional uncommitted state. So when clicking the button, the command can commit those changes, trigger the source updates and all validations and get a single result if it succeeded. So the command’s action could be this:

 if (bindingGroup.CommitEdit())
     SaveEverything();

CommitEdit will only return true if all validations succeeded. It will take IDataErrorInfo into account and also check binding exceptions. This seems to be a perfect solution for choice 2. The only thing that is a bit of a hassle is managing the binding group with the bindings, but I’ve built myself something that mostly takes care of this (related).

If a binding group is present for a binding, the binding will default to an explicit UpdateSourceTrigger. To implement choice 1 from above using binding groups, we basically have to change the trigger. As I have a custom binding extension anyway, this is rather simple, I just set it to LostFocus for all.

So now, the bindings will still update whenever a text field changes. If the source could be updated (binding engine throws no exception) then IDataErrorInfo will run as usual. If it couldn’t be updated the view is still able to see it. And if we click our button, the underlying command can call CommitEdit (although nothing needs to be committed) and get the total validation result to see if it can continue.

We might not be able to disable the button easily this way. At least not from the view model. Checking the validation over and over is not really a good idea just to update the command status, and the view model isn’t notified when a binding engine exception is thrown anyway (which should disable the button then)—or when it goes away to enable the button again. We could still add a trigger to disable the button in the view using the Validation.HasError so it’s not impossible.

Solution?

So overall, this seems to be the perfect solution. What is my problem with it though? To be honest, I’m not entirely sure. Binding groups are a complex thing that seem to be usually used in smaller groups, possibly having multiple binding groups in a single view. By using one big binding group for the whole view just to ensure my validation, it feels as if I’m abusing it. And I just keep thinking, that there must be a better way to solve this whole situation, because surely I can’t be the only one having these problems. And so far I haven’t really seen many people use binding groups for validation with MVVM at all, so it just feels odd.

So, what exactly is the proper way to do validation in WPF with MVVM while being able to check for binding engine exceptions?


My solution (/hack)

First of all, thanks for your input! As I have written above, I’m using IDataErrorInfo already to do my data validation and I personally believe it’s the most comfortable utility to do the validation job. I’m using utilities similar to what Sheridan suggested in his answer below, so maintaining works fine too.

In the end, my problem boiled down to the binding exception issue, where the view model just wouldn’t know about when it happened. While I could handle this with binding groups as detailed above, I still decided against it, as I just didn’t feel all that comfortable with it. So what did I do instead?

As I mentioned above, I detect binding exceptions on the view-side by listening to a binding’s UpdateSourceExceptionFilter. In there, I can get a reference to the view model from the binding expression’s DataItem. I then have an interface IReceivesBindingErrorInformation which registers the view model as a possible receiver for information about binding errors. I then use that to pass the binding path and the exception to the view model:

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
    // ...
 }

In the view model I then remember whenever I am notified about a path’s binding expression:

HashSet<string> bindingErrors = new HashSet<string>();
void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception)
{
    bindingErrors.Add(path);
}

And whenever the IDataErrorInfo revalidates a property, I know that the binding worked, and I can clear the property from the hash set.

In the view model I then can check if the hash set contains any items and abort any action that requires the data to be validated completely. It might not be the nicest solution due to the coupling from the view to the view model, but using that interface it’s at least somewhat less a problem.

解决方案

Warning: Long answer also

I use the IDataErrorInfo interface for validation, but I have customised it to my needs. I think that you'll find that it solves some of your problems too. One difference to your question is that I implement it in my base data type class.

As you pointed out, this interface just deals with one property at a time, but clearly in this day and age, that's no good. So I just added a collection property to use instead:

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

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

To address your problem of not being able to display external errors (in your case from the view, but in mine from the view model), I simply added another collection property:

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

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

I have an HasError property which looks at my collection:

public virtual bool HasError
{
    get { return Errors != null && Errors.Count > 0; }
}

This enables me to bind this to Grid.Visibility using a custom BoolToVisibilityConverter, eg. to show a Grid with a collection control inside that shows the errors when there are any. It also lets me change a Brush to Red to highlight an error (using another Converter), but I guess you get the idea.

Then in each data type, or model class, I override the Errors property and implement the Item indexer (simplified in this example):

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;
    }
}

The AddUniqueIfNotEmpty method is a custom extension method and 'does what is says on the tin'. Note how it will call each property that I want to validate in turn and compile a collection from them, ignoring duplicate errors.

Using the ExternalErrors collection, I can validate things that I can't validate in the data class:

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);
}

To address your point regarding the situation where a user enters an alphabetical character into a int field, I tend to use a custom IsNumeric AttachedProperty for the TextBox, eg. I don't let them make these kinds of errors. I always feel that it's better to stop it, than to let it happen and then fix it.

Overall I'm really happy with my validation ability in WPF and am not left wanting at all.

To end with and for completeness, I felt that I should alert you to the fact that there is now an INotifyDataErrorInfo interface which includes some of this added functionality. You can find out more from the INotifyDataErrorInfo Interface page on MSDN.


UPDATE >>>

Yes, the ExternalErrors property just let's me add errors that relate to a data object from outside that object... sorry, my example wasn't complete... if I'd have shown you the IsGenreNameUnique method, you would have seen that it uses LinQ on all of the Genre data items in the collection to determine whether the object's name is unique or not:

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

As for your int/string problem, the only way I can see you getting those errors in your data class is if you declare all your properties as object, but then you'd have an awful lot of casting to do. Perhaps you could double your properties like this:

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

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

Then if Foo was used in code and FooObject was used in the Binding, you could do this:

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;
    }
}

That way you could fulfil your requirements, but you'll have a lot of extra code to add.

这篇关于适当的验证与MVVM的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆