6

这是一个与此处已发布的问题非常相似的问题: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;
  }

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

我们的 CSHTML 类包含一个 ValidationSummary,以“true”作为参数,这意味着它只会显示模型错误。

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

  [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);
  }

...当我使用 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);
  }

...因此依赖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;
  }

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

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

提前致谢!

更新

看来,当被告知排除属性错误时,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 在我们的 live 项目中这样做,而不是在简单的测试应用程序中呢?

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

更新...再次

它在我们的实际项目中起作用的原因是在我们的基本控制器中隐藏了一些代码(所有其他控制器都从中继承),它将模型状态中发现的所有错误复制到具有 String.Empty 键的新条目中。此代码仅在两种情况之一中被调用。因此,真实应用程序和测试应用程序之间没有实际区别。

我现在明白发生了什么以及为什么 ValidationSummary 会这样。

4

1 回答 1

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; }
   }
}

进行验证时,在模型级别添加的错误 (ValidationResults)(其中没有进一步的参数到采用属性名称的 ValidationResult 构造函数,将添加到带有属性名称前缀的 ModelState视图模型的 - 在本例中为“TradeModel”。

我们目前正在考虑几种解决方法。

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

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

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

于 2012-07-05T13:14:37.570 回答