实现修改功能

通过上一节的实践,相信大家对我们 MVC 项目开发一个页面的基本流程已经有所了解了,实体类、Service、Controller、Action、View 之间的分工应该也差不多明确了,那让我们进行下一步开发——编辑页面。上一节仅仅实现了列表的显示功能,但是要想对里面的信息进行修改还需要做一些工作,那现在就开始把。
本节目标是实现访问 /MyUser/Edit/{id} 可修改这位用户的信息。

调整列表页面

修改功能必须是对一条已经存在的数据进行修改的,所以我们先改造一下列表页,在每条记录后为编辑页面提供一个入口,列表页的最终效果如下
https://dev.shijinet.cn/trac/Kunlun/raw-attachment/wiki/bill.nong/blog/20170307172958/List page with edit link.png

打开 List.cshtml,我们为页面的表格新增一列“操作”列,并在每一行的操作列中添加一个可以跳转到编辑页面的超链接

    <table class="table table-hover">
        <thead>
            <tr>
                <th>Id</th>
                <th>用户名</th>
                <th>性别</th>
                <th>生日</th>
                <th>上次登陆时间</th>
                <th>操作</th>
            </tr>
        </thead>

        @foreach (var user in Model.MyUserList)
        {
            <tr>
                <td>@user.Id</td>
                <td>@user.Username</td>
                <td>@user.Gender</td>
                <td>@user.Birthday</td>
                <td>@user.LastLoginDate</td>
                <td>
                    <a href="@Url.Action("Edit", new { Id = user.Id })">修改</a>
                </td>
            </tr>
        }
    </table>

Url.Action() 方法用于生成适合的 URL,这里采用的是它接收 Action Name 和 URL 参数的重载。第一个参数表示 Action Name,第二个参数 new { Id = user.Id } 通过一个匿名类型,来表示有一个名为 Id 的参数,其值为每次循环时 user.Id 的值。可以尝试删除这个参数,或者在匿名类型中增加参数,观察 URL 发生的变化。默认生成的 URL 是以当前 Controller 为准的,所以这里当 Id 为 1 时,生成的 URL 为 /MyUser/Edit/1
可以将鼠标移动到超链接上,浏览器底部一般会显示出具体地址。也可以选择使用开发人员工具来查看。
https://dev.shijinet.cn/trac/Kunlun/raw-attachment/wiki/bill.nong/blog/20170307172958/Developer Tools.png

请务必熟练使用浏览器提供的开发人员工具功能!大部分现代浏览器按下 F12 键都会打开开发人员工具,通过它可以查看 HTML 文档、修改页面上的元素内容、样式、执行 JavaScript 控制台、监听请求等等。

现在点击这个超链接会看到一个 404 页面,它表示找不到资源,因为我们还没开始开发呀。

添加编辑页面

与上一节一样,我们需要自己添加 Action、View、View Model 和 Service 中的方法,下面再来快速的过一遍。
首先在 MyUserController 中,添加 Edit Action,因为有一个参数 Id 需要接收,所以需要在 Edit() 方法的参数列表添加这个参数。

    public ActionResult Edit(int id)
    {
        return View();
    }

接着添加 Service 方法,实现我们“修改”的业务。打开 MyUserService,新增名为 Update() 的方法,该方法接收 MyUser 类型的参数,没有返回值。MyUser 参数中的内容,就是修改后的数据,要将它保存到数据库中,Update() 方法内部实际上只需要调用 Repository<T>Update() 方法即可。

    public void Update(MyUser myUser)
    {
        _myUserRepository.Update(myUser);
    }

然后回到 MyUserController,生成 Edit Action 的 View,并添加 Edit 页面所使用的 View Model —— EditMyUserModel。注意,我们一般建议一个 View 对应一个 View Model,但是项目中的实际情可能并非如此(多个页面使用了同一个 View Model),在本文档中将按照一个 View 一个 View Model 的原则进行开发。
EditMyUserModel 中包含所有需要显示和编辑的内容,也就是说基本上和 MyUser 类(不含 BaseEntity)的属性一致,下面是我们的 EditMyUserModel 类:

    using System;

    namespace Kunlun.CRS.Web.Models.MyUser
    {
        public class EditMyUserModel
        {
            public int Id { get; set; }

            public string Username { get; set; }

            public string Password { get; set; }

            public int Gender { get; set; }

            public DateTime? Birthday { get; set; }

            public DateTime? LastLoginDate { get; set; }
        }
    }

