模态 pop-up 中的表单:除了 drop-down 之外的所有内容都经过验证,即使在使用 .IsEmpty() 时也是如此

Form in modal pop-up: Everything is validated *except* for the drop-down, even when using .IsEmpty()

当前项目:

所以我有一堆结构相同 table 的“笔记”,它们应该与至少两页的单个元素配对,并在第三页进行总结。所有需要注释的元素都是单个“循环”的一部分,因此元素都是相同 table 的片段,注释 table 挂在上面。例如,“演示文稿”由完成 (yes/no) 布尔值和周期 table 中的日期组成。演示文稿注释是一个单独的 table,仅针对那两个循环列,它挂在循环 table 之外(注释 table 有一个外键,它是循环的主键)。由于这些笔记仅用于演示,因此整个笔记 table 称为 PresentationNotes。循环中的许多其他元素都有自己的注释 table,并且整个项目中的所有注释 table 在结构上都是相同的。

从这个相同的结构中,我能够抽象出模型和视图,这样我就不必为每个笔记复制不同的 CRUD 模型和 CRUD 视图 table。我在控制器中所要做的就是为每个笔记取模型 table 并将特定条目与通用笔记模型中的通用条目相关联。

例如前面提到的Presentation模型:

namespace CCS.Models {
  public class CycleNotesPresentation {
    [Key]
    public Guid NotesId { get; set; }
    [DisplayName("Cycle")]
    public Guid CycleId { get; set; }
    [DisplayName("Comm. Type")]
    public Guid NotesStatusId { get; set; }
    [DisplayName("Date")]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}")]
    public DateTime NotesDate { get; set; }
    [DisplayName("Notes")]
    [DataType(DataType.MultilineText)]
    public string Notes { get; set; }
    #region Essentials
    //Essential DB components for each and every table. Place at end.
    [HiddenInput, DefaultValue(true)]
    public bool Active { get; set; }
    [HiddenInput, Timestamp, ConcurrencyCheck]
    public byte[] RowVersion { get; set; }
    [HiddenInput]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}")]
    public DateTime Recorded { get; set; }
    [HiddenInput]
    public DateTime Modified { get; set; }
    [HiddenInput]
    public string TouchedBy { get; set; }
    #endregion

    [ForeignKey("CycleId")]
    public virtual Cycle Cycle { get; set; }
    [ForeignKey("NotesStatusId")]
    public virtual NotesStatus NotesStatus { get; set; }
  }
}

如您所见,这里有很多不一定需要在抽象模型和视图中。

抽象的 Notes 模型,至少对于 Create,是这样的:

[Validator(typeof(CreateNotesValidator))]
public class CreateNotes {
  public string NotesCategory { get; set; }
  [DisplayName("Comm. Type")]
  public string NotesStatusId { get; set; }
  [DisplayName("Date")]
  [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}")]
  public DateTime NotesDate { get; set; }
  [DisplayName("Notes")]
  public string Notes { get; set; }
}

当然,我还有其他三个模型:查看、编辑和删除,但我们现在只关注这个。如果我可以修复创建,我可以修复编辑,这是唯一一个具有需要 client-side 验证的 drop-down 菜单的其他菜单。

请注意上面的区别 -- NotesStatusId 字段实际上是一个字符串,而不是一个 Guid。好吧,事实证明,如果我一直使用 Guid,我的 client-side 验证选项将非常有限。另外,client-side 验证仍然不适用于 Guid,因此我决定通过使用字符串来简化模型(以及验证)。

因此,当我拉取原始 Presentation 模型时,我会将 Guid 转换为字符串,当我处理 Notes 模型并将其转储回 Presentation 模型时,我会将字符串转换回 Guid。这让我有更多 client-side 验证选项。

整个过程我的控制器是这样的:

