如何綁定可動態新增或移除之資料集合(EditorTemplate)
前言
客戶需要可動態輸入數組買家及地址的輸入介面。所以筆者將設計一個ViewModel來存放訂單資料清單(List),而訂單(Order)就包括買家姓名(Buyer)及地址(Address)屬性,並且透過定義Order的EditorTemplate來將資料呈現於畫面上,並且在Submit時將一系列資料Binding到ViewModel的訂單清單中。
示意程式碼如下所示
public ActionResult Create()
{
// Get view model
OrderViewModel viewModel = new OrderViewModel();
// Do somthing here for the view model
// ...
// Return view model
return View(viewModel);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(OrderViewModel viewModel)
{
if (ModelState.IsValid)
{
// save
}
return View("Index);
}
// view model
public class OrderViewModel
{
public List<Order> Orders { get; set;}
}
public class Order
{
public string Buyer { get; set;}
public string Address { get; set;}
}
EditorTemplate - Order
<!-- group box - Order -->
<fieldset class="well the-fieldset ">
<legend class="the-legend">Order</legend>
<table width="100%">
<tr>
<!-- input - Buyer -->
<td>@Html.LabelFor(model => Model.Buyer, htmlAttributes: new { @class = "control-label" })</td>
<td>@Html.EditorFor(model => Model.Buyer, new { htmlAttributes = new { @class = "form-control input-sm" } })</td>
</tr>
<tr>
<!-- input - Address -->
<td>@Html.LabelFor(model => Model.Address, htmlAttributes: new { @class = "control-label" })</td>
<td>@Html.EditorFor(model => Model.Address, new { htmlAttributes = new { @class = "form-control input-sm" } })</td>
</tr>
</table>
</fieldset>
動態新增刪除所面臨之挑戰
如何透過ViewModel將複雜資料結構化的呈現在頁面上
PartialView: 僅將資料傳入 (需自行處理prefix好讓資料回傳時可依名稱自動Binding至ViewModel)
EditorTemplates: 不須自行處理即可獲得正確prefix,且享有單筆及多筆資料自動呈現的便捷性。
實作可參考 如何使用樣板(EditorTemplates)綁定(Binding)整個List資料進行編輯
(當然是使用EditorTemplates來處理囉!!)
如何設定Ajax新增資料的名稱Prefix符合MVC Model Array Binding規範
透過前述EditorTemplate的幫忙,我們可以輕易地將ViewModel中多筆訂單(List<Order>)產出於View中進行各訂單買家(Buyer)及買家住址(Address)編輯工作,以下為產出的Html碼。由於該input element命名都有依循固定規則,所以當我們將資料傳回Server時,透過MVC Model Binding機制讓我們可輕易地從ViewModel取得資料。
<input type="text" name="Orders[0].Address" />
<input type="text" name="Orders[1].Buyer" />
<input type="text" name="Orders[1].Address" />
當我們要利用Ajax動態新增一筆資料時,就必須依循相同規則產生序號+1的資料如下。其實這是相當擾人的事情,是否我們需要先從頁面上找出當前最後的序號? 然後再硬組出以下的Html?
<input type="text" name="Orders[2].Address" />
假設這三組訂單資訊是可供用使用者自行刪除,試想以下狀況,在資料傳回時將只會有一組資料而已,因為序號中斷而無法正確Binding此一系列的資料
<input type="text" name="Orders[0].Address" />
<input type="text" name="Orders[2].Buyer" />
<input type="text" name="Orders[2].Address" />
好在微軟允許開發者使用非序號方式定義List型態的資料,透過一個hidden的input來定義Order陣列的Index,所以在Model Binding時就可以進行識別而將所有Order的資料綁定至ViewModel中的訂單陣列。但可惜的是微軟目前沒有提供此方式的HtmlHelper方法,所以稍後我們將自行定義一組擴充方法以達成目的。
<input type="text" name="Orders[MyOrderOne].Buyer" />
<input type="text" name="Orders[MyOrderOne].Address" />
<input type="hidden" name="Orders.Index" value="MyOrderTwo" />
<input type="text" name="Orders[MyOrderTwo].Buyer" />
<input type="text" name="Orders[MyOrderTwo].Address" />
解決方案
首先,定義一組使用非序號方式產出陣列資料的HtmlHelper擴充方法。其中hasPrefix是針對比較複雜的結構而定義,通常在複雜結構中子物件本身就會包函父物件的Prefix,例如ViewModel有許多Order,Order又有許多的Product,因此Product就會有類似Orders[0].Products[0].Name的結構,再呼叫EditFor(m=>m.Name)時EditorTemplate會自動產生Prefix(Orders[0].Products[0].),所以只需再呼叫EditFor的當下提供本身的FieldName即可,否則會出現重複的Prefix造成資料Binding的錯誤。
{
var items = expression.Compile()(html.ViewData.Model);
var sb = new StringBuilder();
var hasPrefix = false;
if (String.IsNullOrEmpty(htmlFieldName))
{
var prefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
hasPrefix = !String.IsNullOrEmpty(prefix);
htmlFieldName = (prefix.Length > 0 ? (prefix + ".") : String.Empty) + ExpressionHelper.GetExpressionText(expression);
}
foreach (var item in items)
{
var dummy = new { Item = item };
var guid = Guid.NewGuid().ToString();
var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, expression.Parameters);
sb.Append(String.Format(@"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldName, guid));
sb.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", hasPrefix?ExpressionHelper.GetExpressionText(expression):htmlFieldName, guid)));
}
return new MvcHtmlString(sb.ToString());
}
接著就可以在View中使用啦
<!-- Orders -->
@Html.EditorForMany(model => model.Orders)
</div>
呈現出的效果就如同以下的Html
<input id="Orders_d4150ea4-5e46-411a-848a-19a00c88ea3e__Buyer" name="Orders[d4150ea4-5e46-411a-848a-19a00c88ea3e].Buyer" type="text" >
<input id="Orders_d4150ea4-5e46-411a-848a-19a00c88ea3e__Address" name="Orders[d4150ea4-5e46-411a-848a-19a00c88ea3e].Address" type="text" >
<input type="hidden" name="Orders.Index" value="4c93fd01-fc1b-4ec6-91d6-2db3d5c4d1da" />
<input id="Orders_4c93fd01-fc1b-4ec6-91d6-2db3d5c4d1da__Buyer" name="Orders[4c93fd01-fc1b-4ec6-91d6-2db3d5c4d1da].Buyer" type="text" >
<input id="Orders_4c93fd01-fc1b-4ec6-91d6-2db3d5c4d1da__Address" name="Orders[4c93fd01-fc1b-4ec6-91d6-2db3d5c4d1da].Address" type="text" >
此時我們就可以簡單地寫個Ajax來Append新資料至頁面,大致如下。
jQuery
$.ajax({
type: "GET",
url: '@Url.Action("AddOrder", "Order")',
traditional: true,
data: "prefix=Orders",
success: function (resultHTML) {
$('#orderContainer').append(resultHTML);
},
error: function () {
alert("處理失敗!");
}
});
});
Controller
{
AddOrderViewModel viewModel = new AddOrderViewModel()
{
Orders = new List<Order>() { new Order() },
Prefix = prefix
};
return View(viewModel);
}
ViewModel
{
public List<Order> Orders { get; set; }
public string Prefix { get; set; }
}
View - AddOrder
@using Test.Infrastructure.Mvc.Extensions
@{
Layout = null;
}
@Html.EditorForMany(model => model.Orders, Model.Prefix)
當我們按下新增鍵時,就會產生一組訂單輸入資料集。如下所示,由於陣列Index是沒有序號順序的,所以在操控這些資料集時就自由了許多,可隨意新增或刪除都不會影響資料回傳時Model Binding的結果。
<input id="Orders_29a9aa12-42f5-4925-b095-fa81ec9ff064__Buyer" name="Orders[29a9aa12-42f5-4925-b095-fa81ec9ff064].Buyer" type="text" >
<input id="Orders_29a9aa12-42f5-4925-b095-fa81ec9ff064__Address" name="Orders[29a9aa12-42f5-4925-b095-fa81ec9ff064].Address" type="text" >
延伸閱讀
最後,若有需要在後端進行驗證並透過ModelState將錯誤傳出的朋友,可能會發現前端自動產生的Array GUID Index目前是沒有辦法被後端取得,所以造成在新增ModelState錯誤時不知道要如何設定Key值來與前端對應。因應此問題可以參考 動態物件集合綁定之後端驗證ModelState探討 有進一步的說明。
參考資料
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !