代码之家  ›  专栏  ›  技术社区  ›  René Kåbis

模态弹出窗口中的表单:除了下拉列表之外,所有内容都被验证,即使使用.IsEmpty()

  •  0
  • René Kåbis  · 技术社区  · 8 年前

    当前项目:

    • ASP。净额4.5.2
    • MVC五
    • 实体框架6
    • Fluent验证

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

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

    例如,下面是上述演示模型:

    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,因此我决定使用字符串来简化模型(因此也简化了验证)。

    因此,当我拉取原始的演示模型时,我将从Guid转换为字符串,当我处理Notes模型并将其转储回演示模型中时,我会将字符串转换回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方法从未收到数据,因此提交将挂起)。此外,整个页面的刷新效果更好。所以让我们别管这个了,好吗?

    现在最重要的是:抽象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,视图是一个简单的Partial,表单本身就像你能得到的一样:

    @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文本区域得到了漂亮的验证。然而,下拉菜单已经完全过时了——无论我尝试什么,都可以 .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 › » 第一购股权 应该 意思是 .非空() .NotNull() 应该 完美工作。但事实并非如此。如果我删除日期(它是自动填充表单加载的,请参阅上面的控制器),并保持下拉列表和文本区域不变,则只有日期字段和文本区域被标记——下拉列表根本没有被标记。

    建议?


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


    编辑2: 布勒?布勒?


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

    1 回复  |  直到 8 年前
        1
  •  0
  •   René Kåbis    8 年前

    这是给任何追赶我的可怜灵魂的。具体来说,我的处境是:

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

    我解决这个问题的方法部分是偶然的,部分是放弃了不明智的方法。明确地 ViewBag s、 在这里我学到了 ViewBag(查看包) 当他们完成填充需要验证的下拉选择的工作时,让客户端无法验证下拉选择。

    因此,我在控制器和模型方面的工作是我运气的一部分。因为站点的不同部分有多个结构相同的Notes表,所以我能够抽象出一个完整的模型、视图和注释验证,这样这个抽象的集合就可以处理多个Notes表的完整CRUD需求。代码重用,FTW!。如果可能的话,我也会把控制器的一部分抽象出来,但这是另一天的事情。

    因此,看看我的原始帖子的内容,我的抽象Notes的Create和Edit部分模型有一个非常简单的补充:

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

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

    我希望这有帮助。如果您自己有问题,请创建一篇引用这篇文章的帖子,并给我发送PM。我会看看我能做些什么。