// GET: Onboarding/CreateCycleNotesPresentation
[HttpGet]
public ActionResult CreateCycleNotesPresentation() {
  var model = new CreateNotes() {
    NotesCategory = "Presentation",
    NotesDate = DateTime.Now
  };
  ViewBag.NotesStatusId = new SelectList(db.NotesStatus.Where(x => x.Active == true), "NotesStatusId", "NotesStatusName");
  return PartialView("_CreateNotesPartial", model);
}
// POST: Onboarding/CreateCycleNotesPresentation
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> CreateCycleNotesPresentation(CreateNotes model) {
  if(ModelState.IsValid) {
    var id = new Guid(User.GetClaimValue("CWD-Cycle"));
    CycleNotesPresentation cycleNotes = new CycleNotesPresentation();
    cycleNotes.NotesId = new Guid();
    cycleNotes.CycleId = id;
    cycleNotes.NotesStatusId = new Guid(model.NotesStatusId);
    cycleNotes.NotesDate = model.NotesDate;
    cycleNotes.Notes = model.Notes;
    cycleNotes.Active = true;
    cycleNotes.Recorded = DateTime.UtcNow;
    cycleNotes.Modified = DateTime.UtcNow;
    cycleNotes.TouchedBy = User.Identity.GetFullNameLF();
    db.CycleNotesPresentation.Add(cycleNotes);
    await db.SaveChangesAsync();
    return RedirectToAction("Index");
  }
  model.NotesCategory = "Presentation";
  ViewBag.NotesStatusId = new SelectList(db.NotesStatus.Where(x => x.Active == true), "NotesStatusId", "NotesStatusName", model.NotesStatusId);
  return PartialView("_CreateNotesPartial", model);
}

在这里我们可以看到一些有趣的部分 -- 我添加了一个 NotesCategory 条目,以便视图可以填充要添加注释的元素的标题。这个最后就不处理了

我还将以刷新整个页面来结束 POST。我发现这是最简单的解决方案,因为我无法使 JSON 提交正常工作(实际的 POST 方法从未收到数据,因此提交会挂起)。此外,通过 whole-page 刷新,整个页面效果更好。所以让我们别管它,k?

现在是最重要的事情:抽象的 Notes 模型和视图的验证器:

namespace CCS.Validators {
  class NotesValidator {
  }
  public class CreateNotesValidator : AbstractValidator<CreateNotes> {
    public CreateNotesValidator() {
      RuleFor(x => x.NotesDate)
    .NotEmpty().WithMessage("Please select a date that this communication occurred on.");
      RuleFor(x => x.NotesStatusId)
    .NotEmpty().NotNull().WithMessage("Please indicate what type of communication occurred.");
      RuleFor(x => x.Notes)
    .NotEmpty().WithMessage("Please submit notes of some kind.")
    .Length(2, 4000).WithMessage("Please provide notes of some substantial length.");
    }
  }
  public class EditNotesValidator : AbstractValidator<EditNotes> {
    public EditNotesValidator() {
      RuleFor(x => x.NotesDate)
    .NotEmpty().WithMessage("Please select a date that this communication occurred on.");
      RuleFor(x => x.NotesStatusId)
    .NotNull().NotEmpty().NotEqual("00000000-0000-0000-0000-000000000000").Matches("^[{(]?[0-9A-F]{8}[-]?([0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$").WithMessage("Please indicate what type of communication occurred.");
      RuleFor(x => x.Notes)
    .NotEmpty().WithMessage("Please submit notes of some kind.")
    .Length(2, 4000).WithMessage("Please provide notes of some substantial length.");
    }
  }
}

我们现在可以在很大程度上忽略 EditNotesValidator,因为这不是我们正在做的事情。

该视图是抽象注释的简单部分,表单本身与您所能得到的一样普通:

@model CCS.Models.CreateNotes
<div class="modal-header">
  <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
  <h3 class="modal-title">Create Note for “@Model.NotesCategory”</h3>
</div>

