ASP.NET MVC - MVC和Web API之Model Binder的陷阱
其實也不能說陷阱啦,只能說小弟自己太笨,今天又踩進去一次 ( 記得以前也踩進過一次 ),所以決定在這邊紀錄一下,讓自己有個印象深刻的記憶QQ。
先說明一下,這個絕對不是ASP.NET MVC的Bug,只能說這種細節,一不小心就會撞到XDD
Model
首先我們先看看Model,這個Model簡單到爆炸,反正就是有Id、Name、Phone,然後利用DisplayName來定義未來要在View那邊顯示的資訊。
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;
namespace MvcOrz.Models
{
public class Customer
{
[DisplayName("ID")]
public int Id { get; set; }
[DisplayName("Name")]
public string Name { get; set; }
[DisplayName("Phone")]
public string Phone { get; set; }
}
}
就這麼簡單,接下來我們看一下Controller。
Controller
這個Controller其實也很簡單,就是兩個Action,第一個Action Index很簡單,就只是顯示出畫面,第二個Action Index2則會傳入兩個參數,第一個是id,也就是會去收網址後面的值,例如/Home/Index2/123,就會把123帶進去到id裡面;而第二個則會把View那頁Post或是其他方式進來的資料,自動轉成Customer Model。
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcOrz.Models;
namespace MvcOrz.Controllers
{
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
return View();
}
public ActionResult Index2(int id,Customer customer)
{
customer.Id = id;
return View();
}
}
}
基本上這還滿簡單的,接下來我們看一下View。
VIew
View也簡單到一個不行,幾乎都是利用HtmlHelper來利用Model產生需要的東西,但有個地方要注意到的,就是在Html.BeginForm的地方,我們會多加上一個RouterValue,並設定id = 1;
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
<div>
@using (Html.BeginForm("index2","Home", new { id = "1" }))
{
<fieldset>
<legend>文案</legend>
<dl>
<dt>@Html.LabelFor(model =>model.Id)</dt>
<dd>@Html.EditorFor(model => model.Id)</dd>
<dt>@Html.LabelFor(model => model.Name)</dt>
<dd>@Html.EditorFor(model => model.Name)</dd>
<dt>@Html.LabelFor(model => model.Phone)</dt>
<dd>@Html.EditorFor(model => model.Phone)</dd>
</dl>
</fieldset>
<input type="submit" value="Send" />
}
</div>
</body>
</html>
就這樣!非常簡單,而且我相信各位大家應該也猜得出梗了。
第一個
我們把整個專案執行起來,並且在Name和Phone填入3。
到後面我們利用中斷偵錯,我們會發現,沒錯!,第一個參數id,變成3了!,但實際上,我們預期的卻是1阿!
其實這個沒有甚麼好奇怪,因為ASP.NET MVC的預設就是這樣,會依據Model對應到的屬性名稱優先,不過,通常也不會有人會寫這樣的程式碼,因為id通常都是不能改的,所以通常會寫成第二個範例的樣子。
第二個
接下來第二個範例,我們把id拿掉了。
的確,這樣第一個id參數就會如預期的變成1了。
但是好景不長XDD,Customer裡面的Id也變成1了。
這樣子的狀況,某方面來講也不算是問題的,因為通常網址後面帶的id,也和我們準備要改的資料id是同樣的 ( 會在BeginForm裡面的RouterValue值換成當前的id ),其次為了安全性,我們也通常都會再加上Exclue,來過濾id,例如下面程式碼,所以幾乎不會有問題。
int id, [Bind(Exclude = "Id")]Customer customer)
{
customer.Id = id;
return View();
}
第三個就比較容易犯錯…
第三個
第三個範例,是笨笨小弟我今天踩到的地雷,這是一個Silverlight裡面的一段程式片段,程式的畫面如下。
當然這裡不會全部解釋,主要的地方是更新的區塊,小弟我利用WebRequest來對Web Service進行Http的PUT命令,詳細的程式碼小弟就不解釋了,反正重點在於,我先用取得id這個TextBox的值( idTbx.Text ),因為要知道id才能進行Update,接下來,也從TextBox裡面取得要變更的Name和Phone,然後設定WebRequest的URI為Web Service再加上剛剛取得的id ( 例如URI為api/Customer/1 ),然後就進行更新,程式碼在下面,其實也不用看懂,因為重點不是程式碼XD。
{
string id = idTbx.Text;
Customer customer = new Customer();
customer.Name = nameTbx.Text;
customer.Phone = phoneTbx.Text;
#region 使用WebRequest
WebRequest webRequest = WebRequestCreator.ClientHttp.Create(new Uri(_url + "/" + id));
webRequest.ContentType = "application/json";
webRequest.Method = "PUT";
webRequest.BeginGetRequestStream(requestAsyncCallback =>
{
Stream requestStream = webRequest.EndGetRequestStream(requestAsyncCallback);
string json = JsonConvert.SerializeObject(customer, Formatting.Indented);
byte[] buffer = System.Text.Encoding.Unicode.GetBytes(json);
requestStream.Write(buffer, 0, buffer.Length);
requestStream.Close();
webRequest.BeginGetResponse(responseAsyncCallback =>
{
WebResponse webResponse = webRequest.EndGetResponse(responseAsyncCallback);
using (StreamReader reader = new StreamReader(webResponse.GetResponseStream()))
{
string result = reader.ReadToEnd();
this.Dispatcher.BeginInvoke(() =>
{
MessageBox.Show(result);
});
}
}, null);
}, null);
#endregion
}
我們填入了這些值,希望針對id 1的Customer,將Name改為1,Phone也改為1。
結果看一下中斷點,變成0!!。
為什麼會這樣呢?ASP.NET MVC Web API裡面,第一個參數,應該就是URI後面的參數阿,我們的URI為api/Customer/1,那這裡的id應該為1阿,為什麼會為0呢?再看一下Customer變數。
沒錯Customer也為0,其實答案很簡單,那是因為在Silverlight裡面的程式碼段落,我們只有設定Customer.Name和Customer.Phone,而沒有設定Customer.Id,所以Customer.id就為0,如第一個範例一樣,預設上,ASP.NET MVC的模型繫節,會以Model為主,如果沒有對應到,才會以網址參數對應,但如果在Silverlight裡面補上Customer.Id = id的程式碼;就會讓Put的第一個參數id和Customer裡面的Id都有值了。
但其實到這邊,不管有無機率踩到。最好的方法,要嘛Model不要取名為id,要嘛把Router改一下。( 在Global.asax.cs檔案裡面 )
將下面的"{controller}/{action}/{id}", 的{id}改成新的名字,例如routerValueId。( 如果是Web Api,也要去改Web Api的地方,Web Api在這行 routeTemplate: "api/{controller}/{id}" ),下圖是ASP.NET MVC的地方,另外,別忘了後面的id也要改到,這樣如果沒有填入id值,就會自動幫我們設定預設值。
改成如下程式碼 ( 以下為MVC範例。 )
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{routerValueId}", // URL with parameters
new { controller = "Home", action = "Index", routerValueId}" = UrlParameter.Optional } // Parameter defaults
);
}
當然,別忘了所有Action的參數也全部都要改,例如Put的程式碼片段,但記得全部的地方都要改阿!
{
customer.Id = routerValueId;
if (!_repository.Update(customer))
{
//如果找不到,就拋出HTTP的Response例外,內容是尋找不到,也就是404
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}
除非很不幸的,連改了名子都會和Model的屬性撞到;不然這樣幾乎就不會有任何的問題了。
後記
其實真正的重點是希望大家能了解Model Binder的一些特性,其實只要了解了,但不了解的話,也真的是欲哭無淚,找不到問題,所以也在這邊po出來,希望大家能注意一下,Model Binder的一些小細節。