2014年10月29日 星期三

[iThome 第七屆鐵人賽 29] Javascript和Mvc溝通 - 實作篇

上一篇介紹完了在Mvc的框架裡面,用Json方式傳遞資料的問題之後,接下來將會看這些問題將會如何解決。

解決思路

首先,最大的問題是Json內建的Serialization是沒有辦法讓我們解決這些問題。因此,我們將會使用Json .Net作為Serialization的library。

再來,會建立一個Class繼承JsonResult,來對裡面做一些處理。不過,需要注意到,因為Mvc裡面的JsonResult寫的比較死,因此有些部分還是需要重複原始的JsonResult內容,讓整個使用起來和之前的Api一樣。

最後,會在之前建立的BaseController寫一些method,方便呼叫來建立新的JsonResult的class。

CoreJsonResult

先從客制的JsonResult開始,首先這個客制的JsonResult會記錄需要顯示的錯誤訊息,並且保留和之前JsonResult一樣的api,因此:

public class CoreJsonResult : JsonResult
{
    public IList<string> ErrorMessages { get; set; }

    public CoreJsonResult()
    {
        ErrorMessages = new List<string>();
    }

    public void AddError(string errorMessage)
    {
        ErrorMessages.Add(errorMessage);
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
            string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("GET access is not allowed.  Change the JsonRequestBehavior if you need GET access.");
        }

        var response = context.HttpContext.Response;
        
        response.ContentType = string.IsNullOrEmpty(ContentType) ? "application/json" : ContentType;

        if (ContentEncoding != null)
        {
            response.ContentEncoding = ContentEncoding;
        }

        SerializeData(response);
    }
}

再來SerializeData才是實際要序列化物件的時候要做的事情:

protected virtual void SerializeData(HttpResponseBase response)
{
    if (ErrorMessages.Any())
    {
        var originalData = Data;
        Data = new
        {
            Success = false,
            OriginalData = originalData,
            ErrorMessage = string.Join("\n", ErrorMessages),
            ErrorMessages = ErrorMessages.ToArray()
        };

        response.StatusCode = 400;
        response.TrySkipIisCustomErrors = true;
    }

    var settings = new JsonSerializerSettings
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver(),

        Converters = new JsonConverter[]
        {
            new StringEnumConverter(), 
        }
    };

    response.Write(JsonConvert.SerializeObject(Data, settings));
}

可以看到,這一邊的序列化使用的就是Json .Net。那用Json .Net序列化的時候有增加一些設定:

  1. 使用了CamelCase作為序列化出來的參數樣式
  2. 遇到Enum的時候,序列化出來的是string的樣子而不是數字的值

最後,為了方便傳遞序列化物件的形態,可以在增加一個泛型的版本:

public class CoreJsonResult<T> : CoreJsonResult
{
    public new T Data
    {
        get { return (T)base.Data; }
        set { base.Data = value; }
    }
}

在BaseController增加呼叫方法

還記得之前有介紹過使用BaseController。在之後使用這個CoreJsonResult,當然不希望手動new這個物件出來,因此可以再Controller增加方法,讓使用CoreJsonResult變的更加的簡單。

public class BaseController : Controller
{
    protected CoreJsonResult JsonValidationError()
    {
        var result = new CoreJsonResult();

        foreach (var validationError in 
    ModelState.Values.SelectMany(v => v.Errors))
        {
            result.AddError(validationError.ErrorMessage);
        }

        return result;
    }

    protected CoreJsonResult JsonError(string errorMessage)
    {
        var result = new CoreJsonResult();

        result.AddError(errorMessage);

        return result;
    }

    protected CoreJsonResult<T> JsonSuccess<T>(T data)
    {
        return new CoreJsonResult<T> { Data = data };
    }
}

基本上有三個方便呼叫的方法:

  1. JsonValidationError - 這個是當ModelState驗證失敗的時候,呼叫的版本
  2. JsonError - 這個是當發生其他類型的錯誤的時候
  3. JsonSuccess - 這個是當要Json返回ViewModel的時候呼叫

呼叫的範例

再來看一下實際使用的時候的範例:

// POST: /Users/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Edit viewModel)
{
    if (!ModelState.IsValid)
    {
        return JsonValidationError();
    }

    if (viewModel.Id == null)
    {
        return JsonError("Id不能是空");
    }

    // 如果需要以Json格式返回viewModel的時候
    return JsonSuccess(viewModel);
}

這邊給了三個方法使用的時候和情況。可以注意到的是如果是手動回傳錯誤訊息(JsonValidationError或者是JsonError),返回的http status是400,因此在前端的Ajax,也是由 error方法執行。假設,今天是系統出錯了,例如丟出了exception,前端接到的還是error的方法,這樣error就是處理和錯誤有關的,不管是驗證錯誤還是不預期的錯誤。

結語

透過自訂的JsonResult,不只是在速度上面比原生的還快,還可以控制很多設定,例如使用camel case而不是用pascal case。

有了自訂的JsonResult,前端需要和後端溝通也變的更加簡單。


沒有留言 :

張貼留言