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




沒有留言:
張貼留言