使用TryUpdateModel绑定时,MVC ValidationSummary忽略模型级验证错误

Cam*_*per 6 c# asp.net validationsummary asp.net-mvc-3

这是一个与已发布的问题非常相似的问题:ASP.NET MVC:在TryUpdateModel中设置的验证消息未显示ValidationSummary

我不确定那个旧主题是否参考了早期版本的MVC,但在MVC3中,我遇到了类似行的奇怪行为.

我有一个名为Trade的模型类.它继承自IValidatableObject,因此实现了Validate方法.在此范围内,我们对模型进行了整体验证(与强制验证属性的数据注释相对).验证如下:

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
  {
     var validationResults = new List<ValidationResult>();

     if (this.EndDate < this.StartDate)
     {
        validationResults.Add(new ValidationResult("End date must be greater than start date"));
     }

     return validationResults;
  }
Run Code Online (Sandbox Code Playgroud)

我们有一个视图模型来帮助显示交易.这包含通过TradeModel属性对交易模型的引用.所以基本上,视图模型是一个交易模型,加上一些额外的信息,用于下拉列表的比例,例如交易对手等.

我们的CSHTML类包含一个ValidationSummary,其中"true"作为参数,这意味着它只显示模型错误.

如果我实现我的HttpPost控制器方法来创建一个新的贸易如下...

  [HttpPost]
  public ActionResult Create(FormCollection collection)
  {
     var trade = new Trade();

     if (this.TryUpdateModel(trade))
     {
        if (this.SaveChanges(this.ModelState, trade))
        {
           return this.RedirectToAction("Index");
        }
     }

     return this.View(trade);
  }
Run Code Online (Sandbox Code Playgroud)

...当我使用StartDate> EndDate进行交易时,我发现TryUpdateModel返回false并且用户被定向回他们的交易.这看似合乎逻辑.不幸的是ValidationSummary没有显示任何错误消息.

如果我在Create方法中放置一个断点并调查ModelState,我可以看到字典中有一条错误消息.它违反了"TradeModel"的密钥,而不是针对任何属性.再次,这似乎合乎逻辑.

关于其原因的一个理论是ValidationSummary假定针对非String.Empty的键的任何验证错误必须是属性验证错误,它忽略了我们的验证错误,因为我们有一个包含对模型的引用的视图模型,因此导致Key成为"TradeModel".

这个理论脱离水的原因是:如果我重写控制器的Create函数如下......

  [HttpPost]
  public ActionResult Create(Trade trade, FormCollection collection)
  {
     if (this.SaveChanges(this.ModelState, trade))
     {
        return this.RedirectToAction("Index");
     }

     return this.View(trade);
  }
Run Code Online (Sandbox Code Playgroud)

...因此依赖MVC"自动"执行绑定,并重新运行相同的测试场景,向用户显示预期的错误消息!

如果我添加一个断点并查看ModelState,我会看到与之前相同的密钥的相同错误消息,但这次ValidationSummary会选择它们!

如果我按如下方式修改验证,那么它适用于以任一方式编写的控制器的Create函数:

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
  {
     var validationResults = new List<ValidationResult>();

     if (this.EndDate < this.StartDate)
     {
        validationResults.Add(new ValidationResult("End date must be greater than start date", new[] { "StartDate" }));
     }

     return validationResults;
  }
Run Code Online (Sandbox Code Playgroud)

很明显,这只是模型级验证错误的问题.

任何有关这方面的帮助将不胜感激!有些原因(我现在不会讨论)为什么我们需要手动创建视图模型的实例并使用TryUpdateModel调用绑定.

提前致谢!

UPDATE

似乎ValidationSummary的理论只在String.Empty的ModelState中显示错误时被告知排除属性错误实际上是真的.我查看了源代码,它实际上使用ViewData.TemplateInfo.HtmlFieldPrefix来查找模型级别的验证错误.默认情况下,这是String.Empty.

因此,将此值更改为"TradeModel"似乎是合乎逻辑的,但它会导致每个HTML ID或名称都带有前缀,因此绑定会失败!

