- 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
沒有留言:
張貼留言