接下来按下 F6 重新生成解决方案,并到浏览器中访问 Edit 页面,发现已经不显示 404 错误了,而是一个熟悉的空白界面。

记得要通过我们新增在列表页面的“修改”超链接来访问,如果直接在 View 上按 Ctrl+F5 的话,你会收到这样一个错误:对于“Kunlun.CRS.Web.Controllers.MyUserController”中方法“System.Web.Mvc.ActionResult Edit(Int32)”的不可以为 null 的类型“System.Int32”的参数“id”,参数字典包含一个 null 项。可选参数必须为引用类型、可以为 null 的类型或声明为可选参数。 别忘了 Edit() Action 需要一个 int 类型、名为 Id 的参数!在 View 上按下 Ctrl+F5 只会访问没有参数的该 View。

向编辑页面添加 HTML 控件

回到代码中,打开 Edit.cshtml,在第一行加上 @model 指向编辑页面对应的 View Model。

    @model Kunlun.CRS.Web.Models.MyUser.EditMyUserModel

让我们来复习一下如何从 Controller 中向 View 传递数据,请先不要看下文,试一下将 Edit Action 接收的 Id 显示到 View 上。
相信大家都做出来了,在 MyUserController 中:

    public ActionResult Edit(int id)
    {
        var model = new EditMyUserModel();
        model.Id = id;
        
        return View(model);
    }

Edit.cshtml

    @model Kunlun.CRS.Web.Models.MyUser.EditMyUserModel
    @{
        ViewBag.Title = "Edit";
    }

    @Model.Id

从数据库中获取要显示的数据

实际上没有这么简单,因为从列表页带来的信息只有 User 的 Id 这一样,要想获取这个 User 的完整信息,必须通过这个 Id 到数据库中查询一次,但 Service 中并没有这样的方法,所以我们要实现一个名为 GetById() 的方法,该方法接收一个 int 类型的参数 Id,并返回 MyUser 对象:

    public MyUser GetById(int id)
    {
        return _myUserRepository.GetById(id);
    }

接下来在 Edit Action 调用这个方法得到 MyUser,接下来就是通过 AutoMapper 将 MyUser 给映射到 EditMyUserModel 了,不要忘记先添加 AutoMapper 的映射规则。下面是改为从数据库中查询 User 的 Edit Action:

    public ActionResult Edit(int id)
    {
        var myUser = _myUserService.GetById(id);
        var model = myUser.MapTo<MyUser, EditMyUserModel>();

        return View(model);
    }

调试代码解决问题

上面这段代码实际上是有问题的,如果有好事者通过把浏览器地址中 Id 那一位改为数据库中不存在的 Id,例如将 URL 改为了 MyUser/Edit/5,那么“未将对象引用设置到对象的实例”的错误将会显示在页面上。通过错误源信息可以看到,这个错误发生在 Edit.cshtml@Model.Id 中。“未将对象引用设置到对象的实例”异常一般是访问了对象内容为 null 的属性导致的,也就是说 View Model 在 View 上的值为 null。首当其冲的应该怀疑 Edit Action 的 return View(model) 一行,因为这里的 model 就是 View 中 @Model 的值。
https://dev.shijinet.cn/trac/Kunlun/raw-attachment/wiki/bill.nong/blog/20170307172958/Debug.png
这时需要通过通过调试代码来解决问题。现在我们要分析代码运行到 return View(model) 这行时 model 变量的值,因此需要在这里设置一个断点——将鼠标移动到这行代码的行号的左侧,并点击一下,能看到一个红色的圆出现在行首。然后按下 F5 键开始调试。
用引发异常的条件重新执行一遍 Action,也就是说重新访问 MyUser/Edit/5 来重现问题,你会发现 Visual Studio 会自动弹出,并且刚才设置断点的红色圆圈上面有一个箭头,这表示即将运行这一行代码。将鼠标移动到 model 上时,发现它的值为 null!
https://dev.shijinet.cn/trac/Kunlun/raw-attachment/wiki/bill.nong/blog/20170307172958/model is null.png
model 为什么会是 null 呢,往前追溯,发现在 AutoMapper 映射之前,也就是 var myUser = _myUserService.GetById(id) 这一行得到的 myUser 已经是 null 了……原来,Service 的 GetById() 方法中,使用的 Repository<T>.GetById() 方法,在数据库中没有 Id 匹配时,会直接返回 null,导致了这一问题。
找到问题后就需要修正这个问题了,既然不能让 myUser 为 null 时显示编辑页面,那就添加一个 if 语句,当 id 查不到东西时,重定向会列表页面,并提示“没有找到这条记录”:

    public ActionResult Edit(int id)
    {
        var myUser = _myUserService.GetById(id);
        if (myUser == null)
        {
            ErrorNotification("没有找到这条记录");
            return RedirectToList();
        }

        var model = myUser.MapTo<MyUser, EditMyUserModel>();

        return View(model);
    }

