ASP.NET MVC CRUD之路 (3) Decimal Binding問題及ModelBinder擴充應用
2 |
試做ASP.NET MVC CRUD沒多久就碰到個小釘子,原因是我在Model中將金額欄位定義成decimal型別(古有明訓: 算錢用浮點,遲早被人扁) ,ASP.NET MVC在Post回Controller時,會自動將傳回欄位一一對應到Method的輸入參數上,遇到decimal時出了點狀況。
用一個簡單的範例來重現問題。Index.cshtml內容如下,按下Submit鈕時會呼叫HomeController的Test Method出來處理。
@using (Html.BeginForm("Test", "Home")) {
<div>
<span>Amount:</span>
@Html.TextBox("amount")
</div>
<input type="submit" value="Submit" />
}
而HomeController.cs中宣告了Test(decimal amount)承接前端傳回的amount參數。
using System.Web.Mvc;
namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "Welcome to ASP.NET MVC!";
return View();
}
public ActionResult Test(decimal amount)
{
return View(amount);
}
}
}
輸入1,234.56,按下Submit鈕,爆出以下錯誤。
The parameters dictionary contains a null entry for parameter 'amount' of non-nullable type 'System.Decimal' for method 'System.Web.Mvc.ActionResult Test(System.Decimal)' in 'MvcApplication1.Controllers.HomeController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
Parameter name: parameters
Phil Haack有篇Model Binding Decimal Values說明了這算ASP.NET MVC3的一個Bug(有趣的是,Phil所遇到的問題來自文化差異,巴西慣用"1.234,56"格式的數字標示,跟我們常用的”.”與”,",意義剛好相反),目前的解決方法是為decimal型別額外註冊ModelBinder,自訂string轉成decimal的邏輯。
我做了一點點改寫,把string –> object的部分抽離出來,變成建構式的傳入參數,定義了FlexModelBinder(等一下會示範要如此設計的好處),如此,傳入不同的Func<string, object>就可以實現各種型別的ModelBinder囉!
using System;
using System.Web.Mvc;
using System.Web.Routing;
using System.Globalization;
namespace MvcApplication1
{
public class MvcApplication : System.Web.HttpApplication
{
//...省略...
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
//加上String->Decimal的轉換函數
ModelBinders.Binders.Add(
typeof(decimal),
new FlexModelBinder(
s =>
Convert.ToDecimal(s, CultureInfo.CurrentCulture)
));
}
}
//將轉換核心抽出來變成Func<string, object>當成初始化參數
//傳入不同轉換函數,就可以變成不同型別的ModelBinder
public class FlexModelBinder : IModelBinder
{
Func<string, object> _convFn = null;
public FlexModelBinder(Func<string, object> convFunc)
{
_convFn = convFunc;
}
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
actualValue = _convFn(valueResult.AttemptedValue);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
}
如此,即便輸入1,234.56也能正確傳到Controller端囉!
約略了解ASP.NET MVC ModelBinder原理後,我們可以玩一些更有趣的應用。例如: 將Javascript物件陣列轉成JSON字串Post回Controller,Controller Method直接用List<SomeObject>作為參數型別接回料。
舉個實例,假設我們在C#裡定義一個資料類別ScoreRecord:
public class ScoreRecord
{
public string UserId { get; set; }
public int Score { get; set; }
}
在HomeController裡宣告Index(List<ScoreRecord> scores),大大方方地直接將HTML Form傳回的scores欄位視為List<ScoreRecord>讀入,再將List內容轉成字串透過ViewBag.Result抛回前端。
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
namespace MvcLab.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Result = "Waiting for input!";
return View();
}
[HttpPost]
public ActionResult Index(List<ScoreRecord> scores)
{
ViewBag.Result = string.Join("\n",
scores.Select(
o =>
string.Format("<li>{0} : {1}</li>",
o.UserId, o.Score)).ToArray()
);
return View();
}
public ActionResult About()
{
return View();
}
}
}
至於Index.cshtml,宣告一個<textarea name="scores">用來傳資料,寫一小段Javascript,建立Javascript版的物件陣列,一樣使用UserId及Score當屬性名稱,轉成JSON字串後存入<textarea name=”scores”>,按Submit時傳回後端。
@using (Html.BeginForm())
{
<textarea name="scores" id="scores" style="width: 200px; height: 80px;">
</textarea>
<input type="submit" value="Submit" />
<script type="text/javascript">
$(function () {
var scores = [
{ UserId: "Jeffrey", Score: 9999 },
{ UserId: "Darkthread", Score: 32767 }
];
$("#scores").val(JSON.stringify(scores));
});
</script>
<ul>
@Html.Raw(ViewBag.Result)
</ul>
}
再施一點點魔法,就可實現"HTML Form傳回JSON字串,Controller直接收取.NET物件參數"的美妙結果!
魔法是什麼? 其實,我只是在Global.asax.cs原本註冊decimal轉換函數的地方,為List<ScoreRecord>也註冊一段使用JavaScriptSerializer的轉換JSON字串為物件的邏輯罷了。因為有FlexModelBinder,這裡只需要寫一行Code就搞定了。
ModelBinders.Binders.Add(
typeof(List<ScoreRecord>),
new FlexModelBinder(
s =>
(new JavaScriptSerializer()).Deserialize<List<ScoreRecord>>(s)
));
為ASP.NET MVC架構的可擴充性按一個【讚】! (呵! 我好像每天都在為.NET按讚,不過,就真的很讚咩!)
Comments
# by 心爾
黑大真是厲害,這系列豬走路文章算是開了眼,不過小弟還是不知道如何有效率與有系統的學 asp.net mvc 該如何著手,不知會否出入門系列文章與實際應用面的文章,若是無理詢問,不理即可。
# by Jeffrey
to 心爾,我目前已開始嘗試將ASP.NET MVC小規模應用在工作專案上,會陸續寫一些開發經驗分享,但會以自己開發要克服的問題為主,不太能當成系統化學習的入門教材。若要系統化學習,保哥有一本中文書,另外http://www.asp.net/mvc/videos 有不少教學錄影(多是MVC2時代錄的,但MVC3基本原理沒有差太多),雖然是英文,但蠻淺顯的,看畫面操作就可以懂,也值得看看。