demoshop

demo, trying to be the best_

ViewModel 的使用在 ASP.NET MVC 中是一個很重要的觀念,但是初學者很容易遇到一個問題就是「一個頁面只有一個 ViewModel 怎麼夠用」,大多數的初學者可能就直接使用 ViewData 或 ViewBag 去傳遞第二個資料物件,如果你這樣傳最直接的問題就是喪失了「內建驗證」的方便性,那究竟應該怎麼正確的處理多個 ViewModel 的問題呢?
因為太多人問了,所以 demo 決定寫一個簡單的範例來有效解決初學者的多數 ViewModel 的問題。

最終目的是完成一個頁面,同時支援「使用者註冊」與「使用者登入」的功能,現在就讓我們來一步一步的完成下去

首先就從建立 ViewModel 開始,請分別建立 LoginViewModels 與 RegisterViewModels 作為登入和註冊使用

LoginViewModels.cs

public class LoginViewModels
{
    [EmailAddress]
    public string Email { get; set; }
    public string Password { get; set; }
}

RegisterViewModels.cs

public class RegisterViewModels
{
    [EmailAddress]
    public string Email { get; set; }
    public string Password { get; set; }
}
你會發現這兩個 ViewModel 根本一模一樣,這是因為 demo 要避免讀者花太多時間在思考所以故意設計的,同時你也應該注意到 Email 欄位使用了內建驗證要求一定要是 Email 格式。

ViewModel 建立完畢後新增對應的 Controller HomeController.cs

public ActionResult Login()
{
    return View();
}
[HttpPost]
public ActionResult Login(LoginViewModels model)
{
    return View();
}
 
public ActionResult Register()
{
    return View();
}
[HttpPost]
public ActionResult Register(RegisterViewModels model)
{
    return View();
}

再來就可以利用 ViewModel 搭配 scaffold 建立出 View Login.cshtml

@model GroupViewModelSample.Models.LoginViewModels
 
@{
    ViewBag.Title = "Login";
}
 
<h2>Login</h2>
 
 
@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>LoginViewModels</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
            </div>
        </div>
 
        <div class="form-group">
            @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
            </div>
        </div>
 
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}
 
<div>
    @Html.ActionLink("Back to List", "Index")
</div>
 
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Register.cshtml

@model GroupViewModelSample.Models.RegisterViewModels
 
@{
    ViewBag.Title = "Register";
}
 
<h2>Register</h2>
 
 
@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>RegisterViewModels</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
            </div>
        </div>
 
        <div class="form-group">
            @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
            </div>
        </div>
 
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}
 
<div>
    @Html.ActionLink("Back to List", "Index")
</div>
 
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

這時候讀者可以試試看資料都是正常

到目前為止我們分別完成了註冊與登入的頁面,但這是個別頁面,要如何合併成一個畫面呢?


請建立一個新的 ViewModel GroupViewModels.cs

public class GroupViewModels
{
    public LoginViewModels Login { get; set; }
    public RegisterViewModels Register { get; set; }
}
這個組合的 Viewmodel 中只有兩個屬性,而這兩個屬性的型別就是剛剛的 LoginViewModels 與 RegisterViewModels 。

接下來實做頁面 Combine.cshtml

@model GroupViewModelSample.Models.GroupViewModels
@{
    ViewBag.Title = "Combine";
}
 
<h2>Combine</h2>
@using (Html.BeginForm("Login","Home"))
{
    @Html.AntiForgeryToken()
 
    <div class="form-horizontal">
        <h4>LoginViewModels</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Login.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Login.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Login.Email, "", new { @class = "text-danger" })
            </div>
        </div>
 
        <div class="form-group">
            @Html.LabelFor(model => model.Login.Password, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Login.Password, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Login.Password, "", new { @class = "text-danger" })
            </div>
        </div>
 
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}
 
@using (Html.BeginForm("Register", "Home"))
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        <h4>RegisterViewModels</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Register.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Register.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Register.Email, "", new { @class = "text-danger" })
            </div>
        </div>
 
        <div class="form-group">
            @Html.LabelFor(model => model.Register.Password, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Register.Password, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Register.Password, "", new { @class = "text-danger" })
            </div>
        </div>
 
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}
目前新的頁面就是將註冊與登入放一起了,其中要注意的是:
  1.  Model 的宣告是 GroupViewModels 這個組合的 ViewModel
  2. 兩個 Form 的 Action 分別傳入到各自的 Action 並不是傳到 Combine 的Post Action
  3. 強型別 HtmlHelper 的部分多了 Model 的屬性名稱
    1. model.Login.Email
    2. model.Register.Email

這時候您可以再次嘗試使用「登入」或「註冊」功能(本範例選擇登入)

各位讀者可以發現值沒有正確的接到,這主要的問題在於因為我們使用了組合的 ViewModel ,造成實際要對應的屬性名稱中間還多了一層,仔細看原始碼也可以明確的發現

有許多開發者到這裡就卡死了,而轉去選擇讓「登入」與「註冊」都是傳遞到 Combine Action[Post],然後在 後端程式碼判斷使用者要的是哪個功能,這樣不但造成程式碼複雜度增加也嚴重低估了 ASP.NET MVC 的 ModelBinder 能力。正確的解決方式非常簡單,只需要指定 ModelBinder 時的 Prefix 就可以了

[HttpPost]
public ActionResult Login([Bind(Prefix = "login")]LoginViewModels model)
{
    return View();
}

口說無憑,來看影片吧

經由以上的示範,各位開發者應該對於 ViewModel 是複雜型別的時候該怎麼解決有個底了

如果你對以上的程式有興趣的話可以點擊下方連結觀看原始程式

 https://github.com/demofan/GroupViewModelSample

回應討論