MVC5模型繫結 - 1

by vivid 8. 三月 2017 12:08

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170318101
出刊日期: 2017/03/08

Model Binder是一個MVC的元件,根據HTTP請求傳送到伺服端的資料,來建立模型物件。MVC內建一個預設的模型繫結器(Default Model Binder),讓我們可以很容易的取得HTTP請求中的資料,本文將介紹ASP.NET MVC 5預設模型繫結器(Default Model Binder)的基本應用。

 

了解模型繫結

模型繫結器(Model Binder)可以將資料傳送到控制器行動方法的參數之中,預設的模型繫結器(Default Model Binder)會拿HTTP請求中參數的名稱與控制器行動方法參數名稱進行比對,將名稱相符的HTTP請求的值,複製到行動方法的參數。如此可以少寫許多抓取資料的程式碼。

預設的模型繫結器是一個DefaultModelBinder類型的物件,按照以下優先順序(由高到低),搜集HTTP請求中的資料,將之傳入行動方法參數:

  1. 表單資料(Form Data)
  2. 路由(Route)資料
  3. 查詢字串(Query String)
  4. ·檔案,為簡單起見,本文先不討論檔案上傳部分。

從路由取得資料

為了方便說明,我們先使用Visual Studio 2015來建立MVC 5的網站,從「File」-「New」-「Project」,在「New Project」對話盒中,確認視窗上方.NET Framework的目標版本為「.NET Framework 4.6」以上,選取左方「Installed」-「Templates」-「Visual C#」程式語言,從「Web」分類中,選取「ASP.NET Web Application(.NET Framework)」,適當設定專案名稱與專案存放路徑,按下「OK」鍵,請參考下圖所示:

clip_image002

圖 1:建立MVC 5專案。

在「New ASP.NET Web Application」對話盒中選取「MVC」,勾選下方的「MVC」項目,然後按一下畫面中的「OK」 按鈕,請參考下圖所示:

clip_image004

圖 2:勾選下方的「MVC」項目。

預設在專案中會包含一個RouteConfig.cs檔案,其中的RouteConfig類別定義MVC專案預設的路由,路由中包含一個「id」參數,參考以下範例程式碼:

namespace ModelBindingDemo
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

 

先小試一下預設Model Binder的功能,讓我們修改HomeController類別的Index方法,讓它傳入一個名為「id」的參數,並將它的值放到ViewBag之中傳到檢視,參考以下範例程式碼:

public class HomeController : Controller
{
    public ActionResult Index(int? id)
    {
        ViewBag.result = id;
        return View();
    }
}

 

修改Index.cshtml檔案,在Index檢視中將控制器傳來的ViewBag資料取出,顯示在畫面上,參考以下範例程式碼:

@{
    ViewBag.Title = "Home Page";
}

<h1>
    @ViewBag.result
</h1>


按CTRL + F5執行這個專案,執行時輸入以下URL,預設的模型繫結器(Default Model Binder)會拿HTTP請求路由中的{id}參數與控制器Index方法id參數進行比對,然後將路由中id指定的值,複製到Index行動方法的id參數:

http://localhost:46158/Home/Index/100

執行結果請參考下圖所示,URL中的100對應到路由中的{id}參數,因此在畫面中可以看到顯示100:

clip_image006

圖 3:使用路由讀取資料。

若不使用模型繫結器,我們需要手動地透過RouteValueDictionary物件來取得路由資料,若要達到上述範例的要求,可以使用RouteData.Values["id"]取出路由中{id}參數的值,參考以下範例程式碼:

public ActionResult Index()
{
    var id = RouteData.Values["id"];
    ViewBag.result = id;
    return View();
}

從查詢字串繫結資料

接下來我們來看看如何使用Model Binder從查詢字串取得資料,控制器的Index方法程式碼不變,仍舊傳入一個名為id的輸入參數,參考以下範例程式碼

public ActionResult Index(int? id)
{
    ViewBag.result = id;
    return View();
}

按CTRL + F5執行這個專案,執行時輸入以下URL,並使用查詢字串傳遞id參數值,預設的模型繫結器(Default Model Binder)便會將查詢字串中id對應的值「200」複製到Index方法中的id參數中:

http://localhost:46158/Home/Index?id=200

不意外的,此測試的結果和上個範例相同,執行結果請參考下圖所示:

clip_image008

圖 4:使用查詢字串繫結資料。

若不使用模型繫結器,我們需要透過Request.QueryString物件來取得資料,參考以下範例程式碼,執行的結果和上個範例是一樣的:

public ActionResult Index()
{
    var id = Request.QueryString["id"];
    ViewBag.result = id;
    return View();
}

 

從表單取得資料

