模态弹出窗体中的表单:即使使用.IsEmpty()

时间:2016-04-10 05:23:20

标签: asp.net asp.net-mvc asp.net-mvc-5 fluentvalidation

当前项目:

  • ASP.NET 4.5.2
  • MVC 5
  • 实体框架6
  • FluentValidation

所以我有一堆“笔记”,它们是结构相同的表,意味着与至少两页的单个元素配对,并在第三页中进行总结。所有需要注释的元素都是单个“循环”的一部分,因此元素是注释表挂起的同一个表的所有片段。例如,“表示”由完成(是/否)布尔值和循环表中的日期组成。演示说明是一个单独的表,仅适用于那些挂起循环表的两个循环列(notes表有一个外键,即循环的主键)。由于这些注释仅用于演示,因此整个注释表称为PresentationNotes。循环中有许多其他元素都有自己的Notes表,整个项目中的所有Notes表在结构上都是相同的。

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

例如,这是前面提到的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; }
  }
}

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

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

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

当然,我还有其他三个模型:查看,编辑和删除,但现在让我们专注于这个模型。如果我可以修复Create,我可以修复Edit,这是唯一一个需要客户端验证的下拉菜单。

注意上面的差异 - NotesStatusId字段实际上是一个字符串而不是Guid。好吧,事实证明,如果我一直使用Guid,我的客户端验证选项非常有限。另外,客户端验证仍然无法使用Guid,因此我决定使用字符串来简化模型(以及验证)。

因此,当我拉出原始的Presentation模型时,我将从Guid转换为字符串,当我处理Notes模型并将其转储回Presentation模型时,我会将字符串转换回Guid。这允许我有更多的客户端验证选项。

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

// 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方法从未收到数据,因此提交将挂起)。此外,通过整页刷新整个页面效果更好。那么,让我们一个人留下,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,因为这不是我们正在做的工作。

视图是抽象的Notes的简单部分,而表单本身就像你能得到的一样:

@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>

所以,是的。 Date验证器完全按预期工作。 Notes textarea得到了很好的验证。然而,下拉菜单完全不适合午餐 - 无论我尝试什么,无论是.NotEmpty()还是.NotNull()还是其他任何被FluentValidation明确标记为在客户端正常运行的内容,适用于下拉菜单。检查原始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 › »第一个选项的空值应该意味着.NotEmpty().NotNull() 应该完美地运作。但他们不是。如果我擦除日期(自动填充表单加载,请参阅上面的控制器)并保持下拉菜单和textarea不变,只有日期字段和textarea得到标记 - 下拉列表没有被标记一点都不。

建议?

编辑1: Eheh,oops ...添加错误的控制器...现在修复。

编辑2: ... Bueller? ... Bueller?

编辑3:我发现很难相信没有其他人通过FluentValidation在下拉菜单中进行客户端验证时遇到问题。

1 个答案:

答案 0 :(得分:0)

这是为了追随我的任何可怜的灵魂。具体来说,我的情况包括:

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

我解决这个问题的方式是部分意外发现,部分放弃了不可取的方法。具体而言,ViewBag s。在这里,我了解到ViewBag使得下拉选项的客户端验证在完成填充需要验证的下拉选项时无法实现。

所以偶然的运气的部分原因在于我是如何使用控制器和模型的。因为有多个相同结构的Notes表用于站点的各个不同部分,所以我能够“抽象”整个模型,查看和验证注释,以便这个抽象集合可以处理多个Notes表的完整CRUD需求。代码重用,FTW!如果可能的话,我也会想要抽出控制器的一部分,但那是另一天的事情。

所以看一下我原来帖子的内容,我对抽象笔记的创建和编辑部分的模型有一个非常简单的补充:

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

您会看到,NotesStatusId是NotesStatus表的外键,它具有基本的通讯详细信息 - 电话,电子邮件,会议,语音邮件等。所以我需要告诉模型我将从这个表中列出一个列表。

接下来是我的控制器。因为我已经使用了特定的Notes模型并将它们填充到抽象的Notes模型中,所以我能够扩展它以包含下拉菜单的内容,而不是将其填充到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部分?

编辑有点复杂,因为我们不仅需要调出抽象的Notes,而且还要填充您想要编辑的Notes表中的内容:

[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而不是调用ViewBag的松散耦合的"NotesStatusId"我认为这是整个客户端工作的关键。使用ViewBag,您只需使用已选择默认值的列表填充下拉列表,此处将默认值绑定到列表,然后直接从ViewModel / Controller填充它。因为它是强耦合的,所以现在有一些客户端验证可以验证。

完成所有这些后,所需要的只是确保我的验证只有一个.NotEmpty()而不是像.NotEmpty().NotNull()这样的双链,它确实会引发异常(显然是双重要求)

我希望这会有所帮助。如果您自己遇到问题,请创建一个引用此问题的帖子并向我发送PM。我会看到我能做些什么来帮助你。