情境如下, 在 ASP.NET MVC 用一小段程式顯示部門下拉清單,資料來自資料庫,因欄位較多且命名不直覺,我將由資料庫取得的集合轉成匿名型別 Select(o => new { DeptId = o.DI, DeptName = o.DN },再以 Razor 語法 @foreach (var dept in ViewBag.Depts) { <option value="@dept.DeptId">@dept.DeptName</option> } 轉成下拉選單選項。程式碼範例如下:
HomeController.cs
publicclass HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Depts = DataHelper.GetDepts()
.Select(o => new { DeptId = o.DI, DeptName = o.DN })
.ToList();
return View();
}
}
Index.cshtml
@{
Layout = null;
}
<!DOCTYPEhtml>
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>MVC Test</title>
</head>
<body>
<div>
<select>
@foreach (var d in ViewBag.Depts)
{
<optionvalue="@d.DeptId">@d.DeptName</option>
}
</select>
</div>
</body>
</html>
看似正常,執行時卻遇到錯誤,資料繫結(Data Binding)抱怨不認得匿名型別的 DeptId 屬性。
這才想到,我踩了匿名型別的紅線:參考
您無法將欄位、屬性、事件或方法的傳回類型,宣告為具有匿名類型。 同樣地,您無法將方法、屬性、建構函式或索引子的型式參數宣告為具有匿名類型。 若要以方法引數的形式來傳遞匿名類型或含有匿名類型的集合,您可以將參數宣告為物件(object)。 但是這樣做將失去強式類型的目的。如果您必須在方法界限外儲存或傳遞查詢結果,請考慮使用一般具名結構或類別來取代匿名類型。
匿名型別在 CSHTML 被當成 object 型別,無法滿足資料繫結的強型別需求。改用 @Newtonsoft.Json.JsonConvert.SerializeObject(ViewBag.Depts) 測試,驗證資料已正確傳到前端,其中差別在於 Json.NET 靠 Reflection 解析欄位可以正確讀取屬性,而資料繫結需要強型別。
有幾種解法:第一種是從 JavaScript 下手,既然資料能正確轉為 JSON,便可再轉為 JavaScript 物件陣列,用 Angular、Knockout 等 MVVM 框架可輕易繫結成下拉選單。[參考]
若想在伺服器端處理,第二種做法是放棄匿名型別,乖乖宣告一個 DeptInfo 之類的具名型別,其中定義 DeptId、DeptName 屬性做為 CSHTML 與 Controller 間的共通規格,這是最守規矩的正統解法。
如果你像我一樣崇尚簡潔勝過嚴謹,討厭為了一丁點限制搞出一堆只用一次的雞肋型別,可以參考看看我找到的第三種做法。(如果你也愛用 Dapper、Tuple,那我們應該是一國的)
在 Stackoverflow 找到一則相關討論,提到以前介紹過的既然要動態就動個痛快 - ExpandoObject,可以兼顧動態及強型別繫結要求。關鍵在於將匿名型別轉成 ExpandoObject,為求簡便,轉換程序可寫成擴充方法,再透過 .Select(o => new { DeptId = o.DI, DeptName = o.DN }.ToExpando()) 將匿名型別物件轉成 ExpandoObject 即可。ToExpando() 有個值得偷學的技巧:RouteValueDictionary 可將任何物件轉成 IDictionary<string, object>,再用 foreach + Add 方式將屬性複製到 ExpandObject 上,比 Reflection 寫法簡潔很多,但要記住 ExpandoObject 與 dynamic 背後仍是走 Reflection,留意可能的效能代價。
publicclass HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Depts = DataHelper.GetDepts()
.Select(o => new { DeptId = o.DI, DeptName = o.DN }.ToExpando())
.ToList();
return View();
}
}
publicstaticclass ExpandoExtensions
{
//http://stackoverflow.com/a/5670899/4335757
publicstatic ExpandoObject ToExpando(thisobject anonymousObject)
{
IDictionary<string, object> anonymousDictionary =
new RouteValueDictionary(anonymousObject);
IDictionary<string, object> expando = new ExpandoObject();
foreach (var item in anonymousDictionary)
expando.Add(item);
return (ExpandoObject)expando;
}
}
就這樣,匿名型別也可以在 CSHTML foreach 做資料繫結囉~
最後補充一點,匿名型別真的不能在不同方法間傳遞嗎?倒也未必,如下例,善用 dynamic 就可以克服:
不過,以上範例並不能解決本次案例遇到的狀況。CSHTML 雖然支援 dynamic 資料繫結(例如 ViewBag 本身就是 dynamic) ,但 foreach 時不適用,猜想與 foreach 情境的資料繫結實作方式有關,這部分就交給 ExpandoObject 搞定囉~
2017-01-11 補充
感謝 Dino 大大補充更簡便的做法,如果 foreach 的目的是要展開成 Text/Value 性質選項,有個 SelectList,可以透過 dataValueField、dataTextField 參數指定屬性名稱,轉成具有 Text、Value 屬性物件的集合:
@foreach (var dept innew SelectList(ViewBag.Depts, "DeptId", "DeptName"))
{
<option value="@dept.Value">@dept.Text</option>
}
順便補上剛才找到 mrkt 針對 MVC 下拉選單處理的一系列深入探討。