C# 使用 MVVM 进行正确验证
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/19498485/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Proper validation with MVVM
提问by poke
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 manyapproaches, but everything feels somewhat hacky at some point and I'm really not sure how to do it the right way?.
好的,使用 MVVM 时在 WPF 中进行验证。我现在已经阅读了很多东西,看了很多 SO 问题,并尝试了很多方法,但在某些时候一切都感觉有些不自然,我真的不知道如何以正确的方式去做?
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.
理想情况下,我希望使用IDataErrorInfo
;在视图模型中进行所有验证。所以这就是我所做的。然而,有不同的方面使该解决方案不是整个验证主题的完整解决方案。
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
.
让我们采用以下简单的形式。正如你所看到的,这没什么特别的。我们只有两个文本框,每个文本框都绑定到视图模型中的string
和int
属性。此外,我们有一个绑定到ICommand
.
So for the validation we now have a two choices:
所以对于验证我们现在有两个选择:
- 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.
- 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.
理想情况下,我想实现选择 1。对于激活的普通数据绑定,ValidatesOnDataErrors
这是默认行为。因此,当文本更改时,绑定会更新源并触发IDataErrorInfo
对该属性的验证;错误被报告回视图。到现在为止还挺好。
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.
有趣的一点是让视图模型或本例中的按钮知道是否有任何错误。方法IDataErrorInfo
有效,主要是将错误报告回视图。因此,视图可以轻松查看是否有任何错误,显示它们,甚至使用Validation.Errors
. 此外,验证总是在查看单个属性时发生。
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.
所以让视图模型知道什么时候有任何错误,或者验证是否成功,是很棘手的。一个常见的解决方案是简单地触发IDataErrorInfo
视图模型本身中所有属性的验证。这通常使用单独的IsValid
属性来完成。好处是这也可以很容易地用于禁用命令。缺点是这可能会过于频繁地对所有属性运行验证,但大多数验证应该足够简单,不会损害性能。另一种解决方案是使用验证记住哪些属性产生了错误并只检查这些属性,但这在大多数情况下似乎有点过于复杂且不必要。
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:
最重要的是,这可以正常工作。IDataErrorInfo
提供对所有属性的验证,我们可以简单地在视图模型本身中使用该接口来运行整个对象的验证。问题介绍:
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
.
视图模型为其属性使用实际类型。所以在我们的例子中,整数属性是一个实际的int
. 然而,视图中使用的文本框本身只支持text。所以当绑定到int
视图模型中时,数据绑定引擎将自动执行类型转换——或者至少它会尝试。如果您可以在用于数字的文本框中输入文本,则内部并不总是有效数字的可能性很高:因此数据绑定引擎将无法转换并抛出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.
在视图方面,我们可以很容易地看到这一点。来自绑定引擎的异常会被 WPF 自动捕获并显示为错误 — 甚至不需要启用Binding.ValidatesOnExceptions
对于在 setter 中抛出的异常来说是必需的。错误消息确实有一个通用文本,所以这可能是一个问题。我已经通过使用Binding.UpdateSourceExceptionFilter
处理程序为自己解决了这个问题,检查抛出的异常并查看源属性,然后生成一个不太通用的错误消息。所有这些都封装在我自己的 Binding 标记扩展中,所以我可以拥有我需要的所有默认值。
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.
所以景色还不错。用户犯了一个错误,看到一些错误反馈并可以纠正它。然而,视图模型丢失了。由于绑定引擎抛出异常,源从未更新。所以视图模型仍然在旧值上,这不是向用户显示的,并且IDataErrorInfo
验证显然不适用。
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.
更糟糕的是,视图模型没有好的方法可以知道这一点。至少,我还没有找到一个好的解决方案。有可能是让视图向视图模型报告出现错误。这可以通过将Validation.HasError
属性数据绑定回视图模型来完成(这是不可能直接实现的),因此视图模型可以首先检查视图的状态。
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.
另一种选择是将处理的异常中继Binding.UpdateSourceExceptionFilter
到视图模型,因此它也会收到通知。视图模型甚至可以为绑定提供一些接口来报告这些事情,允许自定义错误消息而不是通用的每个类型的错误消息。但这会创建从视图到视图模型的更强耦合,我通常希望避免这种情况。
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.
另一个“解决方案”是去掉所有类型的属性,使用普通string
属性并在视图模型中进行转换。这显然会将所有验证转移到视图模型,但也意味着数据绑定引擎通常会处理大量重复的事情。此外,它会改变视图模型的语义。对我来说,视图是为视图模型构建的,而不是相反——当然,视图模型的设计取决于我们想象视图要做什么,但视图如何做到这一点仍然存在普遍的自由。所以视图模型定义了一个int
属性,因为有一个数字;视图现在可以使用文本框(允许所有这些问题),或者使用本机处理数字的东西。所以不,将属性的类型更改为string
对我来说不是一个选择。
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 groupsare 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 allof those validation sources, for example using CommitEdit
.
绑定组是我试图解决这个问题的一种方法。绑定组能够对所有验证进行分组,包括IDataErrorInfo
异常和抛出的异常。如果视图模型可用,它们甚至可以检查所有这些验证源的验证状态,例如使用CommitEdit
.
By default, binding groups implement choice 2 from above. They make the bindings update explicitly, essentially adding an additional uncommittedstate. So when clicking the button, the command can committhose changes, trigger the source updates and all validations and get a single result if it succeeded. So the command's action could be this:
默认情况下,绑定组实现上面的选项 2。它们使绑定显式更新,本质上添加了一个额外的未提交状态。因此,当单击按钮时,该命令可以提交这些更改,触发源更新和所有验证,如果成功则获得单个结果。所以命令的动作可能是这样的:
if (bindingGroup.CommitEdit())
SaveEverything();
CommitEdit
will only return true if allvalidations 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).
CommitEdit
只有在所有验证都成功时才会返回 true 。它将IDataErrorInfo
考虑并检查绑定异常。这似乎是选项 2 的完美解决方案。唯一有点麻烦的是用绑定管理绑定组,但我已经为自己构建了一些主要负责这个的东西(相关)。
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.
如果绑定存在绑定组,则绑定将默认为显式UpdateSourceTrigger
。要使用绑定组实现上面的选择 1,我们基本上必须更改触发器。因为我有一个自定义绑定扩展,所以这很简单,我只是将它设置LostFocus
为所有人。
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.
所以现在,只要文本字段发生变化,绑定仍然会更新。如果源可以更新(绑定引擎不会抛出异常),则将IDataErrorInfo
照常运行。如果无法更新,视图仍然可以看到它。如果我们点击我们的按钮,底层命令可以调用CommitEdit
(虽然不需要提交任何内容)并获得总验证结果,看看它是否可以继续。
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.
我们可能无法通过这种方式轻松禁用该按钮。至少不是来自视图模型。一遍又一遍地检查验证并不是一个好主意,只是为了更新命令状态,并且当绑定引擎异常被抛出时(这应该禁用按钮)或当它消失时不会通知视图模型再次启用按钮。我们仍然可以添加一个触发器来禁用视图中的按钮,Validation.HasError
所以这并非不可能。
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.
所以总的来说,这似乎是完美的解决方案。我的问题是什么?老实说,我并不完全确定。绑定组是一个复杂的东西,似乎通常在较小的组中使用,可能在单个视图中有多个绑定组。通过对整个视图使用一个大绑定组来确保我的验证,感觉好像我在滥用它。我一直在想,必须有更好的方法来解决整个情况,因为肯定不会只有我一个人遇到这些问题。到目前为止,我还没有真正看到很多人使用绑定组来验证 MVVM,所以感觉很奇怪。
So, what exactly is the proper way to do validation in WPF with MVVM while being able to check for binding engine exceptions?
那么,在能够检查绑定引擎异常的同时,使用 MVVM 在 WPF 中进行验证的正确方法究竟是什么?
My solution (/hack)
我的解决方案(/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.
首先,感谢您的投入!正如我上面所写的,我已经使用它IDataErrorInfo
来进行我的数据验证,我个人认为这是进行验证工作最舒适的实用程序。我正在使用类似于 Sheridan 在下面的回答中建议的实用程序,因此维护工作也很好。
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:
正如我上面提到的,我通过侦听绑定的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
// ...
}
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.
每当IDataErrorInfo
重新验证一个属性时,我就知道绑定有效,我可以从哈希集中清除该属性。
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.
在视图模型中,我可以检查散列集是否包含任何项目并中止任何需要完全验证数据的操作。由于从视图到视图模型的耦合,它可能不是最好的解决方案,但使用该接口至少不是一个问题。
采纳答案by Sheridan
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.
我使用该IDataErrorInfo
界面进行验证,但我已根据自己的需要对其进行了自定义。我想你会发现它也解决了你的一些问题。与您的问题的一个区别是我在我的基本数据类型类中实现了它。
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:
我有一个HasError
属性可以查看我的收藏:
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.
这使我能够将其绑定到Grid.Visibility
使用自定义BoolToVisibilityConverter
,例如。显示一个Grid
里面有一个集合控件,当有错误时显示错误。它还可以让我更改 aBrush
以Red
突出显示错误(使用 another Converter
),但我想您明白了。
Then in each data type, or model class, I override the Errors
property and implement the Item
indexer (simplified in this example):
然后在每个数据类型或模型类中,我覆盖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;
}
}
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.
该AddUniqueIfNotEmpty
方法是一种自定义extension
方法,并且“按照罐头上的说明执行操作”。请注意它将如何依次调用我想要验证的每个属性并从中编译一个集合,而忽略重复的错误。
Using the ExternalErrors
collection, I can validate things that I can't validate in the data class:
使用该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);
}
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.
为了解决关于其中一个用户输入一个字母字符成的情况你的观点int
场,我倾向于使用自定义IsNumeric AttachedProperty
的TextBox
,如。我不会让他们犯这种错误。我总觉得与其让它发生然后再修复它,不如阻止它。
Overall I'm really happy with my validation ability in WPF and am not left wanting at all.
总的来说,我对我在 WPF 中的验证能力非常满意,并且一点也不想要。
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
Interfacepage on MSDN.
最后,为了完整起见,我觉得我应该提醒您注意,现在有一个INotifyDataErrorInfo
界面,其中包含一些附加功能。您可以从MSDN 上的INotifyDataErrorInfo
接口页面找到更多信息。
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 allof the Genre
data items in the collection to determine whether the object's name is unique or not:
是的,该ExternalErrors
属性只是让我从该对象外部添加与数据对象相关的错误...抱歉,我的示例不完整...如果我向您展示了该IsGenreNameUnique
方法,您会看到它使用LinQ
在所有的的Genre
集合中的数据项来确定物体的名称是否是唯一的或者不是:
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 thoseerrors 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:
至于您的int
/string
问题,我可以看到您在数据类中收到这些错误的唯一方法是,如果您将所有属性声明为object
,但是您将有大量的转换要做。也许你可以像这样将你的属性加倍:
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:
然后,如果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;
}
}
That way you could fulfil your requirements, but you'll have a lot of extra code to add.
这样您就可以满足您的要求,但是您将需要添加很多额外的代码。
回答by nmclean
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 unecessary for most times.
缺点是这可能会过于频繁地对所有属性运行验证,但大多数验证应该足够简单,不会损害性能。另一种解决方案是使用验证记住哪些属性产生了错误并只检查这些属性,但这在大多数情况下似乎有点过于复杂和不必要。
You don't need to track which properties have errors; you only need to know that errors exist. The view model can maintain a list of errors (also useful for displaying an error summary), and the IsValid
property can simply be a reflection of whether the list has anything. You don't need to check everything each time IsValid
is called, as long as you ensure that the error summary is current and that IsValid
is refreshed each time it changes.
您不需要跟踪哪些属性有错误;你只需要知道错误存在。视图模型可以维护一个错误列表(对于显示错误摘要也很有用),并且该IsValid
属性可以简单地反映列表是否有任何内容。您不需要在每次IsValid
调用时都检查所有内容,只要您确保错误摘要是最新的并且IsValid
每次更改时都会刷新。
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.
归根结底,这是视图的问题。视图(及其数据绑定引擎)负责为视图模型提供适当的值以供使用。但是在这种情况下,似乎没有好的方法告诉视图模型它应该使旧的属性值无效。
You can listen to errors within the container that is bound to the view model:
您可以侦听绑定到视图模型的容器内的错误:
container.AddHandler(Validation.ErrorEvent, Container_Error);
...
void Container_Error(object sender, ValidationErrorEventArgs e) {
...
}
This notifies you when errors are added or removed, and you can identify binding exceptions by whether e.Error.Exception
exists, so your view can maintain a list of binding exceptions and inform the view model of it.
这会在添加或删除错误时通知您,并且您可以通过是否e.Error.Exception
存在来识别绑定异常,因此您的视图可以维护一个绑定异常列表并将其通知视图模型。
But any solution to this problem will always be a hack, because the view is not filling its role properly, which is giving the user a means of reading and updating the view model structure. This should be seen as a temporary solution until you correctly present the user with some kind of "integerbox" instead of a textbox.
但是这个问题的任何解决方案都将永远是一个黑客,因为视图没有正确地填充它的角色,这给了用户一种阅读和更新视图模型结构的方法。这应该被视为一种临时解决方案,直到您正确地向用户呈现某种“整数框”而不是文本框。
回答by G.Y
Ok, I believe I have found the answer you were looking for...
It will not be easy to explain - but..
Very easy to understand once explained...
I do think It is most accurate/"certified" to MVVM viewed as "standard" or at the least attempted standard.
好的,我相信我已经找到了您正在寻找的答案......
这并不容易解释 - 但是......
一旦解释就很容易理解......
我确实认为这是最准确/“认证”的 MVVM 查看作为“标准”或至少是尝试的标准。
But before we begin.. you need to change a concept which you got used to regarding MVVM:
但在我们开始之前……你需要改变一个你习惯的关于 MVVM 的概念:
"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"
“此外,它会改变视图模型的语义。对我来说,视图是为视图模型构建的,而不是相反——当然,视图模型的设计取决于我们想象视图要做的事情,但仍然有一般性自由观点如何做到这一点”
That paragraph is the source of your problem.. - why?
那段是你问题的根源.. - 为什么?
Because you are stating the View-Model has no role to adjust itself to the View..
That is wrong in many ways - as I'll prove to you very simply..
因为您说 View-Model 没有将自身调整到 View 的作用..
这在很多方面都是错误的 - 我将非常简单地向您证明..
If you have a property such as:
如果你有这样的财产:
public Visibility MyPresenter { get...
public Visibility MyPresenter { get...
What is Visibility
if not something that serves the View?
The type itself and the name that will be given to the property is definitely made up for the view.
是什么Visibility
,如果不是,供应有何看法?
类型本身和将赋予属性的名称绝对是为视图构成的。
There two distinguishable View-Models categories in MVVM according to my experience:
根据我的经验,MVVM 中有两个可区分的 View-Models 类别:
- Presenter View Model - which is to be hooked to buttons, menus, Tab Items etc....
- Entity View Model - which is to be hocked to controls that brings the entity data to screen.
- 演示者视图模型 - 将连接到按钮、菜单、选项卡项目等......
- 实体视图模型 - 与将实体数据显示在屏幕上的控件相关联。
These are two different - completely different concerns.
这是两个不同的——完全不同的关注点。
And now to the solution:
现在解决方案:
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;
}
}
}
And now somewhere in your code - your button command 'CanExecute' method can add to its implementation a call to VmEntity.HasErrors.
现在在您的代码中的某个地方 - 您的按钮命令“CanExecute”方法可以向其实现添加对 VmEntity.HasErrors 的调用。
And may peace be upon your code regarding validation from now on :)
从现在开始,您关于验证的代码可能会平安:)
回答by Hemant
In my opinion, the problem lies in validation happening at too many places. I also wished to write all my validation login in ViewModel
but all those number binding were making my ViewModel
crazy.
在我看来,问题在于验证发生在太多地方。我还希望写下我所有的验证登录信息,ViewModel
但所有这些号码绑定都让我ViewModel
发疯。
I solved this problem by creating a binding that never fails. Obviously, if a binding is always successful then the type itself has to handle the error conditions gracefully.
我通过创建一个永不失败的绑定解决了这个问题。显然,如果绑定总是成功,那么类型本身必须优雅地处理错误条件。
Failable Value Type
可失败值类型
I started by creating a generic type which would gracefully support the failed conversions:
我首先创建了一个通用类型,它可以优雅地支持失败的转换:
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;
}
}
}
Note that even if the type fails to initialise because of invalid input string (second constructor), it quietly stores the invalid state along with invalid textalso. This is required in order to support the round-trip of binding even in case of wrong input.
请注意,即使类型由于无效输入字符串(第二个构造函数)而无法初始化,它也会悄悄地将无效状态与无效文本一起存储。这是必需的,以便即使在错误输入的情况下也支持绑定的往返。
Generic Value Converter
通用值转换器
A generic value converter could be written using above type:
可以使用上述类型编写通用值转换器:
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 Handy Converters
XAML 便捷转换器
Since creating and using the instances of generics is pain in XAML, lets make static instances of common converters:
由于在 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>();
}
}
Other value types can be extended easily.
其他值类型可以轻松扩展。
Usage
用法
Usage is pretty simple, just need to change the type from int
to Failable<int>
:
用法很简单,只需要将类型从int
改为Failable<int>
:
ViewModel
视图模型
public Failable<int> NumberValue
{
//Custom logic along with validation
//using IsValid property
}
XAML
XAML
<TextBox Text="{Binding NumberValue,Converter={x:Static local:Failable.Int32Converter}}"/>
This way, you can use the same validation mechanism (IDataErrorInfo
or INotifyDataErrorInfo
or anything else) in ViewModel
by checking the IsValid
property. If IsValid
is true, you can directly use the Value
.
这样,您可以通过检查属性来使用相同的验证机制(IDataErrorInfo
或INotifyDataErrorInfo
其他任何机制)。如果为真,则可以直接使用.ViewModel
IsValid
IsValid
Value
回答by Richard Moore
Here's an effort to simplify things if you don't want to implement tons of additional code...
如果您不想实现大量额外的代码,这里有一个简化事情的努力......
The scenario is that you have an int property in your viewmodel (could be decimal or another non-string type) and you bind a textbox to it in your view.
场景是您的视图模型中有一个 int 属性(可以是十进制或其他非字符串类型),并且您在视图中将文本框绑定到它。
You have validation in your viewmodel that fires in the setter of the property.
您的视图模型中有在属性的 setter 中触发的验证。
In the view a user enters 123abc and the view logic highlights the error in the view, but can't set the property because the value is the wrong type. The setter never gets called.
在视图中,用户输入 123abc,视图逻辑突出显示视图中的错误,但无法设置属性,因为值类型错误。setter 永远不会被调用。
The simplest solution is to change your int property in the viewmodel to be a string property, and cast the values into and out of it from the model. This allows the bad text to hit the setter of your property, and your validation code can then check the data and reject it as appropriate.
最简单的解决方案是将 viewmodel 中的 int 属性更改为字符串属性,并将值从模型中输入和输出。这允许错误文本命中您的属性的设置器,然后您的验证代码可以检查数据并酌情拒绝它。
IMHO validation in WPF is broken, as can be seen from the elaborate (and ingenious) ways people have tried to work around the problem given previously. For me I don't want to add a huge amount of extra code or implement my own type classes to enable a textbox to validate so basing these properties on strings is something I can live with, even if it does feel like a bit of a kludge.
恕我直言,WPF 中的验证已被破坏,这可以从人们试图解决先前给出的问题的精心(和巧妙)方式中看出。对我来说,我不想添加大量额外的代码或实现我自己的类型类来启用文本框进行验证,因此将这些属性基于字符串是我可以接受的,即使它确实感觉有点像混杂。
Microsoft should look at fixing this so that the scenario of invalid user input in a textbox bound to an int or decimal property can somehow communicate this fact elegantly to the viewmodel. It should be possible, for instance for them to create a new bound property for a XAML control to communicate view logic validation errors to a property in the viewmodel.
微软应该考虑修复这个问题,以便在绑定到 int 或 decimal 属性的文本框中的无效用户输入的场景可以以某种方式优雅地将此事实传达给视图模型。例如,他们应该可以为 XAML 控件创建新的绑定属性,以便将视图逻辑验证错误传达给视图模型中的属性。
Thanks and respect to the other guys that have provided detailed answers to this topic.
感谢并尊重为该主题提供详细答案的其他人。