【问题标题】:MVC Validation Fundamentals (with Entity Framework)MVC 验证基础(使用实体框架)
【发布时间】:2012-10-15 22:36:03
【问题描述】:

MVC 验证基础(使用实体框架)

场景:

我有一个模型类如下(通过实体框架 EF.x DbContext 生成器自动生成)。

(目前没有视图模型)。

public partial class Activity
{
    public int Id { get; set; }

    public byte Progress { get; set; }
    public decimal ValueInContractCurrency { get; set; }
    public System.DateTime ForecastStart { get; set; }
    public System.DateTime ForecastEnd { get; set; }

    public int DepartmentId { get; set; }   
    public int OwnerId { get; set; }
    public int StageId { get; set; }
    public int StatusId { get; set; }

    public virtual Department Department { get; set; }
    public virtual Owner Owner { get; set; }
    public virtual Stage Stage { get; set; }
    public virtual Status Status { get; set; }
}

当我在强类型视图上提交空白表单时,我会收到以下验证消息:

进度字段是必需的。

ValueInContractCurrency 字段是必需的。

ForecastStart 字段是必需的。

ForecastEnd 字段是必需的。

即db 表中的所有字段。

如果我填写这些并再次提交,那么控制器就会被调用。由于 IsValid 为 false,控制器然后返回视图页面。

然后屏幕会重新显示这些验证消息:

StageId 字段是必需的。

DepartmentId 字段是必需的。

StatusId 字段是必需的。

OwnerId 字段是必需的。

即db 表中的所有外键字段(这些也是所有选择框)。

如果我填写了这些,表单就会成功提交并保存到数据库中。

问题:

  1. 鉴于我没有使用任何 [必需] 属性,验证从何而来?这和实体框架有关吗?

  2. 为什么表单不能立即在客户端验证所有内容,外键(或选择框)有什么不同,它们仅由 IsValid() 检查,即使它们是空的,因此显然无效?

  3. 如何使所有内容都在一个步骤中得到验证(对于空字段),这样用户就不必提交两次表单并且一次显示所有验证消息?是否必须关闭客户端验证?

(我尝试将 [Required] 属性添加到外键字段,但这似乎没有任何区别(可能它们只影响 IsValid)。我也尝试调用 Html.EnableClientValidation() 但这并没有也有任何区别)。

4..最后,我看到人们使用 [MetadataType[MetadataType(typeof(...)]] 进行验证。如果您有视图模型,为什么要这样做,或者只有在没有视图模型的情况下才这样做? ?

显然我在这里遗漏了一些基础知识,因此,如果有人知道关于 MVC 验证过程如何准确地逐步工作的详细教程,包括 javascript/controller 调用,而不仅仅是另一篇关于属性的文章,那么我也可以使用指向该链接的链接:c)


关于神秘人的更多信息:

解决方案设置如下:

.NET4

MVC3

EF5

EF5.x Db 上下文生成器

在 edmx 设计图面上使用“添加代码生成项”来关联 EF.x Db 上下文生成器文件(.tt 文件)

控制器如下所示:

    // GET: /Activities/Create
    public ActionResult Create()
    {
        ViewBag.DepartmentId = new SelectList(db.Departments, "Id", "Name");
        ViewBag.OwnerId = new SelectList(db.Owners, "Id", "ShortName");
        ViewBag.ContractId = new SelectList(db.Contracts, "Id", "Number");
        ViewBag.StageId = new SelectList(new List<string>());
        ViewBag.StatusId = new SelectList(db.Status.Where(s => s.IsDefaultForNewActivity == true), "Id", "Name");
        return View();
    } 


    // POST: /Activities/Create
    [HttpPost]
    public ActionResult Create(Activity activity)
    {
        if (ModelState.IsValid)
        {
            db.Activities.Add(activity);
            db.SaveChanges();
            return RedirectToAction("Index");  
        }

        ViewBag.DepartmentId = new SelectList(db.Departments, "Id", "Name");
        ViewBag.OwnerId = new SelectList(db.Owners, "Id", "ShortName");
        ViewBag.ContractId = new SelectList(db.Contracts, "Id", "Number");
        ViewBag.StageId = new SelectList(db.Stages, "Id", "Number");
        ViewBag.StatusId = new SelectList(db.Status, "Id", "Name");
        return View(activity);
    }

