A=123&B=abc&C=<Root><A>123</A><B>abc</B><C>kkk</C><D>iii</D></Root>
其中A與B是普通字串,而C是XML字串
在Visual Studio 2013裡有個好用的功能,可以將XML或JSON字串轉成Class,只要使用選擇性貼上即可。底下就是寫好的一個使用者參數的Model:
public class SendModel { [Required(AllowEmptyStrings = false, ErrorMessage = "{0}不可為空")] [StringLength(3, MinimumLength = 3, ErrorMessage = "{0}長度必須為{2}")] public string A { get; set; } [Required(AllowEmptyStrings = false, ErrorMessage = "{0}不可為空")] [StringLength(3, MinimumLength = 3, ErrorMessage = "{0}長度必須為{2}")] public string B { get; set; } [Required(AllowEmptyStrings = false, ErrorMessage="xml不可為空,請檢查格式是否正確")] public SendXmlModel C { get; set; } //這個類別就是將XML貼上之後所產生的,格式相同就可序列化成字串或反序列化回該類別的物件 [Serializable] [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] [System.Xml.Serialization.XmlRootAttribute("Root", Namespace = "", IsNullable = false)] public partial class SendXmlModel { public string A { get; set; } public string B { get; set; } public string C { get; set; } public string D { get; set; } } }
其中SendXmlModel就是我們將XML以選擇性貼上之後,Visual Studio自動幫我們產生的Class。
接下來我們產生新的Controller,裡面有個自訂的Action:
[HttpPost] [ValidateInput(false)] public ActionResult Test(SendModel model) { if (!ModelState.IsValid) { return Content("error"); } return Content("aaa"); }
設定好之後,我們就將指定好的參數發送到這個Action裡面,它會自動為我們將參數綁定到指定好的Model裡面,而C只要XML格式對,我想應該會自動轉成XendXmlModel的物件才是。
但結果會是顯示error!參數也都對啊,但為什麼還是會報錯呢?
仔細看錯誤訊息:
A與B都有正確綁定到Model內,但C是null...
看來是因為我們的Model裡面,C是屬於自訂的Class,預設的模型綁定沒辦法處理...問題應該就是在那個「型別轉換器」上了!
為了這個問題我找了好幾天,網路上很難找到有類似的案例,後來終於查到,要自訂一個型別轉換器,也就是新的ValueProvider讓程式知道這個參數的型別才行(也就是錯誤訊息裡說的型別轉換器)。
所以我們來寫一個自訂的ValueProviderFactory,繼承ValueProviderFactory:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace WebApplication3.Provider { public class MyValueProviderFactory : ValueProviderFactory { public override IValueProvider GetValueProvider(ControllerContext controllerContext) { } } }
接著再新增自訂ValueProvider,繼承IValueProvider介面:
public class StringToXmlValueProvider : IValueProvider { private HttpContextBase httpContext; public StringToXmlValueProvider(HttpContextBase httpContext) { this.httpContext = httpContext; } public bool ContainsPrefix(string prefix) { return prefix.Contains("C"); //指定如果參數名稱是C,則回傳true } public ValueProviderResult GetValue(string key) { if (!ContainsPrefix(key)) { return null; } //參數如果是C,則進行底下轉換 string _xml = httpContext.Request[key]; SendXmlModel xml; try { xml = SerializeTool.XmlDeserialize<SendXmlModel>(_xml); } catch { xml = null; } return new ValueProviderResult(xml, _xml, System.Globalization.CultureInfo.CurrentCulture); } }
將兩個整合:
public class MyValueProviderFactory : ValueProviderFactory { public override IValueProvider GetValueProvider(ControllerContext controllerContext) { return new StringToXmlValueProvider(controllerContext.HttpContext); } public class StringToXmlValueProvider : IValueProvider { private HttpContextBase httpContext; public StringToXmlValueProvider(HttpContextBase httpContext) { this.httpContext = httpContext; } public bool ContainsPrefix(string prefix) { return prefix.Contains("C"); //指定如果參數名稱是C,則回傳true } public ValueProviderResult GetValue(string key) { if (!ContainsPrefix(key)) { return null; } //參數如果是C,則進行底下轉換 string _xml = httpContext.Request[key]; SendXmlModel xml; try { xml = SerializeTool.XmlDeserialize<SendXmlModel>(_xml); //這裡使用一個泛型的XML序列化與反序列化工具,程式碼最後會補上 } catch { xml = null; } return new ValueProviderResult(xml, _xml, System.Globalization.CultureInfo.CurrentCulture); } } }
最後將MyValueProviderFactory註冊到Global.asax的Application_Start()內,使用插入的方式,讓自訂的ValueProviderFactory可以優先被搜尋到:
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); ValueProviderFactories.Factories.Insert(0, new MyValueProviderFactory()); } }
再跑一下,可看到已經正常綁定了:
這樣就正常囉!!但要注意的是,我們在自訂的ValueProvider裡面,只有判斷參數名稱是「C」的時候,才要進行轉換,所以如果有這需求的話,參數名稱要注意一下使用相同名稱。
事情到這邊,基本綁定就OK了,但我還有另一個特殊需求,必須檢查A和B參數的值,是否和C XML裡面的A與B參數是否相同,在Model裡面使用Compare驗證是沒有辦法的,所以我們必須再寫個自訂的模型綁定才行...
新增一個自訂類別MyBinder,繼承DefaultModelBinder:
public class MyBinder : DefaultModelBinder { }
再來覆寫原有的BindModel方法,這裡判斷如果Model類型是我們自訂的SendModel,才要做處理,否則回傳預設的BindModel:
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { if (bindingContext.ModelType == typeof(SendModel)) { } else { return base.BindModel(controllerContext, bindingContext); } }
完成後的完整程式:
public class MyBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { if (bindingContext.ModelType == typeof(SendModel)) { //取得參數 var request = controllerContext.HttpContext.Request; var reqA = request["A"]; var reqB = request["B"]; var _reqC = request["C"]; SendXmlModel reqC; try { reqC = SerializeTool.XmlDeserialize<SendXmlModel>(_reqC); } catch { reqC = null; } //建立新的ModelBindingContext ModelBindingContext NewBindingContext = new ModelBindingContext() { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType( () => new SendModel() { A = reqA, B = reqB, C = reqC }, typeof(SendModel)), ModelState = bindingContext.ModelState, ValueProvider = bindingContext.ValueProvider }; //加入自訂的模型驗證與ModelState錯誤訊息 if (reqC != null) { if (reqA != reqC.A) { NewBindingContext.ModelState.AddModelError("A", "參數A必須與XML內的A相符"); } if (reqB != reqC.B) { NewBindingContext.ModelState.AddModelError("B", "參數B必須與XML內的B相符"); } } //回傳BindModel return base.BindModel(controllerContext, NewBindingContext); } else { return base.BindModel(controllerContext, bindingContext); } } }
完成後一樣要在Global.asax註冊:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); ValueProviderFactories.Factories.Insert(0, new MyValueProviderFactory()); ModelBinders.Binders.Add(typeof(SendModel), new MyBinder()); }
接著在Action裡面,也要用我們指定的綁定模型才行:
[HttpPost] [ValidateInput(false)] public ActionResult Test([ModelBinder(typeof(MyBinder))] SendModel model) { if (!ModelState.IsValid) { return Content("error"); } return Content("aaa"); }
來試試看,假設A參數與XML內的A不同的話,ModelState會回報驗證不通過:
這樣就完成我們的要求了。
底下是好用的泛型將XML序列化或反序列化工具:
using System.Xml; using System.Xml.Serialization; public class SerializeTool { /// <summary> /// 以UTF-8編碼將XML物件序列化成字串 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="value"></param> /// <returns></returns> public static string SerializeXml<T>(T value) { if (value == null) { return null; } XmlSerializer ser = new XmlSerializer(typeof(T)); XmlWriterSettings settings = new XmlWriterSettings(); settings.OmitXmlDeclaration = true; settings.NamespaceHandling = NamespaceHandling.Default; settings.Encoding = Encoding.UTF8; XmlSerializerNamespaces nspace = new XmlSerializerNamespaces(); nspace.Add("", ""); StringBuilder sb = new StringBuilder(); using (XmlWriter xmlWriter = XmlWriter.Create(sb, settings)) { ser.Serialize(xmlWriter, value, nspace); } return sb.ToString(); } /// <summary> /// 將XML字串反序列化成指定型別物件 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="xml"></param> /// <returns></returns> public static T DeserializeXml<T>(string xml) { if (string.IsNullOrEmpty(xml)) { return default(T); } XmlSerializer ser = new XmlSerializer(typeof(T)); XmlReaderSettings settings = new XmlReaderSettings(); // No settings need modifying here using (StringReader textReader = new StringReader(xml)) { using (XmlReader xmlReader = XmlReader.Create(textReader, settings)) { return (T)ser.Deserialize(xmlReader); } } } }
參考資料:
http://stackoverflow.com/questions/5820637/custom-model-binding-model-state-and-data-annotations
http://www.codeproject.com/Articles/605595/ASP-NET-MVC-Custom-Model-Binder
http://donovanbrown.com/post/How-to-create-a-custom-Value-Provider-for-MVC.aspx
http://www.dotblogs.com.tw/maev85/archive/2010/09/16/17772.aspx
沒有留言:
張貼留言