@using(Html.BeginForm()) {
  @Html.AntiForgeryToken()
  <div class="modal-body">

    <fieldset>
      @Html.LabelFor(m => Model.NotesDate, new { @class = "control-label" })<div class="input-group date">@Html.TextBoxFor(m => m.NotesDate, "{0:yyyy-MM-dd}", new { @class = "form-control date" })<span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span></div>
      @Html.ValidationMessageFor(m => m.NotesDate)
      @Html.LabelFor(m => Model.NotesStatusId, new { @class = "control-label" })@Html.DropDownList("NotesStatusId", null, "« ‹ Select › »", htmlAttributes: new { @class = "form-control" })
      @Html.ValidationMessageFor(m => m.NotesStatusId)
      @Html.LabelFor(m => Model.Notes, new { @class = "control-label" })@Html.TextAreaFor(m => m.Notes, new { @class = "form-control required" })
      @Html.ValidationMessageFor(m => m.Notes)
    </fieldset>
  </div>

  <div class="modal-footer">
    <span id="progress" class="text-center" style="display: none;">
      <img src="/images/wait.gif" alt="wait" />
      Wait..
    </span>
    <button type="submit" value="Save" title="Save" class="btn btn-primary glyphicon glyphicon-floppy-disk"></button>
    <button class="btn btn-warning" data-dismiss="modal">Close</button>
  </div>
}
<script>
    $("form").removeData("validator");
    $("form").removeData("unobtrusiveValidation");
    $.validator.unobtrusive.parse("form");
    $(function () {
      $.fn.datepicker.defaults.format = "yyyy-mm-dd";
      $(".date").datepicker();
    });

</script>

所以,是的。日期验证器完全按预期工作。 Notes textarea 得到了很好的验证。然而,drop-down 菜单完全是午餐时间——无论我尝试什么,无论是 .NotEmpty() 还是 .NotNull() 或任何其他被 FluentValidation 明确标记为在客户端起作用的东西, drop-down 菜单无效。原始 HTML 的检查表明我正在正确构建 SelectList:

<select id="NotesStatusId" class="form-control" name="NotesStatusId">
  <option value="">« ‹ Select › »</option>
  <option value="98e9f033-20df-e511-8265-14feb5fbeae8">Phone Call</option>
  <option value="4899dd4d-20df-e511-8265-14feb5fbeae8">eMail</option>
  <option value="8c073863-20df-e511-8265-14feb5fbeae8">Voice Mail</option>
  <option value="8a13ec76-20df-e511-8265-14feb5fbeae8">Meeting</option>
</select>

而默认 « ‹ Select › » 第一个选项 should 的空值意味着 .NotEmpty().NotNull() should 完美运行。但他们不是。如果我删除日期(表单加载时 auto-filled,请参阅上面的控制器)并保持 drop-down 和文本区域不变,只有日期字段和文本区域会被标记——drop-down 根本没有被标记。

建议?


编辑 1: 哎呀,哎呀……添加了错误的控制器……现在修复了。


编辑 2: …Bueller? …布勒?


编辑 3: 我发现很难相信 no-one else 在 drop-down 上进行 client-side 验证时遇到过问题通过 FluentValidation 的菜单。

这是献给所有追随我的可怜人的。具体来说,我的情况涉及:

  • 模态对话框中的表单。
  • 模态对话框是用通用部分启动的,网站的许多不同部分使用该部分来填充具有相同结构的多个 table。因此,单个通用 Partial/Form 可以在许多地方用于许多不同的 identically-structured tables
  • 模态对话框关闭以刷新整个页面,没有任何 JSON。
  • 因为这是模态对话框中的一个表单,并且因为模态本身无法刷新(我不知道如何刷新),所以所有验证都必须在客户端进行。这是我遇到的最重要的问题。
  • 由于 select 菜单的创建方式,客户端验证未按需要运行。

我解决这个问题的方法部分是偶然发现的,部分是放弃了不明智的方法。具体来说,ViewBags。在这里我了解到 ViewBags 使 client-side 验证 drop-down selects 不可能,当他们完成填充 drop-down selects 的工作时需要验证。