接著我們來測試使用Model Binder取得表單資料,延續上個範例,控制器的Index方法程式碼不變,仍舊傳入一個名為id的輸入參數,參考以下範例程式碼

public ActionResult Index(int? id)
{
    ViewBag.result = id;
    return View();
}

修改Index檢視,使用Html.BeginForm()加入表單標籤,並利用一個Submit類型的按鈕將表單資料提交到伺服端,參考以下範例程式碼

@{
    ViewBag.Title = "Home Page";
}

<fieldset>
    <legend>表單</legend>
    @using (Html.BeginForm())
    {
        <text>請輸入資料:</text>@Html.TextBox("id")
        <input type="submit" value="送出表單" />
    }
</fieldset>

<h1>
    @ViewBag.result
</h1>


 

這個檢視一執行將會產生以下的HTML標籤,請特別注意,提交表單是根據標籤的name attribute,不是根據id Attribute來送資料,參考以下範例程式碼:

<fieldset>
    <legend>表單</legend>
    <form action="/Test/Index" method="post">
        請輸入資料:<input id="id" name="id" type="text" value="" />
        <input type="submit" value="送出表單" />
    </form>
</fieldset>

 

執行結果請參考下圖所示:

clip_image010

圖 5:表單繫結執行結果測試。

我們利用Chrome瀏覽器攔截按下「送出表單」按鈕送出的HTTP請求可以看到表單資料被打包成以下格式送到伺服端「id=100」,請參考下圖所示:

clip_image012

圖 6:攔截HTTP請求表單資料。

若不使用模型繫結器,我們需要透過Request.Form物件來取得資料,上述的範例可以改寫如為以下程式碼:

public ActionResult Index()
{
    var id = Request.Form["id"];
    ViewBag.result = id;
    return View();
}

 

繫結簡單型別

在前面的範例中,控制器Index方法傳入一個可為Null的id參數(Nullable),若修改控制器程式碼,讓Index方法接收一個int型別的id參數,參考以下範例程式碼:

public ActionResult Index(int id)
{
    ViewBag.result = id;
    return View();
}

 

 

執行Index方法時,若在瀏覽器輸入URL時,有傳遞路由的id參數,如以下:

http://localhost:46158/Home/Index/100

則程式碼可以順利執行,但若沒有傳遞id參數,例如輸入以下URL測試:

http://localhost:46158/Home/Index

將會得到一個錯誤訊息,參考如下:

The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.Web.Mvc.ActionResult Index(Int32)' in 'ModelBindingDemo.Controllers.HomeController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
Parameter name: parameters

執行結果請參考如下圖所示:

clip_image014

圖 7:未傳值例外。

這是因為預設的DefaultModelBinder試著將HTTP請求中的id路由資料(字串),轉型成int型別,因未提供值無法順利轉型,導致產生例外錯誤。若要修訂這個問題,可以將方法參數的型別宣告為Nullable,例如前文範例所示,或者給予方法參數一個預設值,例如以下範例程式碼:

public ActionResult Index(int id = 100)

{

ViewBag.result = id;

return View();

}

複雜型別的繫結-查詢字串

當行動方法的參數是一個自訂的型別時,DefaultModelBinder類別便使用反射(Reflection)技術,取得類別的公開屬性,並將搜集到的資料與屬性名稱做比對,將資料傳到屬性值之中。舉例來說,若有一Employee模型定義如下,包含EmployeeId、EmployeeName、BirthDate三個屬性,參考以下範例程式碼:

public class Employee

{

public int EmployeeId { get; set; }

public string EmployeeName { get; set; }

public DateTime BirthDate { get; set; }

}

在控制器定義一個Index方法,傳入Employee物件當參數,並在Index方法中將EmployeeId、EmployeeName、BirthDate三個屬性的值取出串接成字串放到ViewBag中傳遞到檢視,參考以下範例程式碼:

public class HomeController : Controller
{
    public ActionResult Index(Employee emp )
    {
        ViewBag.result = $@"
            Employee id : {emp.EmployeeId}
            Employee Name : {emp.EmployeeName}
            Birth Date : {emp.BirthDate}
            ";
        return View();
    }
}

 

在Index 檢視之中,再讀取出ViewBag的內容,顯示在網頁中,參考以下範例程式碼:

<h1>

@ViewBag.result

</h1>

執行Index方法,然後輸入以下URL測試,使用查詢字串傳遞EmployeeId、EmployeeName、BirthDate三個屬性的值:

http://localhost:46158/Home/Index?employeeid=1&employeename=mary&birthdate=2016/1/2

執行結果請參考如下圖所示,預設的模型繫結器,順利地根據key值將查詢字串的值截取出來,放到Employee物件與key值同名的屬性中:

clip_image016

圖 8:查詢字串繫結執行結果測試。