视图是这样的:

<!-- this refers to  the EF.x DB Context class shown at the top of this post -->
@model RDMS.Activity  

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>


@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Activity</legend>


        <div class="editor-label">
            @Html.LabelFor(model => model.StageId, "Stage")
        </div>
        <div class="editor-field">
            @Html.DropDownList("StageId", String.Empty)
            @Html.ValidationMessageFor(model => model.StageId)
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.Progress)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Progress)
            @Html.ValidationMessageFor(model => model.Progress)
        </div>

    <!-- ETC...-->

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

【问题讨论】:

  • 顺便说一句,很抱歉在这里到处都是,但有点迷路了......
  • 这里显然发生了其他事情。您所显示的不应给出这些结果。您是否在项目中添加了任何其他包?您是否使用任何自定义模型绑定器或值绑定器?你的行动方法是什么样的?获取和发布。

标签: asp.net-mvc asp.net-mvc-3 entity-framework-5 asp.net-mvc-validation


【解决方案1】:

之所以需要验证是因为属性是值类型(即它们不能为空)。由于它们不能为 null,因此框架要求您为它们填写值(否则将不得不抛出一些奇怪的异常)。

这个问题表现在几个方面。我在 Slashdot 上一遍又一遍地看到这一点。我不确定为什么这么多人陷入这个问题,但这很常见。通常这会导致一个奇怪的异常,指的是没有抛出默认构造函数,但由于某种原因,这里没有发生。

问题源于您使用 ViewBag 并将 ViewBag 中的项目命名为与您的模型属性相同。提交页面时,模型绑定器会被类似名称的项目混淆。

更改这些以在末尾添加列表:

ViewBag.DepartmentList = new SelectList(db.Departments, "Id", "Name");
ViewBag.OwnerList = new SelectList(db.Owners, "Id", "ShortName");
ViewBag.ContractList = new SelectList(db.Contracts, "Id", "Number");
ViewBag.StageList = new SelectList(new List<string>());
ViewBag.StatusList = new SelectList(db.Status
        .Where(s => s.IsDefaultForNewActivity == true), "Id", "Name");

并更改您的视图以使用 DropDownListFor 的强类型版本:

@Html.DropDownList(x => x.StageId, ViewBag.StageList, string.Empty)
... and so on

另一个注意事项。在上面的示例中,我希望您没有使用某种全局数据上下文或更糟的是,单例。这将是灾难性的,并可能导致数据损坏。

如果 db 只是您在构造函数中新建的控制器的成员,那没关系,但并不理想。更好的方法是在每个操作方法中创建一个新的上下文,由 using 语句包装(然后连接立即关闭并销毁)或在控制器上实现 IDisposable 并显式调用 Dispose。

更好的方法是不在您的控制器中执行任何此操作,而是在业务层中执行任何操作,但这可以等到您更进一步。

【讨论】:

  • 谢谢。标记为答案。关于您关于数据库上下文的问题,它不是全局的,我已经明确调用了 Dispose,但感谢您提及它:c)
  • 在 MVC3 的下拉列表中似乎有一个相关的讨论,在 MVC4 中有一个错误修复:aspnet.codeplex.com/workitem/7629
  • @jimasp - 是的,它是相关的。然而,问题在于人们混淆了几个不同的问题并将它们混合在一起。简单的经验法则..永远不要使用DropDownList(),始终使用DropDownListFor(),并且永远不要将您的列表命名为与您选择的ID相同。
  • 谢谢。只是为了帮助其他人阅读,Mystere 答案中的第二段代码应该是 @Html.DropDownListFor(model => model.StageId, (IEnumerable)ViewBag.StageIdList, string.Empty)。对我来说似乎是多余的演员,但 DropDownListFor 不这么认为。
  • @jimasp - 我正要提到这一点,但我忘记了。 ViewBag 是动态类型,html helpers 并不总是能够准确地确定它是什么类型,因此您必须对其进行强制转换以给出提示
