代码之家  ›  专栏  ›  技术社区  ›  Adrian Grigore

在自定义视图模型中重用验证属性

  •  11
  • Adrian Grigore  · 技术社区  · 15 年前

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

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

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

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

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

    有没有优雅的解决方法?我一直在考虑对XVAL源代码进行黑客攻击,但也许到目前为止我忽略了其他一些方法。

    谢谢,

    阿德里安

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

    5 回复  |  直到 14 年前
        1
  •  7
  •   John Farrell    14 年前

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

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

    绕过这些的选择都是次优的。我现在已经为3个项目处理了这个问题,并且实现以下解决方案从来都不是干净的,通常是令人沮丧的。我会努力做到 实际的 忘记所有其他人正在进行的第几个讨论中的ddd/db/model/hotness。

    1)多视图模型 拥有几乎相同的视图模型违反了干法原理,但我觉得这种方法的成本非常低。通常违反了干安培的维护成本,但在我看来,这是最低的成本,并不算多。假设您不会经常更改姓氏字段的最大字符数。

    2)动态元数据 MVC 2中有一些钩子,用于为模型提供您自己的元数据。使用这种方法,您可以使用任何方法来提供元数据,根据当前的httpRequest排除某些字段,从而排除操作和控制器。我使用这种技术构建了一个数据库驱动的权限系统,该系统指向数据库,并告诉DataAnnotationsMetadataProvider的子类排除数据库中存储的基于属性的值。

    这项技术在ATM上很有效,但唯一的问题是用 UpdateModel() . 为了解决这个问题,我们创建了一个 SmartUpdateModel() 方法,该方法还将转到数据库并自动生成排除字符串[]数组,以便不验证任何不允许的字段。当然,出于性能原因,我们缓存了它,所以这并不坏。

    只是想重申一下,我们在模型上使用了[validationattributes],然后在运行时用新的规则对它们进行了替换。最终结果是 [Required] 如果用户没有访问该字段的权限,则无法验证user.lastname字段。

    3)疯狂界面动态代理 我尝试的最后一种技术是为视图模型使用接口。最终的结果是我有一个用户对象继承自 IAdminEdit IUserRegistration .iadminedit和iuserregistration都将包含DataAnnotation属性,这些属性执行所有特定于上下文的验证,如带有接口的密码属性。

    这需要一些黑客,而且更像是一种学术活动。2和3的问题在于,需要对updateModel和dataAnnotationsAttribute提供程序进行定制以了解此技术。

    我最大的障碍是我不想将整个用户对象发送到视图,所以我最终使用动态代理来创建 Idmin编辑

    现在我明白这是一个非常XVAL特定的问题,但是像这样的动态验证的所有道路都会导致内部MVC元数据提供程序的定制。因为所有的元数据都是新的,所以在这一点上没有什么是干净的或简单的。要定制MVC的验证行为,您需要做的工作并不困难,但需要一些深入了解所有内部工作原理的知识。

        2
  •  4
  •   John Farrell    14 年前

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

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

        3
  •  3
  •   G-Wiz RameshVel    15 年前

    如果假设视图模型是强制的,那么我建议它们只强制执行不可知域的需求。这包括“需要用户名”和“电子邮件格式正确”等内容。

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

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

    除非您能够预测未来并排除这些可能性,否则只能验证领域不可知的需求。

        4
  •  2
  •   Brandon Linton    15 年前

    我不知道这对于客户端验证有什么作用,但是如果部分验证是您的问题,您可以修改 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);
         }
    }
    
        5
  •  0
  •   G-Wiz RameshVel    15 年前

    我将冒着被否决的风险,声明视图模型(在ASP.NET MVC中)没有好处,特别是考虑到创建和维护它们的开销。如果这个想法是要与领域脱钩,那是不可辩驳的。与域分离的UI不是该域的UI。用户界面 必须 取决于域,所以您要么将视图/操作耦合到域模型,要么将视图模型管理逻辑耦合到域模型。因此,架构的争论是没有意义的。

    如果目的是防止用户利用ASP.NET MVC的模型绑定对可变字段进行黑客攻击,则不允许更改这些字段,那么a)域应强制执行此要求,b)操作应向模型绑定器提供可更新属性的白名单。

    除非你所在的域公开了一些疯狂的东西,比如一个动态的内存对象图,而不是实体副本,否则视图模型是浪费精力的。因此,要回答您的问题,请在域模型中保留域验证。