【问题标题】:Reusing validation attributes in custom ViewModels在自定义 ViewModel 中重用验证属性
【发布时间】:2010-01-13 20:19:34
【问题描述】:

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

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

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

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

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

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

谢谢,

阿德里安

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

【问题讨论】:

  • 我对此也很好奇——我最终只是对自定义视图模型进行了验证并将其移出我的域模型。
  • 所以你一遍又一遍地重复验证属性?
  • 我最终不必这样做,不。通常我没有多个页面(因此也没有视图模型)可以设置相同的值(比如一个人的名字)。极少数情况下,我必须这样做。
  • 不幸的是,在我的项目中并非如此。例如,我有一个业务实体正在被 6 种不同的形式改变。
  • Ctrl + F '16,什么都没有。 2016 年这个问题的答案有什么更新吗?

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


【解决方案1】:

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

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

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

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

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

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

只是想重申一下,我们在模型上使用了 [ValidationAttributes],然后在运行时用新规则取代了它们。最终结果是,如果用户无权访问 [Required] User.LastName 字段,则不会对其进行验证。

3) Crazy Interface Dynamic Proxy Thing 我尝试的最后一种技术是使用 ViewModel 的接口。最终结果是我有一个继承自 IAdminEditIUserRegistration 等接口的用户对象。 IAdminEdit 和 IUserRegistration 都将包含 DataAnnotation 属性,这些属性执行所有特定于上下文的验证,例如带有接口的 Password 属性。

这需要一些技巧,并且比其他任何事情都更像是一种学术练习。 2 和 3 的问题是需要自定义 UpdateModel 和 DataAnnotationsAttribute 提供程序以了解此技术。

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

现在我知道这是一个非常特定于 xVal 的问题,但是像这样的动态验证的所有途径都会导致内部 MVC 元数据提供程序的自定义。由于所有元数据的东西都是新的,在这一点上没有什么是干净或简单的。自定义 MVC 的验证行为所要做的工作并不难,但需要深入了解所有内部结构的工作原理。

【讨论】:

  • 感谢您的全面回复!由于我今年早些时候写了这个 OP,我已经尝试了你提到的 1) 和 3) 方法的变体。我同意由于您已经描述的原因,两者都不理想。关于方法 2:您能否指出任何描述如何完成此操作的博客文章或类似资源?它不必与 xVal 相关,因为我使用的是常规的 MVC 2 模型绑定器和元数据。
【解决方案2】:

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

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

【讨论】:

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

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

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

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

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

【讨论】:

  • 您所描述的正是导致我编写此 OP 的问题。但是你提出什么解决方案? “仅验证与域无关的要求”到底是什么意思?您对编辑器和显示模板元数据有何建议?
  • 与域无关的要求是电话号码格式、电子邮件格式、距离(正数)、年龄(正整数)等。这些东西不会随着业务而改变——它们是系统外部的概念常数。如果视图模型是特定于屏幕的,则可以将特定于屏幕的元数据直接添加到它们。否则,使用数据字典。 (c.f. bit.ly/bvL5KM) 实际上我已经离开 ASP.NET MVC 一段时间了,但是当我使用版本 1 时,我发现 MvcContrib 的强类型 ViewData 扩展是必不可少的。
【解决方案4】:

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

【讨论】:

  • 虽然可以通过分支 xVal 在客户端实现类似的修复,但我不会这样做,因为它依赖于魔术字符串,并且很容易在添加后忘记在此处添加字符串模型的新属性。
  • 我同意这不是一个完美的解决方案,但我们正在通过不断收集字段名称来管理魔术字符串问题,这些字段名称以它们所引用的工作流程中的步骤命名。这样,当控制器调用在发布期间执行验证的类时,它会传入您知道需要对该视图进行验证的字段集合。
【解决方案5】:

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

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

除非你的领域是公开一些疯狂的东西,比如一个活的、内存中的对象图而不是实体副本,否则 ViewModel 是白费力气。因此,要回答您的问题,请将域验证保留在域模型中。

【讨论】:

  • UI 取决于域。它不必直接依赖于域。几乎任何“报告”都需要 DTO。同样,大多数表单可能无法完美地映射到单个域对象,因此需要专门的 UI 实例。 DTO 和 ViewModel 转换的逻辑仍然是域模型的一部分。 您在任何时候都没有破坏任何抽象或违反任何不变量。 WPF 甚至将这一原则具体化为 MVVM(模型-视图-视图模型)。
  • 我认为报告是一个独立于业务领域的领域。单个 UI 可以统一两个域(取决于两者)。为了强类型视图的目的,我毫不犹豫地将多个域对象组合到一个类中。将 ViewModel 放在域中是公然违反关注点分离的行为。现在你不能改变你的用户界面而不改变你的域。并且 ViewModel 可以在支持不同使用场景的不同应用程序/UI 之间重用是有争议的。
  • 顺便说一句,据我了解,在 WPF 特定的说法中,ViewModel 是视图的模型。它是视图特征的抽象。因此,它本质上是在 UI 中定义的。这样一个类的重点是允许视图绑定到模型上不能直接映射到简单 UI 元素(例如下拉列表)的属性。但是根据模式,ViewModel 实际上是组成领域 Model 并在 UI 和 Model 之间执行绑定转换。
  • gWiz:我完全同意验证规则最好存在于模型中,并且不应在 ViewModel 中重复。但是,您根本不需要自定义视图模型,这根本不是真的。如果要保持视图类型安全,则在许多情况下绝对必须使用自定义视图模型。对于像 NerdDinner 这样的示例 ASP.NET MVC 应用程序,情况可能并非如此,但它经常发生在具有更复杂用户界面的大型项目中。
  • 我同意@Adrian。我对 ASP.NET MVC 还是很陌生,但我已经遇到过需要 ViewModel 的示例。一方面:我的User 包含UsernameHashedPassword,但我的“重置密码”视图需要PasswordPasswordConfirmation。这里视图和模型之间没有逻辑映射。我使用 aViewModel 来验证两个密码是否匹配并遵循其他一些长度规则等,然后将单个密码传递到存储库,在存储库中对其进行哈希处理并保存在数据库中。如果没有ViewModel,这将如何完成?
猜你喜欢
  • 1970-01-01
  • 2013-01-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-10
相关资源
最近更新 更多