在自定义ViewModel中重用验证属性

时间:2010-01-13 20:19:35

标签: asp.net-mvc validation architecture automapper xval

当我开始使用xVal进行客户端验证时,我只实现了使用域模型对象作为viewmodel或viewmodel中这些对象的嵌入式实例的操作方法。

这种方法在大多数情况下都能正常工作,但有些情况下视图需要显示并仅回发模型属性的一部分(例如,当用户想要更新密码时,而不是其他人的密码个人资料)。

一个(丑陋的)解决方法是在表单上为表单上没有的每个属性都有一个隐藏的输入字段。

显然,此处的最佳做法是创建一个自定义视图模型,该视图模型仅包含与视图相关的属性,并通过Automapper填充视图模型。它更清晰,因为我只传输与视图相关的数据,但它远非完美,因为我必须重复已经存在于域模型对象上的相同验证属性。

理想情况下,我想通过MetaData属性将域模型对象指定为元类(这通常也称为“伙伴类”),但这不起作用,因为当元数据类具有xVal抛出时viewmodel上不存在的属性。

这有什么优雅的解决方法吗?我一直在考虑攻击xVal源代码,但也许还有其他一些方法我到目前为止都忽略了。

谢谢,

阿德里安

编辑:随着ASP.NET MVC 2的到来,这不仅仅是与验证属性相关的问题,而且它也适用于编辑器和显示属性。

5 个答案:

答案 0 :(得分:7)

这是您的输入屏幕不应与您的模型紧密耦合的典型原因。这个问题实际上会在MVC标签上弹出一个月大约3-4次。如果我能找到上一个问题并且这里的一些评论讨论很有意思,我会欺骗。 ;)

您遇到的问题是您试图将模型的两个不同验证上下文强制转换为在大量场景下失败的单个模型。最好的示例是注册新用户,然后让管理员稍后编辑用户字段。您需要在注册期间验证用户对象上的密码,但不会向管理员显示密码字段以编辑用户详细信息。

绕过这些的选择都是次优的。我现在已经为3个项目解决了这个问题,并且实施以下解决方案从未干净过,而且通常令人沮丧。我将尝试实用并忘记其他人正在进行的所有DDD / db / model / hotness讨论。

1)多视图模型  拥有几乎相同的视图模型违反了DRY原则,但我觉得这种方法的成本非常低。通常违反DRY放大维护成本,但恕我直言,这是最低的,并不是很多。假设您不会更改LastName字段可以经常使用的最大数字字符数。

2)动态元数据 MVC 2中有一些钩子,用于为模型提供自己的元数据。使用这种方法,您可以使用任何用于提供元数据的内容,根据当前的HTTPRequest以及Action和Controller排除某些字段。我已经使用这种技术构建了一个数据库驱动的权限系统,该系统进入数据库并告诉DataAnnotationsMetadataProvider的子类排除存储在数据库中的属性。

这项技术运行良好,但唯一的问题是使用UpdateModel()进行验证。为了解决这个问题,我们创建了一个SmartUpdateModel()方法,该方法也会进入数据库并自动生成exclude string []数组,以便不验证任何不允许的字段。我们当然是出于性能原因而缓存它,所以它还不错。

只是想重申我们在模型上使用[ValidationAttributes],然后用运行时的新规则取代它们。最终结果是,如果用户没有访问权限,则[Required] User.LastName字段未经过验证。

3)疯狂接口动态代理服务器 我尝试的最后一种技术是使用ViewModels的接口。最终结果是我有一个继承自IAdminEditIUserRegistration等接口的User对象。 IAdminEdit和IUserRegistration都包含DataAnnotation属性,这些属性执行所有特定于上下文的验证,如带有接口的Password属性。

这需要一些hackery,而且比其他任何东西都更像是学术活动。 2和3的问题是需要自定义UpdateModel和DataAnnotationsAttribute提供程序以使其了解此技术。

我最大的绊脚石是我不想将整个用户对象发送到视图,因此我最终使用动态代理来创建IAdminEdit的运行时实例

现在我明白这是一个非常xVal的具体问题,但是所有通往动态验证的道路都会导致内部MVC元数据提供商的定制。由于所有元数据都是新的,因此此时没有什么是干净或简单的。您需要做的工作来定制MVC的验证行为并不难,但需要深入了解所有内部工作原理。

答案 1 :(得分:4)

我们将验证属性移至ViewModel图层。在我们的案例中,无论如何,这提供了更清晰的关注点分离,因为我们随后能够设计我们的域模型,使其无法首先进入无效状态。例如,BillingTransaction对象可能需要Date。所以我们不想让它成为Nullable。但是在我们的ViewModel上,我们可能需要公开Nullable,以便我们可以捕获用户没有输入值的情况。

在其他情况下,您可能需要针对每个页面/表单进行特定验证,并且您希望根据用户尝试执行的命令进行验证,而不是设置一堆内容并询问域模型, “你是否有效尝试做XYZ”,在做“ABC”时这些值是有效的。

答案 2 :(得分:3)

如果假设ViewModel被强加给你,那么我建议他们只强制执行与域无关的要求。这包括“需要用户名”和“正确格式化电子邮件”等内容。

如果您从视图模型中的域模型复制验证,那么您已将域紧密耦合到UI。当域验证更改(“每周只能应用2张优惠券”变为“每周只能应用1张优惠券”)时,必须更新UI。一般来说,这会很糟糕,并且不利于敏捷。

如果您将验证从域模型移到UI,您实际上已经毁掉了您的域并将验证的责任放在了UI上。第二个UI必须复制所有验证,并且您将两个单独的UI耦合在一起。现在,如果客户想要一个特殊的界面来管理他们的iPhone库存,那么iPhone项目需要复制网站UI中也可以找到的所有验证。 这比上面描述的验证重复更加糟糕。

除非您可以预测未来并且可以排除这些可能性,否则只能验证与域无关的要求。

答案 3 :(得分:2)

我不知道这将如何用于客户端验证,但如果部分验证是您的问题,您可以修改此处讨论的DataAnnotationsValidationRunner以获取IEnumerable<string>属性名称列表,如下:

public static class DataAnnotationsValidationRunner
{
     public static IEnumerable<ErrorInfo> GetErrors(object instance, IEnumerable<string> fieldsToValidate)
     {
           return from prop in TypeDescriptor.GetProperties(instance).Cast<PropertyDescriptor>().Where(p => fieldsToValidate.Contains(p.Name))
                  from attribute in prop.Attributes.OfType<ValidationAttribute>()
                  where !attribute.IsValid(prop.GetValue(instance))
                  select new ErrorInfo(prop.Name, attribute.FormatErrorMessage(string.Empty), instance);
     }
}

答案 4 :(得分:0)

我要冒这个downvotes的风险,并声明ViewModel(在ASP.NET MVC中)没有任何好处,特别是考虑到创建和维护它们的开销。如果想法是从域中解耦,那是不可原谅的。与域分离的UI不是该域的UI。 UI 必须依赖于域,因此您要么将视图/操作耦合到域模型,要么将ViewModel管理逻辑耦合到域模型。因此架构论证没有实际意义。

如果想要阻止用户攻击利用ASP.NET MVC模型绑定到变异字段的恶意HTTP POST,则不应允许他们更改,那么A)域应该强制执行此要求,并且B)这些操作应该为模型绑定器提供可更新属性的白名单。

除非你的域暴露出像现场内存对象图而不是实体副本一样疯狂的东西,否则ViewModel会浪费精力。因此,要回答您的问题,请在域模型中保留域验证。