添加学生

在添加学生视图中,需要选择专业的下拉列表和年级的下拉列表决定班级的下拉列表中的数据,如果不选择,则默认在班级下拉列表中显示所有班级数据。

image-20240818181636726

因此,同样需要获取专业和年级的列表数据。

添加GetSelectListModel方法

虽然ClassService中存在GetSelectListModel方法,但是它返回的是ClassViewModel,因此,我们还是需要在StudentService创建一个返回StudentViewModel的GetSelectListModel方法。

代码如下:

public StudentViewModel GetSelectListModel()
{
    StudentViewModel model = new StudentViewModel();
    List<Major> majors = majorRepo.GetList();
    model.Majors = new SelectList(majors, "Id", "Name");

    List<Grade> grades = gradeRepo.GetList();
    model.Grades = new SelectList(grades, "Id", "Name");

    return model;
}

创建Add动作

在StudentController中创建Add动作,并从GetSelectListModel获取视图模型返回给视图。

代码如下:

public IActionResult Add()
{
    return View(studentService.GetSelectListModel());
}

创建Add视图

在之前的GetSelectListModel中,我们并没有去获取班级的下拉列表数据,因为我们已经把班级下拉列表作为一个单独的组件。

但是这个组件怎么在视图中引入呢?

有一种方法是通过Ajax请求分部视图的地址,将获取的页面嵌入到视图中的位置。

但是还有一种更简单的方法,就是使用htmx。

引入htmx

<input asp-for="ClassId" hx-get="/Student/GetClassSelectPartial" hx-trigger="load" hx-swap="outerHTML">

在这段代码中,hx-trigger="load"表示在页面加载完成后触发htmx事件,hx-get="/Student/GetClassSelectPartial"表示触发事件后请求/Student/GetClassSelectPartial地址,hx-swap="outerHTML"表示将请求获取的页面替换当前标签。

Add视图代码初步如下:

@{
    ViewBag.Title = "添加学生";
}
@model StudentViewModel

<form method="post" class="mx-auto" style="width:320px;">
    <div asp-validation-summary="All" class="has-text-danger mb-4"></div>
    <div class="field">
        <label asp-for="MajorName" class="label"></label>
        <div class="control">
            <div class="select is-fullwidth">
                <select asp-for="MajorId" asp-items="Model.Majors">
                    <option value="">请选择</option>
                </select>
            </div>
        </div>
    </div>
    <div class="field">
        <label asp-for="GradeName" class="label"></label>
        <div class="control">
            <div class="select is-fullwidth">
                <select asp-for="GradeId" asp-items="Model.Grades">
                    <option value="">请选择</option>
                </select>
            </div>
        </div>
    </div>
    <div class="field">
        <label asp-for="ClassName" class="label"></label>
        <div class="control">
            <div class="select is-fullwidth">
                <input asp-for="ClassId" hx-get="/Student/GetClassSelectPartial" hx-trigger="load" hx-swap="outerHTML">
            </div>
        </div>
    </div>
    <div class="field">
        <label asp-for="Number" class="label"></label>
        <div class="control">
            <input asp-for="Number" class="input">
        </div>
    </div>
    <div class="field">
        <label asp-for="Name" class="label"></label>
        <div class="control">
            <input asp-for="Name" class="input">
        </div>
    </div>
    <div class="field">
        <label asp-for="Birthday" class="label"></label>
        <div class="control">
            <input asp-for="Birthday" class="input">
        </div>
    </div>
    <div class="field">
        <label asp-for="Gender" class="label"></label>
        <div class="control">
            <label class="radio">
                <input type="radio" asp-for="Gender" value="false">
                男
            </label>
            <label class="radio">
                <input type="radio" asp-for="Gender" value="true">
                女
            </label>
        </div>
    </div>
    <div class="field">
        <button class="button is-primary is-fullwidth">提交</button>
    </div>
</form>

下拉列表级联

虽然现在可以显示班级的下拉列表,但是目前存在的问题是当选择专业或年级列表时,班级列表中的数据不会改变。

这是因为我们没有设置触发器让下拉列表中的值改变后执行某些动作。

一种方法是通过JavaScript监听专业和年级下拉列表的值,如果值发生改变,则通过Ajax请求/Student/GetClassSelectPartial地址,并将页面重新嵌入。

另一种方法仍然是使用htmx。

代码如下:

