[ASP Net MVC] 如何綁定可動態新增或移除之資料集合(EditorTemplate)

如何綁定可動態新增或移除之資料集合(EditorTemplate)

前言

 

客戶需要可動態輸入數組買家及地址的輸入介面。所以筆者將設計一個ViewModel來存放訂單資料清單(List),而訂單(Order)就包括買家姓名(Buyer)及地址(Address)屬性,並且透過定義Order的EditorTemplate來將資料呈現於畫面上,並且在Submit時將一系列資料Binding到ViewModel的訂單清單中。

 

image

 

示意程式碼如下所示


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://www.mattlunn.me.uk/blog/2014/08/how-to-dynamically-via-ajax-add-new-items-to-a-bound-list-model-in-asp-mvc-net/

http://stackoverflow.com/questions/14038392/editorfor-ienumerablet-with-templatename/18974918#18974918

http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !