pok*_*oke 57 c# validation wpf mvvm
警告:非常详细的帖子.
好的,在使用MVVM时验证WPF.我现在已经阅读了很多东西,看了很多SO问题,并尝试了很多方法,但是在某些方面我觉得有点哈哈,我真的不确定如何以正确的方式做到这一点.
理想情况下,我希望在视图模型中使用IDataErrorInfo; 这就是我做的.然而,有不同的方面使得该解决方案不是整个验证主题的完整解决方案.
我们采用以下简单形式.如你所见,它没什么特别的.我们只有两个文本框,它们分别绑定到视图模型中的a string和intproperty.此外,我们有一个绑定到的按钮ICommand.

因此,对于验证,我们现在有两个选择:
理想情况下,我想实现选择1.对于激活的普通数据绑定,ValidatesOnDataErrors这是默认行为.因此,当文本更改时,绑定会更新源并触发IDataErrorInfo对该属性的验证; 错误报告回视图.到现在为止还挺好.
有趣的是让视图模型或这种情况下的按钮知道是否有任何错误.方式IDataErrorInfo有效,主要是将错误报告给视图.因此,视图可以轻松查看是否存在任何错误,显示它们甚至显示注释Validation.Errors.此外,验证总是发生在单个属性上.
因此,让视图模型知道何时出现任何错误,或者验证是否成功,是非常棘手的.一个常见的解决方案是简单地触发IDataErrorInfo视图模型本身中所有属性的验证.这通常使用单独的IsValid属性来完成.好处是,这也可以很容易地用于禁用命令.缺点是这可能会经常对所有属性进行验证,但大多数验证应该足够简单,不会影响性能.另一个解决方案是记住哪些属性使用验证产生错误并且只检查那些,但这似乎有点过于复杂并且大多数时候都是不必要的.
最重要的是,这可以正常工作.IDataErrorInfo为所有属性提供验证,我们可以简单地在视图模型本身中使用该接口来为整个对象运行验证.介绍问题:
视图模型使用实际类型作为其属性.所以在我们的例子中,整数属性是实际的int.视图中使用的文本框本身仅支持文本.因此,当绑定到int视图模型时,数据绑定引擎将自动执行类型转换 - 或者至少它会尝试.如果你可以在一个用于数字的文本框中输入文本,那么内部不会总是存在有效数字的可能性很高:所以数据绑定引擎将无法转换并抛出一个FormatException.

从观点来看,我们可以很容易地看到这一点.来自绑定引擎的异常会被WPF自动捕获并显示为错误 - 甚至不需要启用Binding.ValidatesOnExceptionssetter中抛出的异常所需的异常.错误消息确实有一个通用文本,所以这可能是一个问题.我通过使用Binding.UpdateSourceExceptionFilter处理程序,检查抛出的异常并查看source属性然后生成一个不太通用的错误消息来解决这个问题.所有这些都封装在我自己的Binding标记扩展中,所以我可以拥有我需要的所有默认值.
所以观点很好.用户出错,看到一些错误反馈并可以纠正它.然而,视图模型丢失了.当绑定引擎抛出异常时,源从未更新过.因此,视图模型仍然是旧值,这不是向用户显示的内容,并且IDataErrorInfo验证显然不适用.
更糟糕的是,视图模型没有很好的方法来了解这一点.至少,我还没有找到一个好的解决方案.可能的是让视图报告返回到视图模型,表明存在错误.这可以通过将Validation.HasError属性绑定到视图模型(这是不可能直接)的数据来完成,因此视图模型可以首先检查视图的状态.
另一种选择是将处理的异常中继Binding.UpdateSourceExceptionFilter到视图模型中,因此也会通知它.视图模型甚至可以为绑定提供一些接口来报告这些内容,允许自定义错误消息而不是通用的每类型消息.但是这会产生从视图到视图模型的更强耦合,我通常希望避免这种耦合.
另一个"解决方案"是删除所有类型属性,使用普通string属性并在视图模型中进行转换.这显然会将所有验证都移到视图模型中,但也意味着数据绑定引擎通常会处理大量重复的事情.此外,它将改变视图模型的语义.对我来说,视图是为视图模型而不是反过来构建的 - 当然视图模型的设计取决于我们想象的视图,但是视图如何做到这一点仍然是一般的自由.所以视图模型定义了一个int属性,因为有一个数字; 视图现在可以使用文本框(允许所有这些问题),或使用本机与数字一起使用的东西.所以不,改变属性的类型string 对我来说不是一个选择.
最后,这是一个观点的问题.视图(及其数据绑定引擎)负责为视图模型提供适当的值.但在这种情况下,似乎没有好的方法告诉视图模型它应该使旧的属性值无效.
绑定组是我试图解决这个问题的一种方式.绑定组能够对所有验证进行分组,包括IDataErrorInfo抛出异常.如果视图模型可用,它们甚至可以检查所有这些验证源的验证状态,例如使用CommitEdit.
默认情况下,绑定组实现上面的选择2.它们使绑定明确更新,实质上是添加了一个额外的未提交状态.因此,当单击该按钮时,该命令可以提交这些更改,触发源更新和所有验证,并在成功时获得单个结果.所以命令的动作可能是这样的:
if (bindingGroup.CommitEdit())
SaveEverything();
Run Code Online (Sandbox Code Playgroud)
CommitEdit只有在所有验证成功后才会返回true .它将IDataErrorInfo考虑并检查绑定异常.这似乎是一个完美的选择解决方案2.唯一有点麻烦的是用绑定来管理绑定组,但我自己构建了一些主要负责这个(相关)的东西.
如果绑定组存在绑定,则绑定将默认为显式UpdateSourceTrigger.要使用绑定组从上面实现选择1,我们基本上必须更改触发器.因为我有一个自定义绑定扩展,这很简单,我只是LostFocus为所有人设置它.
所以现在,只要文本字段发生变化,绑定仍会更新.如果源可以更新(绑定引擎不会抛出异常),那么IDataErrorInfo将照常运行.如果无法更新,视图仍然可以看到它.如果我们单击我们的按钮,底层命令可以调用CommitEdit(虽然不需要提交任何内容)并获得总验证结果以查看它是否可以继续.
我们可能无法通过这种方式轻松禁用按钮.至少不是来自视图模型.一遍又一遍地检查验证只是更新命令状态并不是一个好主意,并且无论如何抛出绑定引擎异常时都不会通知视图模型(这应该会禁用该按钮) - 或者当它消失时再次启用该按钮.我们仍然可以添加一个触发器来禁用视图中的按钮,Validation.HasError因此这并非不可能.
总的来说,这似乎是一个完美的解决方案.但是我的问题是什么?说实话,我并不完全确定.绑定组是一个复杂的事情,似乎通常在较小的组中使用,可能在单个视图中具有多个绑定组.通过在整个视图中使用一个大的绑定组来确保我的验证,感觉好像我在滥用它.我一直在想,必须有更好的方法来解决这一问题,因为我肯定不会是唯一有这些问题的人.到目前为止,我还没有真正看到很多人使用绑定组进行MVVM验证,所以感觉很奇怪.
那么,在能够检查绑定引擎异常的情况下,使用MVVM在WPF中进行验证的正确方法究竟是什么?
首先,感谢您的投入!正如我上面所写,我已经在使用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
// ...
}
Run Code Online (Sandbox Code Playgroud)
在视图模型中,我记得每当我收到有关路径绑定表达式的通知时:
HashSet<string> bindingErrors = new HashSet<string>();
void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception)
{
bindingErrors.Add(path);
}
Run Code Online (Sandbox Code Playgroud)
每当IDataErrorInfo重新验证属性时,我知道绑定工作,我可以从哈希集中清除属性.
然后,在视图模型中,我可以检查哈希集是否包含任何项,并中止任何需要完全验证数据的操作.由于从视图模型到视图模型的耦合,它可能不是最好的解决方案,但是使用该接口它至少在某种程度上不是问题.
She*_*dan 17
警告:也回答很长
我使用IDataErrorInfo界面进行验证,但我已根据自己的需要对其进行了定制.我想你会发现它也解决了你的一些问题.您的问题的一个不同之处是我在基础数据类型类中实现它.
正如你所指出的,这个界面一次只处理一个属性,但显然在这个时代,这并不好.所以我只是添加了一个集合属性来代替:
protected ObservableCollection<string> errors = new ObservableCollection<string>();
public virtual ObservableCollection<string> Errors
{
get { return errors; }
}
Run Code Online (Sandbox Code Playgroud)
为了解决您无法显示外部错误的问题(在您的情况下从视图中,但在我的视图模型中),我只是添加了另一个集合属性:
protected ObservableCollection<string> externalErrors = new ObservableCollection<string>();
public ObservableCollection<string> ExternalErrors
{
get { return externalErrors; }
}
Run Code Online (Sandbox Code Playgroud)
我有一个HasError查看我的收藏的属性:
public virtual bool HasError
{
get { return Errors != null && Errors.Count > 0; }
}
Run Code Online (Sandbox Code Playgroud)
这使我能够将其绑定到Grid.Visibility使用自定义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;
}
}
Run Code Online (Sandbox Code Playgroud)
该AddUniqueIfNotEmpty方法是一种自定义extension方法,并且"在锡上做了什么".请注意它将如何调用我想依次验证的每个属性并从中编译一个集合,忽略重复的错误.
使用该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);
}
Run Code Online (Sandbox Code Playgroud)
为了解决关于其中一个用户输入一个字母字符成的情况你的观点int场,我倾向于使用自定义IsNumeric AttachedProperty的TextBox,如.我不会让他们犯这些错误.我总觉得最好是阻止它,而不是让它发生然后修复它.
总的来说,我对WPF中的验证能力感到非常满意,并且根本不需要.
为了结束并且为了完整性,我觉得我应该提醒您现在有一个INotifyDataErrorInfo界面包含一些这样的附加功能.您可以从MSDN上的" INotifyDataErrorInfo接口"页面中找到更多信息.
更新>>>
是的,该ExternalErrors属性只是让我添加与该对象外部的数据对象相关的错误...对不起,我的示例不完整...如果我已经向您展示了该IsGenreNameUnique方法,您会看到它使用LinQ在所有的的Genre集合中的数据项来确定物体的名称是否是唯一的或者不是:
private bool IsGenreNameUnique(Genre genre)
{
return Genres.Where(d => d.Name != string.Empty && d.Name == genre.Name).Count() == 1;
}
Run Code Online (Sandbox Code Playgroud)
至于你的int/ string问题,我能看到你在你的数据类中得到这些错误的唯一方法就是你声明你所有的属性object,但是你需要进行大量的转换.也许你可以将你的属性翻倍:
public object FooObject { get; set; } // Implement INotifyPropertyChanged
public int Foo
{
get { return FooObject.GetType() == typeof(int) ? int.Parse(FooObject) : -1; }
}
Run Code Online (Sandbox Code Playgroud)
然后,如果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;
}
}
Run Code Online (Sandbox Code Playgroud)
这样你就可以满足你的要求,但你需要添加很多额外的代码.