【解决方案2】:

鉴于我没有使用任何 [必需] 属性,验证从何而来?这和实体框架有关吗?

在 MVC(不是 EF)中有一个默认的验证提供程序,检查两件事:

  • 所提供值的类型(int 属性中的字符串)=>(不确定,但类似)yyy is not valid for field xxx

  • 值类型的“检查 null”属性(如果您让空字段对应于 int 属性,它会报错,并且会接受 int? 属性的空字段)。 => The xxx field is required

可以在 global.asax 中取消第二个行为(属性名称相当清楚):

DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

启用客户端验证后,这些验证以及与 DataAnnotations 相关的验证(RequiredStringLength...)将在进入控制器之前在客户端引发验证错误。它避免了服务器上的往返,所以它不是没用的。但当然,您不能只依赖客户端验证。

为什么表单不能立即在客户端验证所有内容,外键(或选择框)有什么不同,它们只是 由 IsValid() 检查,即使它们是空的,因此很清楚 无效?

嗯,我必须承认我没有得到令人满意的答案......所以我让这个更胜任​​的一个。它们在 ModelState.IsValid 中被视为错误,因为当客户端验证通过时,您然后转到 ModelBinding(模型绑定查看您的 POST 值,查看相应 HttpPost 方法的参数(ActionResult Create 为您),并且尝试将 POST 值与这些参数绑定。在您的情况下,绑定看到 Activity activity 参数。并且他在您的 POST 字段中没有得到任何 StageId (例如)。由于 StageId 不可为空,他将其作为 ModelState 字典中的错误 => ModelState 不再有效。

但我不知道为什么它没有被客户端验证捕获,即使有 Required 属性。

如何让所有内容一步到位(对于空 字段),因此用户不必两次提交表单 验证消息会立即显示吗?是否必须关闭客户端 侧面验证?

好吧,您必须关闭客户端验证,因为您不能只信任客户端验证。但是,如上所述,客户端验证可以避免到服务器的无用往返。

最后,我看到人们使用 [MetadataType(typeof(...)]] 进行验证。你为什么要 如果您有视图模型,就这样做,还是只有在没有视图模型的情况下才这样做?

仅当您没有 ViewModel,而是在您的 Model 类上工作。它仅在使用 Model First 或 Database First 时才有用,因为每次 edmx 更改时都会生成实体类(使用 T4)。然后,如果您在类上放置自定义数据注释,则必须在每个类(文件)生成后手动将其放回原处,这将是愚蠢的。所以[MetadataType(typeof()]]是一种在类上添加注解的方法,即使“基类文件”被重新生成。

希望这会有所帮助。

顺便说一句,如果您对验证感兴趣,请查看FluentValidation。 这是一个非常好的...流畅的验证(你猜到了吗?)库。

【讨论】:

  • 查看我的回答,了解为什么会出现奇怪的验证行为,仅供参考。
  • 谢谢你,这很有帮助。
【解决方案3】:
  1. 如果该字段不可为空,EF 认为它是必需的。
  2. 因为外键不可为空,所以导航属性也是必需的。
  3. 要一次获得所有验证,您需要使用在验证后在控制器中转换为实体模式的 ViewModel。 更多关于mvc中的属性验证,可以阅读here.

【讨论】:

  • #1 和 2 是正确的,但 #3 是错误的。问题在于他处理 DropDownLists 的方式。
最近更新 更多