複雜型別的繫結-表單

當然在實際運行的網站中,我們不會讓使用者自行輸入查詢字串,一般常運用在開發階段測試功能是否正常,此時我們需要在網頁中使用表單,讓使用者填寫資料。修改控制器的程式碼,提供兩個Index方法,參考以下範例程式碼:

namespace ModelBindingDemo.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(Employee model)
        {
            return View("Result", model);
        }
    }
}


 

第一個Index方法回傳空白的Index檢視呈現空白表單以搜集使用者資料,標示HttpPost的第二個Index方法則接收一個Employee型別的參數,預設的Model Binder便將搜集到的表單資料,拿來和屬性名稱比對,並將名稱相符的資料值複製到Employee物件的屬性中,最後我們將還原的Employee物件丟到Result檢視來顯示。

Index檢視的內容如下,使用Html.EditorFor方法產生HTML標籤,預設這個方法會根據模型屬性的型別自動決定要產生出的HTML標籤。若屬性的型別為字串,則產生<Input type=text/>的HTML標籤;若屬性的型別為數值,則產生<Input type=number/>的HTML標籤,參考以下範例程式碼:

@model ModelBindingDemo.Models.Employee

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Employee</h4>
        <hr />
        <div class="form-group">
            @Html.LabelFor(model => model.EmployeeId, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.EmployeeId, new { htmlAttributes = new { @class = "form-control" } })
            </div>
        </div>

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

        <div class="form-group">
            @Html.LabelFor(model => model.BirthDate, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.BirthDate, new { htmlAttributes = new { @class = "form-control" } })
            </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>
}

 

Result檢視的程式碼如下所示,使用Html.DisplayNameFor 方法顯示提示字串,使用Html.DisplayFor方法來顯示屬性值:

@model ModelBindingDemo.Models.Employee

@{
    ViewBag.Title = "Result";
}

<h2>Result</h2>

<div>
    <h4>Employee</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.EmployeeId)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.EmployeeId)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.EmployeeName)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.EmployeeName)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.BirthDate)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.BirthDate)
        </dd>

    </dl>
</div>
<p>
    @Html.ActionLink("Back to List", "Index")
</p>

 

執行專案,在瀏覽器輸入以下URL測試:

http://localhost:46158/Home/Index

執行Index方法時會看到瀏覽器呈現以下Index檢視的內容,包含三個文字方塊讓你輸入EmployeeId、EmployeeName、BirthDate的值,請參考下圖所示:

clip_image018

圖 9:執行結果測試。

Index檢視執行後,瀏覽器收到的<Form>標籤如下,注意<Form>標籤中的三個Input方塊都設定了name的Attribute,表單會根據此Attribute來提交資料到伺服器:

<form action="/Home/Index" method="post"><input name="__RequestVerificationToken" type="hidden" value="...略" />    <div class="form-horizontal">
        <h4>Employee</h4>
        <hr />
        <div class="form-group">
            <label class="control-label col-md-2" for="EmployeeId">EmployeeId</label>
            <div class="col-md-10">
                <input class="form-control text-box single-line" data-val="true" data-val-number="The field EmployeeId must be a number." data-val-required="The EmployeeId field is required." id="EmployeeId" name="EmployeeId" type="number" value="" />
            </div>
        </div>

        <div class="form-group">
            <label class="control-label col-md-2" for="EmployeeName">EmployeeName</label>
            <div class="col-md-10">
                <input class="form-control text-box single-line" id="EmployeeName" name="EmployeeName" type="text" value="" />
            </div>
        </div>

        <div class="form-group">
            <label class="control-label col-md-2" for="BirthDate">BirthDate</label>
            <div class="col-md-10">
                <input class="form-control text-box single-line" data-val="true" data-val-date="The field BirthDate must be a date." data-val-required="The BirthDate field is required." id="BirthDate" name="BirthDate" type="datetime" value="" />
            </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>
</form>

 

我們利用Chrome瀏覽器攔截按下網頁中的「Create」按鈕送出的HTTP請求,可以看到表單資料被打包成以下格式送到伺服端:

EmployeeId=001&EmployeeName=Mary&BirthDate=2011%2F1%2F2

執行結果請參考如下圖所示:

clip_image020

圖 10:攔截HTTP請求檢視表單資料。

接著畫面會跳轉到Result檢視,從中便可看到使用者在Index檢視中輸入的資料正確地顯示在畫面上,請參考下圖所示:

clip_image022

圖 11:執行結果測試。

另外特別要注意,模型中欲讓Model Binder繫結的資料要宣告成屬性,不可以使用欄位語法,例如若將此例的Employee類別宣告如下,使用欄位語法定義EmployeeId、EmployeeName與BirthDate變數,那麼DefaultModelBinder就無法將表單資料還原成Employee物件,參考以下範例程式碼:

