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


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

这种方法大多数情况下工作正常,但有时候视图需要显示和回发模型属性的一个子集(例如,当用户想要更新他的密码,而不是他的其他配置文件数据时) 。

一个(丑陋的)解决方法是在表单上为每个属性设置一个隐藏的输入字段,而该字段在表单上不存在。

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

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

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

谢谢,

阿德里安

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


6422
2018-01-13 20:19


起源

我也对此感到好奇 - 我最终只是对自定义视图模型进行了验证并将其移出我的域模型。 - Parrots
那么你一遍又一遍地重复验证属性? - Adrian Grigore
我最终不必,不。通常我没有多个页面(因此视图模型)可以设置相同的值(比如一个人的名字)。我必须这样的罕见案例,是的。 - Parrots
不幸的是,我的项目并非如此。例如,我有一个由6种不同形式改变的商业实体。 - Adrian Grigore
Ctrl + F '16,什么都没有。 2016年对此问题的答案有何更新? - Worthy7


答案:


这是您的输入屏幕不应与模型紧密耦合的典型原因。这个问题实际上会在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的接口。最终结果是我有一个继承自接口的User对象 IAdminEdit 和 IUserRegistration。 IAdminEdit和IUserRegistration都包含DataAnnotation属性,这些属性执行所有特定于上下文的验证,如带有接口的Password属性。

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

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

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


7
2017-11-01 17:39



感谢您的全面回复!自从我今年年初写这篇OP以来,我已经尝试了你提到的1)和3)方法的变体。我同意,由于你已经描述过的原因,两者都不太理想。关于方法2:您能否指出我的博客文章或类似资源,描述如何做到这一点?它不必与xVal相关,因为我使用常规的MVC 2模型绑定器和元数据。 - Adrian Grigore


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

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


4
2018-01-17 19:45



那么您是否正在为处理对相同业务实体的更改的视图重复验证属性? - Adrian Grigore
不,我们不对业务实体进行验证属性。不允许业务实体进入无效状态,因此无需在其上放置验证属性。 - Jimmy Bogard
验证是域关注,随域而不是UI而变化。将这种特定于域的逻辑放在UI中会消除这种分离。最多,ViewModels应验证与域无关的“简单要求”,并且不随域更改。例如,必填字段和电子邮件格式。所有其他验证应该被接受(在大多数情况下)作为域中的严格服务器端,没有客户端自动生成支持。 - G-Wiz
@gWiz:我认为他的意思是域对象通过编译时类型安全或实时输入验证来强制执行其有效性,而不是接受无效输入并提供获取“错误”的方法。我倾向于同意这种方法。输入验证和域验证之间存在细微差别,这在MVC中经常被遗忘。 - Aaronaught
我倾向于使用方法来更新成员,而不是在属性设置器中放入大量逻辑。这往往会强制操作顺序(StartDate需要小于EndDate,但如果我先设置EndDate会怎么样?)会变得越来越不透明。验证往往以特定命令为中心,而不是实体。在一个上下文中有效的状态在下一个中可能无效。 - Jimmy Bogard


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

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

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

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


3
2018-01-23 00:36



您正在描述导致我编写此OP的问题。但是你提出什么解决方案?您究竟是什么意思“仅验证与域无关的要求。”?关于编辑器和显示模板元数据,您有什么建议? - Adrian Grigore
域无关的要求是诸如电话号码格式,电子邮件格式,距离(正数),年龄(正整数)等等。这些事情不会随业务而改变 - 它们是系统外部的概念常量。如果视图模型是特定于屏幕的,则可以直接向屏幕添加特定于屏幕的元数据。否则,请使用数据字典。 (C.F. bit.ly/bvL5KM我实际上已经离开ASP.NET MVC一段时间但是当我使用版本1时,我发现MvcContrib中强类型的ViewData扩展是不可或缺的。 - G-Wiz


我不知道这将如何用于客户端验证,但如果部分验证是您的问题,您可以修改 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);
     }
}

2
2018-02-18 04:22



虽然可以通过分支xVal在客户端实现类似的修复,但我不会这样做,因为它依赖于魔术字符串,并且在向模型添加新属性之后很容易忘记在这里添加字符串。 - Adrian Grigore
我同意这不是一个完美的解决方案,但我们通过不断收集字段名称来管理魔术字符串问题,这些字段名称以我们所指的工作流程中的步骤命名。这样,当控制器调用在帖子中执行验证的类时,它会传递您知道需要为该视图验证的字段集合。 - Brandon Linton


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

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

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


0
2018-01-23 00:29



UI取决于域。它不必依赖 直 在域上。几乎任何“报告”都需要一个DTO;同样,大多数表单可能无法完美映射到单个域对象,因此需要专门的UI实例。 DTO和ViewModel转换的逻辑仍然是域模型的一部分。  在任何时候你都没有破坏任何抽象或违反任何不变量。 WPF甚至将此原则结合为MVVM(Model-View-ViewModel)。 - Aaronaught
我认为报告是与业务领域不同的域。单个UI可以统一两个域(取决于两者)。对于强类型视图,我毫不犹豫地将多个域对象组合到一个类中。将ViewModels置于域中是公然违反关注点分离的行为。现在,您无法在不更改域名的情况下更改UI。并且可以说ViewModels可以在支持不同使用场景的不同应用程序/ UI之间重用。 - G-Wiz
顺便说一句,据我所知,在WPF特定的说法中,ViewModel是视图的模型。它是视图特征的抽象。因此,它在UI中固有地定义。这样一个类的要点是允许视图绑定到模型上的属性,这些属性不能直接映射到简单的UI元素(例如下拉列表)。但是根据模式,ViewModel实际上组成了域Model并在UI和Model之间执行绑定转换。 - G-Wiz
gWiz:我完全同意验证规则应理想地驻留在模型上,并且在ViewModel中不会重复。但是,根本不是你根本不需要自定义视图模型。如果您希望保持视图类型安全,那么在很多情况下您绝对必须使用自定义视图模型。对于像NerdDinner这样的示例ASP.NET MVC应用程序,情况可能并非如此,但在具有更复杂用户界面的更大项目中经常发生这种情况。 - Adrian Grigore
我同意@Adrian。我仍然是ASP.NET MVC的新手,但我已经遇到了需要a的示例 ViewModel。一个人:我的 User 包含 Username 和 HashedPassword,但我的“重置密码”视图需要 Password 和 PasswordConfirmation。此处视图和模型之间没有逻辑映射。我用了一个ViewModel 为了验证这两个密码是否匹配并遵循其他一些长度规则等,然后将单个密码传递到存储库,在该存储库中对其进行哈希处理并将其保存在数据库中。怎么做不到a ViewModel? - devuxer