如果视图模型包含对业务模型的引用,则IValidatableObject添加到ModelState的任何错误都会添加一个键,该键包含一个前缀,该前缀等于视图模型中的业务模型属性名称(在我们的示例中为"TradeModel"),从而导致诸如"TradeModel.CounterpartyId"之类的密钥等.使用等于视图模型的业务模型属性名称的密钥("TradeModel")添加模型级错误.

因此,如果以这种方式构建视图模型,业务似乎无法添加模型级验证错误.

让我感到困惑的是,当编写控制器的Create函数以便将Trade视图模型对象作为参数时,这实际上可以在我们的"真实"项目中工作.我一定错过了昨天,但今天看看它,当MVC绑定并且触发验证时,它似乎在字典末尾添加了一个额外的键,其值为String.Empty.这包含使用TradeModel的键添加的错误的副本.正如您所期望的那样,ValidationSummary然后选择它们!

那么为什么MVC在我们的实时项目中这样做,而不是在简单的测试应用程序中呢?

在编写控制器函数以将视图模型作为参数时,我已经看到两次验证被触发.也许这每次都有微妙的不同?

UPDATE ... AGAIN

它在我们的真实项目中工作的原因是我们的基本控制器中隐藏了一些代码(所有其他人都继承了这些代码),它将模型状态中发现的所有错误复制到具有String.Empty键的新条目.此代码仅在两个方案中的一个中被调用.因此,真实应用和测试应用之间没有实际差异.

我现在明白了究竟发生了什么以及为什么ValidationSummary会像这样表现.

Cam*_*per 5

好.我现在正处于一个可以自己回答这个问题的地步.

如果将ValidationSummary与ExcludePropertyErrors参数设置为True一起使用,它将使用等于ViewData.TemplateInfo.HtmlFieldPrefix的键在模型状态中查找错误.默认情况下,这是String.Empty.

如果您有一个公开您的业务模型的视图模型,则如下所示:

namespace ValidationSummary.Models
{
   using System;
   using System.Collections.Generic;
   using System.ComponentModel.DataAnnotations;

   public class TradeModel : IValidatableObject
   {
      public DateTime StartDate { get; set; }

      public DateTime EndDate { get; set; }

      public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
      {
         List<ValidationResult> validationResults = new List<ValidationResult>();

         if (EndDate < StartDate)
         {
            validationResults.Add(new ValidationResult("End date must not be before start date"));
         }

         return validationResults;
      }
   }
}

namespace ValidationSummary.ViewModels
{
   public class Trade
   {
      public Trade()
      {
         this.TradeModel = new Models.TradeModel();
      }

      public Models.TradeModel TradeModel { get; private set; }
   }
}
Run Code Online (Sandbox Code Playgroud)

验证发生时,在模型级别添加的错误(ValidationResults)(其中ValidationResult构造函数没有进一步的参数获取属性名称)被添加到ModelState,其前缀为属性名称视图模型 - 在此示例中为"TradeModel".

我们正在考虑的方法有几种.

  1. 不要在视图模型之外公开业务模型.这主要涉及绑定到视图模型,而不是绑定到视图模型的业务模型.这可以通过两种方式完成:使视图模型成为业务模型的子类; 或者从业务模型到视图模型的重复属性.我更喜欢前者.
  2. 编写代码以将模型级错误复制到String.Empty的新ModelState字典键中.遗憾的是,这可能无法优雅地完成,因为公开业务模型的视图模型的属性名称是用作密钥的.每个控制器/视图模型可能有所不同.
  3. 在视图中使用以下内容.这将显示业务模型的错误消息.本质上,这假装商业模型的模型级错误实际上是属性错误.这些显示与ValidationSummary的显示不同,但也许这可以通过CSS来解决:

    @ Html.ValidationMessageFor(m => m.TradeModel)

  4. 子类ValidationSummary.这将涉及更改它,以便它知道ModelState中的哪些键引用视图模型的业务模型属性.