因此,部分偶然的运气与我使用控制器和模型的方式有关。因为站点的各个不同部分有多个 identically-structured 注释 table,所以我能够“抽象出”整个模型、注释的视图和验证,以便抽象 collection 可以处理多个 Notes table 的全部 CRUD 需求。代码重用,FTW!。如果可能的话,我还将寻求抽象出控制器的一部分,但那是另一天的事情。

所以看看我原来的内容post,我的抽象笔记的创建和编辑部分的模型有一个非常简单的添加:

public IList<SelectListItem> NotesStatus { get; set; }

您看,NotesStatusId 是 NotesStatus table 的外键,它具有基本的通信详细信息 -- phone、电子邮件、会议、语音邮件等。所以我需要告诉模型我要从这个 table.

中列出一个列表

接下来是我的控制器。因为我已经采用特定的 Notes 模型并将它们填充到抽象的 Notes 模型中,所以我能够扩展它以包含 drop-down 菜单的内容,而不是将其填充到 ViewBag 中。将我的控制器上面的内容与下面的内容进行比较:

[HttpGet]
public async Task<ActionResult> CreateProspectingNotes() {
  var model = new CreateNotes() { // We just need to set up the abstracted Notes model with a create -- no populating from the db needed.
    NotesCategory = "Prospecting",
    NotesDate = DateTime.Now,
    NotesStatus = await db.NotesStatus.Where(x => x.Active).Select(x => new SelectListItem { Text = x.NotesStatusName, Value = x.NotesStatusId.ToString() }).ToListAsync()
  };
  return PartialView("_CreateNotesPartial", model);
}

看看我们如何用最终出现在视图中的 SelectList 填充模型的 NotesStatus 部分?

编辑有点复杂,因为我们不仅要调出抽象的笔记,还要用你想编辑的笔记table中的内容填充它:

[HttpGet]
public async Task<ActionResult> EditProspectingNotes(Guid? id) {
  ProspectingNotes prospectingNotes = await db.ProspectingNotes.FindAsync(id); // getting the specific ProspectingNotes table that is to be edited.
  if(prospectingNotes == null) { return HttpNotFound(); }
  EditNotes model = new EditNotes() { // Populating the abstracted Notes model with the specific ProspectingNotes model.
    NotesCategory = "Prospecting",
    NotesId = prospectingNotes.NotesId,
    NotesStatusId = Convert.ToString(prospectingNotes.NotesStatusId),
    NotesDate = prospectingNotes.NotesDate,
    Notes = prospectingNotes.Notes,
    NotesStatus = await db.NotesStatus.Where(x => x.Active).Select(x => new SelectListItem { Text = x.NotesStatusName, Value = x.NotesStatusId.ToString() }).ToListAsync()
  };
  return PartialView("_EditNotesPartial", model);
}

现在进入视图:

@Html.LabelFor(m => Model.NotesStatusId, new { @class = "control-label" })@Html.DropDownListFor(x => x.NotesStatusId, new SelectList(Model.NotesStatus, "Value", "text"), "« ‹ Select › »", htmlAttributes: new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.NotesStatusId)

特别是 .DropDownList().DropDownListFor() 取代,因为我们现在将它牢固地绑定到 x => x.NotesStatusId 而不是 loosely-coupled "NotesStatusId"调用 ViewBag,我认为它是整个 client-side 工作的关键。使用 ViewBag,您只是用一个已经具有默认值 selected 的列表填充 drop-down,在这里您将默认值绑定到列表,然后直接从 [=62] 填充它=].因为是强耦合的,所以现在有东西要client-side验证来验证。

一旦我完成了所有这些,所要做的就是确保我的验证只有一个 .NotEmpty() 而不是像 .NotEmpty().NotNull() 这样确实抛出异常的双链(double REQUIRED,显然)。

希望对您有所帮助。如果您自己遇到问题,请创建一个引用此问题的 post 并给我发送 PM。我会看看能帮上什么忙。