ErrorNotification() 可以在下一次显示的页面上方显示一段错误提示。编译后再访问 MyUser/Edit/5 的话,将会重新跳转会 List 页面,并得到一行错误提示
https://dev.shijinet.cn/trac/Kunlun/raw-attachment/wiki/bill.nong/blog/20170307172958/ErrorNotification.png

使用 HTML 帮助类向页面添加控件

经过了这么多的波折,我们终于可以向 View 中添加内容了!既然是编辑页面,肯定也需要提供文本框之类的东西让人修改内容。
大家应该知道,要想让服务器获得用户输入的数据,需要通过表单form)和表单元素,同时表单元素必须存在 name,才能在服务器端区分出哪个元素的值是多少。MVC团队为了方便的将name、值、样式等内容生成到页面上,为我们封装了一系列的HTML帮助类,在下面的代码中能看到一部分。
先选几个有代表性的属性来进行演示:UsernameBirthday

    @model Kunlun.CRS.Web.Models.MyUser.EditMyUserModel
    @{
        ViewBag.Title = "Edit";
    }

    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()
        @Html.HiddenFor(m => m.Id)

        <div class="box box-solid">
            <div class="box-body">
                <div class="row">
                    <div class="col-xs-12 col-sm-6 col-md-6">
                        <div class="form-group">
                            @Html.LabelFor(m => m.Username)
                            @Html.TextBoxFor(m => m.Username, new { @class = "form-control" })
                            @Html.ValidationMessageFor(m => m.Username)
                        </div>
                    </div>
                </div>

                <div class="row">
                    <div class="col-xs-12 col-sm-6 col-md-6">
                        <div class="form-group">
                            @Html.LabelFor(m => m.Birthday)
                            @Html.TextBoxFor(m => m.Birthday, new { @class = "form-control" })
                            @Html.ValidationMessageFor(m => m.Birthday)
                        </div>
                    </div>
                </div>
            </div>

            <div class="box-footer">
                <button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
                <a href="@Url.Action("List")" class="btn btn-default"><i class="fa fa-undo"></i> 返回</a>
            </div>
        </div>
    }

上面的 HTML 代码是为了使 CCM 页面风格统一而准备的,它们的样式由 Bootstrap 和 AdminLTE 提供,正因为加上了这些 HTML,编辑页面才会看着比上一章做好的列表页面稍微好看一些。请将关注点放在一系列的 Html 类中的方法调用上。为了更能说明情况,重新编译代码并在浏览器中通过 F12 工具,看一下上面的代码会生成什么样的 HTML,然后对比一下,看看 @Html 都变成了什么。

    <form action="/MyUser/Edit/1" method="post" novalidate="novalidate">
        <input name="__RequestVerificationToken" type="hidden" value="7JbOiZRlM7kA2OuiINZ06POMhmbPoIo2cFv3BmG405XgiL7yWreUP9g0lOKfYaYyZQnPogdleRj_Z0u2nsNL8A9sM1qd30TzYGhKZxoS7M_b2N4JXyrNzzHZnEbiaN_mpbJgTud4rB4ADiU17NOTlw2">
        <input data-val="true" data-val-number="字段 Id 必须是一个数字。" id="Id" name="Id" type="hidden" value="1">
        <div class="box box-solid">
            <div class="box-body">
                <div class="row">
                    <div class="col-xs-12 col-sm-6 col-md-6">
                        <div class="form-group">
                            <label for="Username">Username</label>
                            <input class="form-control" id="Username" name="Username" type="text" value="admin">
                            <span class="field-validation-valid" data-valmsg-for="Username" data-valmsg-replace="true"></span>
                        </div>
                    </div>
                </div>

                <div class="row">
                    <div class="col-xs-12 col-sm-6 col-md-6">
                        <div class="form-group">
                            <label for="Birthday">Birthday</label>
                            <input class="form-control" data-val="true" data-val-date="字段 Birthday 必须是日期。" id="Birthday" name="Birthday" type="text" value="1900-01-23 00:00:00">
                            <span class="field-validation-valid" data-valmsg-for="Birthday" data-valmsg-replace="true"></span>
                        </div>
                    </div>
                </div>
            </div>

            <div class="box-footer">
                <button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> 保存</button>
                <a href="/MyUser/List" class="btn btn-default"><i class="fa fa-undo"></i> 返回</a>
            </div>
        </div>
    </form>

