FluentValidation 不正确地从 DropDown 验证模型

FluentValidation Improperly Validating Model from DropDown

我有以下两个模型(剥离到相关部分):

Models\Department.cs:

public class DepartmentValidator : AbstractValidator<Department> {
    public DepartmentValidator() {
        RuleFor(d => d.Name)
            .NotEmpty().WithMessage("You must specify a name.")
            .Length(0, 256).WithMessage("The name cannot exceed 256 characters in length.");
    }
}

[Validator(typeof(DepartmentValidator))]
public class Department {
    public int Id { get; set; }

    [Column(TypeName = "nvarchar")]
    [MaxLength(256)]
    public string Name { get; set; }
}

Models\FacultyMember.cs:

public class FacultyValidator : AbstractValidator<FacultyMember> {
    public FacultyValidator() {
        RuleFor(f => f.Name)
            .NotEmpty().WithMessage("You must specify a name.")
            .Length(0, 64).WithMessage("The name cannot exceed 64 characters in length.");
    }
}

[Validator(typeof(FacultyValidator))]
public class FacultyMember {
    public int Id { get; set; }

    [Column(TypeName = "nvarchar")]
    [MaxLength(64)]
    public string Name { get; set; }

    public virtual ICollection<Department> Departments { get; set; }

    public FacultyMember() {
        Departments = new HashSet<Department>();
    }
}

我有以下控制器代码:

Controllers\FacultyController.cs:

// GET: Faculty/Create
public ActionResult Create() {
    // Get Departments.
    var departmentList = db.Departments.ToList().Select(department => new SelectListItem {
        Value = department.Id.ToString(),
        Text = department.Name
    }).ToList();

    ViewBag.DepartmentList = departmentList;

    var facultyMember = new FacultyMember();
    facultyMember.Departments.Add(new Department()); // Create a single dropdown for a department to start out.
    return View(facultyMember);
}

// POST: Faculty/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,Name,Departments")] FacultyMember facultyMember) {
    // Get Departments.
    var departmentList = db.Departments.ToList().Select(department => new SelectListItem {
        Value = department.Id.ToString(),
        Text = department.Name
    }).ToList();

    ViewBag.DepartmentList = departmentList;

    if (!ModelState.IsValid) { // Problem here...
        return View(facultyMember);
    }

    db.Faculty.Add(facultyMember);
    db.SaveChanges();

    return RedirectToAction("Index");
}

Views\Faculty\Create.cshtml:

...

<div class="form-group">
    @Html.LabelFor(model => model.Departments, new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.Departments, new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.Departments, "", new { @class = "text-danger" })
    </div>
</div>

...

Views\Shared\EditorTemplates\Department.cshtml:

@model MyProject.Models.Department

@Html.DropDownListFor(model => model.Id, ViewBag.DepartmentList as IEnumerable<SelectListItem>, "Select...", new { @class = "form-control" })

所以,当我导航到创建教师页面时,一切都正常显示; 'Departments' 字段有一个下拉列表,其中包含我数据库中的部门。但是,提交表单后,我的模型状态无效(请参阅上面代码中的注释)。经过进一步检查,似乎 FluentValidation 吐出一个错误,因为我的 "Name" 字段为空。当我在 creating/editing 个部门时,这正是它应该做的,但是对于教师人数的下降,它不应该验证整个部门,不是吗?正如我指定的那样,下拉列表唯一发回的是 ID。

此下拉列表发送的唯一内容是部门的 Id,它已被正确接收。那么,我需要做什么才能完成这项工作?我的目标是拥有一组动态的下拉列表,每个下拉列表都填充了数据库中的现有部门。类似于 this 示例。

如果还有什么需要解释的,请告诉我。

正如 Stephen Muecke 所解释的那样,解决方案是创建一个视图模型来表示我想要传递给表单并返回的所有数据。

ViewModel\FacultyMemberViewModel.cs:

public class FacultyMemberViewModelValidator : AbstractValidator<FacultyMemberViewModel> {
    public FacultyMemberViewModelValidator() {
        RuleFor(f => f.Name)
            .NotEmpty().WithMessage("You must specify a name.")
            .Length(0, 64).WithMessage("The name cannot exceed 64 characters in length.");

        RuleFor(s => s.SelectedDepartments)
            .NotEmpty().WithMessage("You must specify at least one department.")
    }
}

[Validator(typeof(FacultyMemberViewModelValidator))]
public class FacultyMemberViewModel {
    public int Id { get; set; }

    public string Name { get; set; }

    public int[] SelectedDepartments { get; set; }
    [DisplayName("Departments")]
    public IEnumerable<SelectListItem> DepartmentList { get; set; }
}

Views\Faculty\Create.cshtml:

...

<div class="form-group">
    @Html.LabelFor(model => model.DepartmentList, new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.ListBoxFor(model => model.SelectedDepartments, Model.DepartmentList, new { @class = "form-control" })             @Html.ValidationMessageFor(model => model.SelectedDepartments, "", new { @class = "text-danger" })
    </div>
</div>

...

Controllers\FacultyController.cs:

// GET: Faculty/Create
public ActionResult Create() {
    var facultyMemberViewModel = new FacultyMemberViewModel {
        DepartmentList = GetDepartmentList()
    };

    return View(facultyMemberViewModel);
}

// POST: Faculty/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,Name,SelectedDepartments,DepartmentList")] FacultyMemberViewModel facultyMemberViewModel) {
    if (!ModelState.IsValid) {
        // Re-set the Department list.
        if (facultyMemberViewModel.DepartmentList == null) {
            facultyMemberViewModel.DepartmentList = GetDepartmentList();
        }

        return View(facultyMemberViewModel);
    }

    var facultyMember = new FacultyMember {
        Id = facultyMemberViewModel.Id,
        Name = facultyMemberViewModel.Name,
    };

    foreach (var departmentId in facultyMemberViewModel.SelectedDepartments) {
        // I'm assuming this is safe to do (aka the records exist in the database)...
        facultyMember.Departments.Add(db.Departments.Find(departmentId));
    }

    db.Faculty.Add(facultyMember);
    db.SaveChanges();

    return RedirectToAction("Index");
}