動態物件集合綁定之後端驗證ModelState探討
前言
前情提要一下,筆者先前在處理動態物件集合的資料綁定問題時,發現可以利用非序號方式來定義List型態資料,透過一個hidden的input來定義Order陣列的Index,解決了原本動態集合新增刪除後Index跳號造成資料的綁定不全;而在實作上為了避免該Index字串有重複的問題,所以一律使用GUID作為Array Index的方式來進行,大略示意圖如下所示。如果不是很了解此運作方式,請先參考 如何綁定可動態新增或移除之資料集合 有較仔細地說明。
問題發生
當筆者在進行Server端的資料驗證時,發現集合中某筆資料錯誤需要再ModelState中增加一筆錯誤資訊,就在這一刻發現事有蹊俏! 大家都知道新增錯誤資訊時需要填入Key及Error Message String,如果Key與ViewModel中屬性的名稱相符就會自動在該屬性相對應的地方呈現錯誤訊息( @Html.ValidationMessageFor),想當然爾這是最好提示用戶錯誤的地方,但我目前所擁有的資訊就是該物件集合Binding到我ViewModel的List<Order>物件,我怎麼會知道每次動態產生的GUID是什麼哩? 就算我知道了每次產出的GUID,我還要保持View中集合物件的各個GUID Index不變動,才好讓我辛苦建立的Model State Error Message順利出現在正確的位置上。以下是原本的解決方案,後續會依照需求進行修改,請參照。
解決方式
大致分為兩個方向。第一需要在後端也能取得前端產生的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數值至該屬性中。
最後在View中加入@Html.HiddenFor(model => model.FrontEndArrayIndex)。實際產出之HTML如下所示,新增了一個Hidden Input來記錄Array GUID Index資訊。
有條件地固定前端Array Index資訊
既然我們已經定義了一個FrontEndArrayIndex屬性來記錄Array GUID Index了,所以我們就可以依照該值來決定是否使用相同的GUID Index或者建立新的GUID Index。首先利用反射取得該FrontEndArrayIndex屬性,如果沒有此屬性或該屬性內容為NULL則表示使用者無此需求,當然就直接建立一組新的GUID Index來使用;反之若存在此屬性且有值,則將此值當作預設GUID Index,達到我們有條件固定Index目標。
使用方式
最後在後端驗證邏輯中,只要有需要新增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());
}
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !