再談jQuery傳送物件JSON給ASP.NET MVC
2 | 16,920 |
使用jQuery傳送物件JSON到ASP.NET MVC的做法之前介紹過,但最近我在專案又遇到新難題。
例如有一個參數物件,ArgObject,內含Name屬性及SubArg屬性,SubArg有其專屬型別SubArgObject,基於特殊需要,SubArgObject使用[JsonProperty]及[JsonIgnore]自訂JSON轉換邏輯(實際專案用的是[JsonConverter(...)],此處簡化為[JsonProperty],指定PropB在JSON中需更名為PropX):
public class ArgObject
{
public string Name { get; set; }
public SubArgObject SubArg { get; set; }
}
public class SubArgObject
{
public string PropA { get; set; }
[JsonProperty("PropX")]
public string PropB { get; set; }
[JsonIgnore]
public string PropC { get; set; }
}
如果是Web API Controller,什麼都不用做就能順利接收參數。依Web API的Binding規則,參數如為複雜型別(Complex Type),將依Content-Type選擇適當的Media Type Formatter進行轉換[參考]。在本例中application/json會使用JsonMediaTypeFormatter,其核心為Json.NET,且已知轉換目標型別為ArgObject,故可精準轉換[JsonProperty("PropX")]無誤。
public class WebApiController : ApiController
{
public ArgObject PostJson(ArgObject args)
{
return args;
}
}
同樣參數與POST內容,套用在MVC Action結果截然不同:(補充:傳回結果採用Json.NET版JsonResult,使用Json.NET序列化,避用預設的JavaScriptSerializer)
public class MvcController : Controller
{
public ActionResult PostJson(ArgObject args)
{
return new Newtonsoft.JsonResult.JsonResult()
{
Data = args
};
}
}
JSON中的PropX值沒有正確對應到PropB:
這個差異源自ApiController及Controller處理Model Binding的機制不同。Controller使用Value Provider概念解析Query Sting參數、HTML Form欄位、XML或JSON,再對應成輸入參數,而JSON由JsonValueProviderFactory負責解析,其內部使用的是JavaScriptSerializer。網路上有將JsonValueProviderFactory換成Json.NET版JsonDotNetValueProviderFactory的做法,但實測無法克服問題。因為MVC預設的Model Binding機制,會先用JsonValuProviderFactory將JSON內容轉成包含三組值的Dictionary:
Name: "Jeffrey",
SubArg.PropA: "AAA",
SubArg.PropX: "XXX"
再從Dictionary取值映對到新建的ArgObject及SubArgObject物件,導致[JsonProperty("PropX")]無從發揮作用。
評估之下,還是得回歸自訂Model Binder。執行IModelBinder.BindModel()時,因轉換對象型別已知,可善用Json.NET的.DeserializeObject(jsonString, bindingContext.ModelType),指定目標型別讓[JsonProperty]、[JsonConverter(…)]等設定發揮作用。
仿效System.Web.Http.FromBodyAtrribute,我做了一個Afet.Mvc.FromBodyAtrribute,將整個JSON內容轉成單一型別。
另外,藉此機會也順便解決實務上另一項常見困擾。為了傳送JSON到SomeAction(Type1 arg1, Type2 arg2),通常要另外宣告一個暫用彙總型別:
class AggTypes {
public Type1 arg1 { get; set; }
public Type2 arg2 { get; set; }
}
接收端再改成SomeAction([Afet.Mvc.FromBody]AggTypes args)。既有自訂Model Binder,若允許由JSON中只取出arg1及arg2,分別轉成Type1及Type2,就可以省去多宣告無意義彙總型別的困擾:
ActionResult SomeAction([Afet.Mvc.FromPartialBody]Type1 arg1, [Afet.Mvc.FromPartialBody]Type2 arg2)
FromBodyAttribute及FromPartialBodyAttribute的程式碼如下:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Web.Http.Controllers;
using System.Web.Http.Validation;
using System.Web.Mvc;
namespace Afet.Mvc
{
[AttributeUsage(AttributeTargets.Parameter)]
public class FromBodyAttribute : CustomModelBinderAttribute
{
protected bool DictionaryMode = false;
public FromBodyAttribute(bool dictionaryMode = false)
{
this.DictionaryMode = dictionaryMode;
}
public override IModelBinder GetBinder()
{
return new JsonNetBinder(DictionaryMode);
}
public class JsonNetBinder : IModelBinder
{
private bool DictionaryMode;
public JsonNetBinder(bool dictionaryMode)
{
this.DictionaryMode = dictionaryMode;
}
const string VIEW_DATA_STORE_KEY = "___BodyJsonString";
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
//Verify content-type
if (!controllerContext.HttpContext.Request.
ContentType.ToLower().Contains("json"))
return null;
var stringified = controllerContext.Controller
.ViewData[VIEW_DATA_STORE_KEY] as string;
if (stringified == null)
{
var stream = controllerContext.HttpContext.Request.InputStream;
using (var sr = new StreamReader(stream))
{
stream.Position = 0;
stringified = sr.ReadToEnd();
controllerContext.Controller
.ViewData.Add(VIEW_DATA_STORE_KEY, stringified);
}
}
if (string.IsNullOrEmpty(stringified)) return null;
try
{
if (DictionaryMode)
{
//Find the property form JObject converted from body
var dict = JsonConvert.DeserializeObject<JObject>(stringified);
if (dict.Property(bindingContext.ModelName) == null)
return null;
//Convert the property to ModelType
return dict.Property(bindingContext.ModelName).Value
.ToObject(bindingContext.ModelType);
}
else
{
//Convert the whole body to ModelType
return JsonConvert.DeserializeObject(stringified,
bindingContext.ModelType);
}
}
catch
{
}
return null;
}
}
}
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class FromPartialBodyAttribute : FromBodyAttribute
{
public FromPartialBodyAttribute()
: base(true)
{
}
}
}
使用方法如下:
public ActionResult PostJson2(
[Afet.Mvc.FromBody]ArgObject args)
{
return new Newtonsoft.JsonResult.JsonResult()
{
Data = args
};
}
public ActionResult PostJson3(
[Afet.Mvc.FromPartialBody]string Name,
[Afet.Mvc.FromPartialBody]SubArgObject SubArg
)
{
return new Newtonsoft.JsonResult.JsonResult()
{
Data = new
{
ParamName = Name,
ParamSubArg = SubArg
}
};
}
有了這兩項工具,使用Json.NET解析MVC輸入參數就方便多了。
Comments
# by 牛大神
為什麼要那麼複雜呢, 直接用 text 傳送就好了, 伺服器分析 json 就已經用上了很多資料, 但直接用 text 傳到伺服器的話, 直接 split 就可以了. 型別問題嗎? 不論 json 還是 text, 我們寫程式時都預設及預知那個變量是那個型別吧! 直的不知道為什麼要用 json? 整潔些? 偵錯易一點?
# by Jeffrey
to 牛大神, 使用CSV之類的純文字檔格式,相較於JSON有一些缺點: 1) 不管用什麼分隔符號,都要解決內容值出現該符號字元值的例外處理,資料都是純數字或日期時問題不大,但若包含可自由輸入文字時,蠻容易埋下地雷,很久之後某一天才引爆。而JSON已經充分考量過這些特殊情境。 2) 如果資料在Server端要還原成物件,必須自己寫轉換函式,將拆出來的值一一對應到欄位上。一旦要增減欄位,轉換函式就得修改,使用JSON格式就省去這層困擾。 3) 在各語言平台上都能找到JSON函式庫,方便與不同系統串接。 4) JSON肉眼可以讀取,也有很多現成工具可以解析結構,相較於CSV要數位置算欄位,的確容易偵錯多了。但JSON字串內含屬性名稱挺囉嗦(但還是比XML簡潔),簡潔不是它的最大強項。