应该很容易的可以发现

  • Html.AntiForgeryToken()<input type="hidden" name="__RequestVerificationToken"> 这个留到下一节再细说,但先记住它的名字叫做防伪标记
  • Html.BeginForm()<form>
  • Html.HiddenFor()<input type="hidden">
  • Html.LabelFor()<label>
  • Html.TextBoxFor()<input type="text">
  • Html.ValidationMessageFor()<span class="field-validation-valid" data-valmsg-for data-valmsg-replace>

并且 <form>actionmethod 属性,<label> for 属性, <input>idnamevalue 属性,都有了正确的内容!采用 @Html 帮助方法配合 View Model,可以极大的减少我们编码的压力,而且 @Html 种提供的方法名称描述性都非常强,我们很容易知道它生成的 HTML 是什么样的。
@Html.TextBoxFor() 方法的第二个参数的匿名类型显然定义了 HTML 标签的 class 属性,由于 class 是 C# 中的关键字,所以此处需要加上 @ 符号避免歧义。还可以为这个匿名类型添加其他属性,这些属性最终也会被添加到生成的 HTML 标签上。

如果对 View 中明明有 Id 但浏览器中的页面却没有显示出来而感到困惑,说明你还没有完全掌握 HTML 表单元素。Id 由 Html.HiddenFor() 方法生成,生成的结果为 <input type="hidden">隐藏域。储存在隐藏域中的内容不会在界面上显示,但最终表单提交时会和页面上的其他内容一起提交给服务器。

学会查看方法的重载参数列表,这样可以迅速掌握如何使用该方法,以及需要为方法提供哪些参数!将光标放在要查看重载列表的方法的括号中,然后按下 Ctrl + Shift + Whitespace(空格) 键!

向页面添加 KendoUI 日期选择控件

先不要着急点击“保存”按钮,先来欣赏一下我们的页面。似乎有些奇怪,Birthday 作为一个日期,却只能让用户一个字符一个字符的修改,用户体验非常糟糕,如果点击 Birthday 的文本框时,能弹出一个日期选择器就好了!
有很多第三方的公司开发了一系列的适合前端的控件,我们选择的是 Kendo UI,来看看它如何将这个普通的文本框变成日期选择器。首先需要明确的是,KendoUI 是一个 JavaScript 类库,并且依赖于 jQuery。在 View 中,js 代码是不能随意摆放的,我们在模板页中定义了专门用于书写 js 代码的区域,为了在 Edit.cshtml 中添加这个区域,需要在现有代码的最下方,添加这些代码

    @section scripts{
        <script>

        </script>
    }

这样才能在 <script> 中添加 js 代码,来将 Birthday 文本框变成日期控件:为 Birthday 添加一个名为 full-width 的 class,之后在 <script> 中添加如下代码

    $("#Birthday").kendoDatePicker();

保存后刷新页面查看效果
https://dev.shijinet.cn/trac/Kunlun/raw-attachment/wiki/bill.nong/blog/20170307172958/Birthday Date Picker.png

