Quantcast
Channel: 黑暗執行緒
Viewing all articles
Browse latest Browse all 2311

再談jQuery傳送物件JSON給ASP.NET MVC

$
0
0

使用jQuery傳送物件JSON到ASP.NET MVC的做法之前介紹過,但最近我在專案又遇到新難題。

例如有一個參數物件,ArgObject,內含Name屬性及SubArg屬性,SubArg有其專屬型別SubArgObject,基於特殊需要,SubArgObject使用[JsonProperty]及[JsonIgnore]自訂JSON轉換邏輯(實際專案用的是[JsonConverter(...)],此處簡化為[JsonProperty],指定PropB在JSON中需更名為PropX):

publicclass ArgObject
    {
publicstring Name { get; set; }
public SubArgObject SubArg { get; set; }
    }
 
publicclass SubArgObject
    {
publicstring PropA { get; set; }
        [JsonProperty("PropX")]
publicstring PropB { get; set; }
        [JsonIgnore]
publicstring PropC { get; set; }
    }

如果是Web API Controller,什麼都不用做就能順利接收參數。依Web API的Binding規則,參數如為複雜型別(Complex Type),將依Content-Type選擇適當的Media Type Formatter進行轉換[參考]。在本例中application/json會使用JsonMediaTypeFormatter,其核心為Json.NET,且已知轉換目標型別為ArgObject,故可精準轉換[JsonProperty("PropX")]無誤。

publicclass WebApiController : ApiController
    {
public ArgObject PostJson(ArgObject args)
        {
return args;
        }
    }

同樣參數與POST內容,套用在MVC Action結果截然不同:(補充:傳回結果採用Json.NET版JsonResult,使用Json.NET序列化,避用預設的JavaScriptSerializer)

publicclass MvcController : Controller
    {
public ActionResult PostJson(ArgObject args)
        {
returnnew 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)]
publicclass FromBodyAttribute : CustomModelBinderAttribute
    {
protectedbool DictionaryMode = false;
 
public FromBodyAttribute(bool dictionaryMode = false)
        {
this.DictionaryMode = dictionaryMode;
        }
 
publicoverride IModelBinder GetBinder()
        {
returnnew JsonNetBinder(DictionaryMode);
        }
 
publicclass JsonNetBinder : IModelBinder
        {
 
privatebool DictionaryMode;
public JsonNetBinder(bool dictionaryMode)
            {
this.DictionaryMode = dictionaryMode;
            }
 
conststring VIEW_DATA_STORE_KEY = "___BodyJsonString";
 
publicobject BindModel(ControllerContext controllerContext,
                ModelBindingContext bindingContext)
            {
//Verify content-type
if (!controllerContext.HttpContext.Request.
                    ContentType.ToLower().Contains("json"))
returnnull;
                var stringified = controllerContext.Controller
                                .ViewData[VIEW_DATA_STORE_KEY] asstring;
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)) returnnull;
 
try
                {
if (DictionaryMode)
                    {
//Find the property form JObject converted from body
                        var dict = JsonConvert.DeserializeObject<JObject>(stringified);
if (dict.Property(bindingContext.ModelName) == null)
returnnull;
//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
                {
 
                }
returnnull;
            }
        }
    }
 
    [AttributeUsage(AttributeTargets.Parameter)]
publicsealedclass FromPartialBodyAttribute : FromBodyAttribute
    {
public FromPartialBodyAttribute()
            : base(true)
        {
        }
    }
}

使用方法如下:

public ActionResult PostJson2(
            [Afet.Mvc.FromBody]ArgObject args)
        {
returnnew Newtonsoft.JsonResult.JsonResult()
            {
                Data = args
            };
        }
 
public ActionResult PostJson3(
            [Afet.Mvc.FromPartialBody]string Name,
            [Afet.Mvc.FromPartialBody]SubArgObject SubArg
            )
        {
returnnew Newtonsoft.JsonResult.JsonResult()
            {
                Data = new
                {
                    ParamName = Name,
                    ParamSubArg = SubArg
                }
            };
        }

有了這兩項工具,使用Json.NET解析MVC輸入參數就方便多了。


Viewing all articles
Browse latest Browse all 2311

Trending Articles