namespace ModelBindingDemo.Models
{
    public class Employee
    {
        public int EmployeeId;
        public string EmployeeName;
        public DateTime BirthDate;
    }
}

 

繫結部分屬性 - Include

假若為了某些理由,我們可能不想讓將搜集到的所有HTTP請求資料都被Model Binder複製到行動方法物件參數的屬性中,例如基於安全性考量,假設你的表單中只讓使用者輸入EmployeeId與EmployeeName的值,但惡意使用者可能會利用工具程式送出HTTP請求,假造表單資料包含BirthDate的值,那麼假造的值就會被Model Binder複製到Employee物件的BirthDate屬性,這樣的結果不是我們想要的,我們可以利用Bind Attribute的Include屬性來設定繫結白名單。

舉例來說,假設目前Index檢視程式碼如下所示:

@model ModelBindingDemo.Models.Employee

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Employee</h4>
        <hr />
        <div class="form-group">
            @Html.LabelFor(model => model.EmployeeId, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.EmployeeId, new { htmlAttributes = new { @class = "form-control" } })
            </div>
        </div>

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

        <div class="form-group">
            @Html.LabelFor(model => model.BirthDate, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.BirthDate, new { htmlAttributes = new { @class = "form-control" } })
            </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>
}

 

Result檢視顯示EmployeeId、EmployeeName、BirthDate三個屬性值,參考以下範例程式碼:

@model ModelBindingDemo.Models.Employee

@{
    ViewBag.Title = "Result";
}

<h2>Result</h2>

<div>
    <h4>Employee</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.EmployeeId)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.EmployeeId)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.EmployeeName)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.EmployeeName)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.BirthDate)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.BirthDate)
        </dd>

    </dl>

</div>
<p>
    @Html.ActionLink("Back to List", "Index")
</p>

 

修改控制器的程式碼,提供兩個Index方法,沒有參數的第一個Index方法主要的工作就是將檢視產生的HTML標籤送到瀏覽器;標識HttpPost Attribute,且包含一個Employee參數的Index方法,則利用Model Binder將HTTP請求送至的表單資料取出,填入Employee物件與表單資料同名的屬性,但透過Bind Attribute的Include屬性,設定只有繫結EmployeeId與EmployeeName兩個屬性,最後將Employee物件傳到Result檢視來顯示,參考以下範例程式碼:

namespace ModelBindingDemo.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index([Bind(Include = "EmployeeId,EmployeeName")]Employee model)
        {
            return View("Result", model);
        }
    }
}

 

執行結果請參考如下圖所示,首先在Index檢視填入員工EmployeeId、EmployeeName、BirthDate資料:

clip_image024

圖 12:執行結果測試。

畫面跳轉到Result檢視時,只有EmployeeId、EmployeeName的值顯示在畫面上,而BirthDate的值是Datetime物件的預設值,這代表預設Model Binder沒有將Index檢視中輸入的日期填入Employee物件的BirthDate屬性中,請參考下圖所示:

clip_image026

圖 13:執行結果測試。

繫結部分屬性 - Exclude

和上例相反,除了利用Bind Attribute的Include屬性設定要繫結的屬性白名單之外,我們可以使用Exclude屬性來設定排除繫結的黑名單。修改上例控制器標識為HttpPost的Index()方法,改用Exclude排除BirthDate,執行的結果將和上例相同,參考以下範例程式碼:

namespace ModelBindingDemo.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(
            [Bind(Exclude = "BirthDate")]Employee model)
        {
            return View("Result", model);
        }
    }
}

 

繫結部分屬性 – 模型

前面的範例在行動方法參數的前方加上Bind Attribute設定要繫結的白名單或黑名單屬性,因為Bind屬性套用在方法中的參數,只對這個方法有用,若有多個方法都有相同的Employee型別參數,我們可以在模型中套用Bind Attribute,參考以下範例程式碼,設定要繫結的黑名單屬性:

namespace ModelBindingDemo.Models
{
    [Bind(Exclude = "BirthDate")]
    public class Employee
    {
        public int EmployeeId { get; set; }
        public string EmployeeName { get; set; }
        public DateTime BirthDate { get; set; }
    }
}

 

或設定要繫結的白名單屬性,參考以下範例程式碼:

namespace ModelBindingDemo.Models
{
    [Bind(Include = "EmployeeId,EmployeeName")]
    public class Employee
    {
        public int EmployeeId { get; set; }
        public string EmployeeName { get; set; }
        public DateTime BirthDate { get; set; }
    }
}

Tags:

.NET Magazine國際中文電子雜誌 | ASP.NET MVC | 許薰尹Vivid Hsu

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List