<select asp-for="MajorId" asp-items="Model.Majors"
        hx-get="/Student/GetClassSelectPartial"
        hx-target="#ClassId"
        hx-include="#GradeId,#ClassId"
        hx-swap="outerHTML">
    <option value="">请选择</option>
</select>

其中hx-target="#ClassId"表示替换的目标,hx-include="#GradeId,#ClassId"表示请求地址时附带的其他值。hx-trigger不需要设置,它在select的值改变时默认触发。

最终,Add视图的代码如下:

@{
    ViewBag.Title = "添加学生";
}
@model StudentViewModel

<form method="post" class="mx-auto" style="width:320px;">
    <div asp-validation-summary="All" class="has-text-danger mb-4"></div>
    <div class="field">
        <label asp-for="MajorName" class="label"></label>
        <div class="control">
            <div class="select is-fullwidth">
                <select asp-for="MajorId" asp-items="Model.Majors"
                        hx-get="/Student/GetClassSelectPartial"
                        hx-target="#ClassId"
                        hx-include="#GradeId,#ClassId"
                        hx-swap="outerHTML">
                    <option value="">请选择</option>
                </select>
            </div>
        </div>
    </div>
    <div class="field">
        <label asp-for="GradeName" class="label"></label>
        <div class="control">
            <div class="select is-fullwidth">
                <select asp-for="GradeId" asp-items="Model.Grades"
                        hx-get="/Student/GetClassSelectPartial"
                        hx-target="#ClassId"
                        hx-include="#MajorId,#ClassId"
                        hx-swap="outerHTML">
                    <option value="">请选择</option>
                </select>
            </div>
        </div>
    </div>
    <div class="field">
        <label asp-for="ClassName" class="label"></label>
        <div class="control">
            <div class="select is-fullwidth">
                <input asp-for="ClassId" hx-get="/Student/GetClassSelectPartial"
                       hx-trigger="load"
                       hx-include="#MajorId,#GradeId"
                       hx-swap="outerHTML">
            </div>
        </div>
    </div>
    <div class="field">
        <label asp-for="Number" class="label"></label>
        <div class="control">
            <input asp-for="Number" class="input">
        </div>
    </div>
    <div class="field">
        <label asp-for="Name" class="label"></label>
        <div class="control">
            <input asp-for="Name" class="input">
        </div>
    </div>
    <div class="field">
        <label asp-for="Birthday" class="label"></label>
        <div class="control">
            <input asp-for="Birthday" class="input">
        </div>
    </div>
    <div class="field">
        <label asp-for="Gender" class="label"></label>
        <div class="control">
            <label class="radio">
                <input type="radio" asp-for="Gender" value="false">
                男
            </label>
            <label class="radio">
                <input type="radio" asp-for="Gender" value="true">
                女
            </label>
        </div>
    </div>
    <div class="field">
        <button class="button is-primary is-fullwidth">提交</button>
    </div>
</form>

添加Insert方法

在StudentService中添加Insert方法,代码如下:

public void Insert(StudentViewModel model)
{
    Student student = new()
    {
        ClassId = model.ClassId,
        Number = model.Number,
        Name = model.Name,
        Gender = model.Gender,
        Birthday = model.Birthday,
    };
    studentRepo.Insert(student);
}

添加post请求的Add动作

由于我们设置了学生的学号不能重复,因此需要通过try...catch去捕获可能发生的异常,如果出现DbUpdateException异常,则说明学号重复,因此可以添加模型验证错误文本学号不能重复字样。

除此之外,由于有些用户可能不选择班级直接提交,这个时候应该提示请选择班级。但是原本的提示会The value '' is invalid.。因此我们可以删除原本的提示,自定义自己的提示。

最终代码如下:

[HttpPost]
public IActionResult Add(StudentViewModel model)
{
    if (ModelState.IsValid)
    {
        try
        {
            studentService.Insert(model);
            return RedirectToAction("Index");
        }
        catch (DbUpdateException)
        {
            ModelState.AddModelError(nameof(Student.Number), "学号不能重复");
        }
    }
    if (model.ClassId == Guid.Empty)
    {
        ModelState.Remove(nameof(Student.ClassId));
        ModelState.AddModelError(nameof(Student.ClassId), "请选择班级");
    }
    StudentViewModel studentViewModel = studentService.GetSelectListModel();
    model.Majors = studentViewModel.Majors;
    model.Grades = studentViewModel.Grades;
    return View(model);
}