实际上小小的日期控件能看出很多细节,现在日期的格式为 1900/1/23,年月日之间用斜线隔开,而我们项目的要求是 YYYY-MM-dd,也就是显示为 1900-01-23 的格式。这些我们在一段时间的实际开发后,都总结经验将他们给加以封装,以更容易设置的方式提供给大家使用。
将 Birthday 使用的 @Html.TextBoxFor() 方法修改为 KLDateTextBoxFor() 方法,并再添加一个 class,名为 kl-date-picker,之后在 @section secripts 的第一行添加上 @Html.Partial("_KendoDatePickerPartial"),最后删除 <script> 中的内容。

    @Html.KLDateTextBoxFor(m => m.Birthday, new { @class = "form-control kl-date-picker full-width" })

    @section scripts{
        @Html.Partial("_KendoDatePickerPartial")

        <script>
        </script>
    }

Html.KLDateTextBoxFor() 方法是我们自己扩展的 HTML 帮助方法。Html.Partial() 方法用于将一个分布视图添加到当前视图中。在引入了 _KendoDatePickerPartial 分布视图后,当页面载入完成时,就会有 javascript 代码自动执行,并寻找页面上所有带有 kl-date-picker 这个 class 的 <input> 标签,并将它转换为 Kendo Date Picker 控件。

添加下拉菜单

接下来实现性别的选择功能,这里将性别有关的选项作为下拉菜单,以供用户选择。调查 @Html 帮助类后,我们发现 DropDownListFor() 这个方法似乎符合我们的需求,查看参数列表,发现至少需要提供一个表达式,指明为 View Model 的哪个属性生成下拉菜单,以及一个用于填充下拉菜单的数据集合(也就是下拉菜单的数据源)。
现在看来,我们只需要解决数据源的问题就 OK 了,既然是需要传递给 View 上的方法的值,那应该存放在 View Model 中。查看 Html.DropDownListFor() 的参数列表发现,这个数据源要求是 IEnumerable<SelectListItem> 类型的,也就是说是一个 SelectListItem 类型的集合。回想一下在制作列表页面时,我们采用的集合,是 List<T> 类型吧,这里也可以采用 List<T> 类型,那么到 EditMyUserModel.cs 中,新增一个名为 Genders 的属性:

    public List<SelectListItem> Genders { get; set; }

然后到 Controller 的 Edit Action 为它添加值。根据数据定义(参见第一章),Gender 为 0 时表示“男”;为 1 时表示“女”,我们自己 new 一个集合并添加这两个值就行了。这是修改后的 Edit Action,注意 AutoMapper 和 return View() 之间的内容。

    public ActionResult Edit(int id)
    {
        var myUser = _myUserService.GetById(id);
        if (myUser == null)
        {
            ErrorNotification("没有找到这条记录");
            return RedirectToList();
        }

        var model = myUser.MapTo<MyUser, EditMyUserModel>();

        var genderDataSource = new List<SelectListItem>();
        genderDataSource.Add(new SelectListItem { Value = "0", Text = "男" });
        genderDataSource.Add(new SelectListItem { Value = "1", Text = "女" });

        model.Genders = genderDataSource;

        return View(model);
    }

最后回到 View 中,在 Username 和 Birthday 之间添加上 Gender 的区域:

    <div class="row">
        <div class="col-xs-12 col-sm-6 col-md-6">
            <div class="form-group">
                @Html.LabelFor(m => m.Gender)
                @Html.DropDownListFor(m => m.Gender, Model.Genders, new { @class = "form-control" })
                @Html.ValidationMessageFor(m => m.Gender)
            </div>
        </div>
    </div>

重新编译并到浏览器中查看最终效果,下拉菜单已经显示出来,并且正确的选中了“女”。但遗憾的是,不同浏览器对下拉菜单默认的显示效果是不同的(可以到IE、FireFox、Edge 浏览器中查看一下这个页面),此外一些复杂的下拉菜单操作,凭借默认的 HTML 下拉菜单是很难实现的,于是 KendoUI 也提供了下拉菜单控件!和日期选择器一样,我们对于这个使用非常频繁的 Kendo 控件也进行了封装。
@section scripts 中添加对 _KendoDropdownListPartial 分布视图的引用(若是忘了怎么做,请到上面找一下),之后为 Gender 的下拉菜单添加一个名为 full-width 的 class。

实现保存功能

编写适用于 POST 请求的 Action

页面添加了这三个可以修改的内容就差不多啦,接下来实现保存到数据库的功能。但是目前有一个严重的问题,不信可以看一眼 View 生成的 Form 表单。大家应该知道,<form>action 属性表示表单提交的目的地,而这里的 Form 显然是想提交给当前页面(实际上通过 Html.BeginForm() 在不加任何参数的情况下,就是生成当前页的 action)!而当前页面对应的 Action 为 Edit Action,它需要一个 int 类型的参数……
其实这也是有破解之道的,那边是 <form> 的另一个属性:method。这应该也是大家非常熟悉的,表示请求方式的值。我们是通过一个超链接,或者刷新了一下浏览器来访问编辑页面的,这种请求方式为 GET 方式。而显然,<form>method 后面接着的是 post!MVC 框架实际上也提供了一个名为 HttpPostAttribute特性,来让 Action 被 POST 请求所访问到。当我们不对 Action 添加任何与请求方式有关的特性时,并且它返回的是一个 ViewResult,那么它默认是允许 GET 请求的,这也是为什么 Edit(int id) 能被正确的访问到的原因。
那接下来,我们仍然需要添加一个 Edit Action 方法,但参数有所不同。这个 Edit 方法需要接收表单的内容,而表单的内容已经全部定义在了 EditMyUserModel 这个 View Model 中,所以这个 Edit Action 将接收一个类型为 EditMyUserModel 的参数。不要忘了添加 HttpPost 特性。

    [HttpPost]
    public ActionResult Edit(EditMyUserModel model)
    {
        return View();
    }

了解 EF 的状态跟踪

接着来编写这个 Action 的实现。因为我们使用 Entity Framework 来进行数据库操作,EF 提供了一个非常有用的特性——状态跟踪,也就是说 EF 能识别出我们修改了实体的哪一个属性值。为了使用这一特性,我们必须先在这个用于保存的 Edit Action 中,使用 View Model 中的 Id 让 EF 重新查询一遍数据库,好让 EF 能在后面保存时,知道我们修改了哪些内容。之后通过 AutoMapper 将 View Model 映射到实体类上(记得到 AutoMapperStartupTask 中添加一个由 EditMyUserModelMyUser 的映射),最后调用 _myUserService 中的 Update() 方法即可保存到数据库中!不过这里需要注意 AutoMapper 的写法有些特殊之处:

    [HttpPost]
    public ActionResult Edit(EditMyUserModel model)
    {
        var entity = _myUserService.GetById(model.Id);

        entity = model.MapTo<EditMyUserModel, MyUser>(entity);
        _myUserService.Update(entity);

        return View();
    }

看一下之前的 MapTo() 方法都是不加参数的,而这里传入了从数据库中查询到的实体类对象的变量。实际上,AutoMapper 每次映射之后都是产生一个新的对象,而由于前面提到的“状态跟踪”机制的存在,如果将一个全新的变量传给 EF,它就没法帮我们进行 Update 操作了;传入这个变量意味着让 AutoMapper 不要产生新的对象,而是将值映射到我们提供的这个对象上,这样便达到了维持“状态跟踪”的目的。

在 AutoMapper 中忽略某个属性的映射

如果这时候你兴冲冲的去点击“保存”按钮,会收到一个错误 :D

不能将值 NULL 插入列 'Password',表 'ECRS_20170124.dbo.MyUser';列不允许有 Null 值。UPDATE 失败。

错误已经明确的告诉了我们,我们把一个 NULL 值插入到了 MyUser 表中,而 MyUser 表的 Password 列是不允许为 NULL 的!这是为什么呢?可以先自己试着通过加断点调试来分析问题原因(问题描述已经清楚的说明了问题的所在,想想断点应该加在哪?)。
实际上前往 View 页面我们能够发现,我们仅仅通过隐藏域缓存了 Id,却没有对 Password 进行缓存(实际上 LastLoginDate 我们也没有使用隐藏域来缓存)。为什么我们缓存 Id 却不缓存这两个属性呢。因为如果没有 Id,我们就无法在后台查询到要修改的记录,而 Password、LastLoginDate 我们这里认为它不能被修改,就没有必要通过隐藏域存放在页面上了。
那你一定有疑问,这样值不就丢了吗?其实这里更适合使用 AutoMapper 的映射规则来忽略某个属性的映射!因为我们是先从数据库中查出结果,再把 View Model 的值映射到实体类上,如果阻止 AutoMapper 将 View Model 的 Password 和 LastLoginDate 映射到实体类上,一切就没有问题了!
前面的例子中,由于我们实体类和 View Model 的契合度非常高,属性名全部都是同名的,所以编写 AutoMapper 映射规则只需要将类名写出,实际上 AutoMapper 可以进行更细致的配置,让我们进入 AutoMapperStartupTask 类,找到上一步中编写的 EditMyUserModelMyUser 的映射,改写为下面这样:

    Mapper.CreateMap<EditMyUserModel, MyUser>()
        .ForMember(dest => dest.Password, mo => mo.Ignore())
        .ForMember(dest => dest.LastLoginDate, mo => mo.Ignore());

可以看到,我们在添加类的映射后,使用了两个 ForMember() 方法,这里给它传入了两个参数,第一个参数是用来配置目标(destination)属性的,第二个参数为具体配置。第二行代码的意思就是,忽略(ignore) 映射目标类的 Password 属性。如此一来 Password 就会保持它在数据库中原本的值,而不会因为页面上没有缓存它而变成 NULL 了。

如果有谁尝试了一下,将 View 中用于生成 Id 隐藏域的代码移除了,发现保存仍然没有问题,这篇教程真是太差劲了!其实这真不怪我,仔细观察 <form>action 属性你会发现 URL /MyUser/Edit/1 的最后一位恰好就是当前记录的 Id。我们的项目中默认的路由配置中约定了这样写法的 URL,最后一位表示 Id。这也是为什么 Edit(int id) 能收到 Id,以及移除 Html.HiddenFor() 之后,EditMyUserModel 中仍然会有 Id 值的原因。人家把 Id 写在 URL 里了啊!表单里没有也无所谓啊!对于这种将提交给后台的值能通过参数或 Model 直接访问的技术,我们称之为模型绑定。默认情况下,同名或具有某些规律的 name 能自动绑定到后台 Action 参数列表、View Model 中。

保存之后要做的事

我们的项目终于可以保存了!但是点了“保存”按钮之后,又双叒叕报错啦

未将对象引用设置到对象的实例

这个错误似曾相识,看了一下下面的“错误源”,并看了一下 Edit(EditMyUserModel model) 方法的具体实现,想必心中一定有数了。我们 return View() 的时候啥也没有给视图哇 (╯‵□′)╯︵┻━┻
这里的处理十分简单,甚至不需要返回视图,既然我们实现好了通过 Id 就能显示编辑页面的 Action,那保存好之后干脆直接重定向过去算啦,顺便再来个“保存成功”的提示就更完美了!

    [HttpPost]
    public ActionResult Edit(EditMyUserModel model)
    {
        var entity = _myUserService.GetById(model.Id);

        entity = model.MapTo<EditMyUserModel, MyUser>(entity);
        _myUserService.Update(entity);

        SuccessNotification("保存成功");
        return RedirectToAction("Edit", new { Id = model.Id });
    }

SuccessNotification() 方法会在下一次显示的页面上出现一个成功提示(你们已经用过 ErrorNotification()了,他们俩是一样的)。RedirectToAction() 是用来重定向的方法,传入要定向到的 Action、Controller 和参数就能正确的重定向了。最终效果是这样的:
https://dev.shijinet.cn/trac/Kunlun/raw-attachment/wiki/bill.nong/blog/20170307172958/Edit save success.png

总结

  • 了解了 URL 帮助方法,它可以用于生成正确的 URL,也了解了如何通过一个匿名类型来表示 URL 的参数列表
  • 学习了 HTML 帮助方法,它们可以帮助我们生成 HTML 标签,并且生成的 HTML 标签与 View Model 密不可分,从 Id、Name、Value 到 CSS 样式,一切都是我们所期待的
  • 尝试了通过 Visual Studio 的断点调试功能,分析问题
  • 第一次使用了 KendoUI 提供的控件、第一次往页面上生成一个内容由自己决定的下拉菜单
  • 体验了 EF 的状态跟踪、Auto Mapper 忽略某个属性的方式以及如何在 Action 中进行重定向

下一节中,我们将实现一个“添加”页面,用于将一条全新的 MyUser 记录添加到数据库中,敬请期待。

扩展阅读

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值