實現修改功能

通過上一節的實踐,相信大家對我們 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 記錄添加到數據庫中,敬請期待。

擴展閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章