[ASP Net MVC] 動態物件集合綁定之後端驗證ModelState探討

動態物件集合綁定之後端驗證ModelState探討

前言

 

前情提要一下,筆者先前在處理動態物件集合的資料綁定問題時,發現可以利用非序號方式來定義List型態資料,透過一個hidden的input來定義Order陣列的Index,解決了原本動態集合新增刪除後Index跳號造成資料的綁定不全;而在實作上為了避免該Index字串有重複的問題,所以一律使用GUID作為Array Index的方式來進行,大略示意圖如下所示。如果不是很了解此運作方式,請先參考 如何綁定可動態新增或移除之資料集合 有較仔細地說明。

 

image

 

 

問題發生

 

當筆者在進行Server端的資料驗證時,發現集合中某筆資料錯誤需要再ModelState中增加一筆錯誤資訊,就在這一刻發現事有蹊俏! 大家都知道新增錯誤資訊時需要填入Key及Error Message String,如果Key與ViewModel中屬性的名稱相符就會自動在該屬性相對應的地方呈現錯誤訊息( @Html.ValidationMessageFor),想當然爾這是最好提示用戶錯誤的地方,但我目前所擁有的資訊就是該物件集合Binding到我ViewModel的List<Order>物件,我怎麼會知道每次動態產生的GUID是什麼哩? 就算我知道了每次產出的GUID,我還要保持View中集合物件的各個GUID Index不變動,才好讓我辛苦建立的Model State Error Message順利出現在正確的位置上。以下是原本的解決方案,後續會依照需求進行修改,請參照。

 

image

 

 

解決方式

 

大致分為兩個方向。第一需要在後端也能取得前端產生的Array GUID Index資料,方便ModelState錯誤Key值的設定;再來需要有條件的固定住前端Array GUID Index的值,好讓我們在後端設定的ModelState錯誤Key值能夠與前端一致,讓錯誤訊息對應到正確的位置上。

 

 

取得前端自動產生之Array GUID Index

 

首先要取得透過自訂HtmlHelper擴充功能產生的Array GUID Index資訊,最快的方法就是直接Binding到該陣列Item上物件類別的屬性中;因此需在ViewModel集合裝載之物件類別中加入FrontEndArrayIndex屬性,讓前端產生的Array GUID Index可以綁定至物件內,方便取得要寫入ModelState的Key值。

{
	public List<Order> Orders { get; set;}
}
public class Order
{
	public string Buyer { get; set;}
	public string Address { get; set;}

	// binding front end array guid index
	public string FrontEndArrayIndex { get; set; }
}

 

再透過反射(Reflection)取得該屬性,並設定動態產生的Aarray GUID Index數值至該屬性中。

 

image

 

最後在View中加入@Html.HiddenFor(model => model.FrontEndArrayIndex)。實際產出之HTML如下所示,新增了一個Hidden Input來記錄Array GUID Index資訊。

 

image

 

 

有條件地固定前端Array Index資訊

 

既然我們已經定義了一個FrontEndArrayIndex屬性來記錄Array GUID Index了,所以我們就可以依照該值來決定是否使用相同的GUID Index或者建立新的GUID Index。首先利用反射取得該FrontEndArrayIndex屬性,如果沒有此屬性或該屬性內容為NULL則表示使用者無此需求,當然就直接建立一組新的GUID Index來使用;反之若存在此屬性且有值,則將此值當作預設GUID Index,達到我們有條件固定Index目標。

 

image

 

 

使用方式

 

最後在後端驗證邏輯中,只要有需要新增ModelState錯誤需求時,即可透過陣列中FrontEndArrayIndex屬性就可找到前端的Array GUID Index了,順利設定對應Key值好讓錯誤正確地出現在需要的位置上。

[ValidateAntiForgeryToken]
public ActionResult Create(OrderViewModel viewModel)
{
    if (ModelState.IsValid )
    {
        foreach (var order in viewModel.Orders )
        {
 			// if get some error in Address
 			string key = string.Format("Orders[{0}].Address", order.FrontEndArrayIndex);
 			ModelState.AddModelError(key, "Address Not Found")
        }
    }
    else
    {
        var errors = ModelState.Where(x => x.Value.Errors.Count > 0)
                    .Select(x => new { x.Key, x.Value.Errors }).ToArray();
    }

    return View(viewModel);
}

 

完整擴充功能如下

    Expression<Func<TModel, IEnumerable<TValue>>> expression, string htmlFieldName = null) where TModel : class
{
    var items = expression.Compile()(html.ViewData.Model);
    if (items == null)
    { return new MvcHtmlString(""); }

    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)
    {
        // get FrontEndArrayIndex property via reflection
        string currentGuid = string.Empty;
        var FrontEndArrayIndexPropertyInfo = item.GetType().GetProperty("FrontEndArrayIndex");
        if (FrontEndArrayIndexPropertyInfo!=null)
        { currentGuid = Convert.ToString( FrontEndArrayIndexPropertyInfo.GetValue(item)) ; }

        // get guid: create or use current guid
        var guid = string.IsNullOrWhiteSpace(currentGuid) ? Guid.NewGuid().ToString() : currentGuid;

        // set FrontEndArrayIndex 
        if (FrontEndArrayIndexPropertyInfo != null)
        { FrontEndArrayIndexPropertyInfo.SetValue(item, guid); }


        var dummy = new { Item = item };
        var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
        var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, expression.Parameters);
        var htmlFieldNameFixed = String.Format("{0}[{1}]", hasPrefix ? ExpressionHelper.GetExpressionText(expression) : htmlFieldName, guid);
        
        // define array guid index
        sb.Append(String.Format(@"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldName, guid));

        // EditorTemplates
        sb.Append(html.EditorFor(singleItemExp, null, htmlFieldNameFixed));
    }

    return new MvcHtmlString(sb.ToString());
}

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

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