Abstract: This is a code generator to declare reflected .NET class of Javascript object using JSON.NET JObject features.

這是跟同事在討論系統架構時冒出的議題...

網頁前端將使用者輸入結果組裝成結構單純的Javascript物件,一個欄位對應一個屬性,但有些欄位如電話、地址等可能有多筆,故屬性型別除了字串、數字外,也有會有電話號碼物件陣列,電話號碼物件則包含國碼、區碼、號碼三個屬性。組裝完成的Javascript透過JSON.stringify會以字串形式傳至後端,針對這種前端動態組成的JSON,我們可在後端沒有事先宣告對應.NET Class的狀況下,將其反序列化成JSON.NET的JObject物件,再透過JObject.Properties探索其中的細節,這就是昨天文章試圖展示的重點。

不過,如果我還是想在寫.NET Code時享受美妙Intellisense,讓我輸入屬性名稱可以少打幾個字,並明確知道屬性類別;而當我豬頭打錯屬性名稱時,也很渴望Visual Studio直接在錯字下方餵我吃一條紅蚯蚓...

嗯,要怎麼收獲先怎麼裁,我們唯一的選擇就是乖乖在Server-Side把對應Javascript物件的.NET類別宣告出來,才能享有這一切。那就看著Javascript物件規格,認命地靠手工把一個個屬性打上去吧... 等等! 若只能乖乖打字修行,又何來此文?

是的,我又想偷懶了!!

想要享受Intellisense又不甘願打字,所以我試寫了一個自動依Javascript物件規格建立對應.NET Class的程式碼產生器。以下是個簡單示範,網頁上有兩個TextArea,上方放JSON字串,下方則是分析JSON字串後產生的.NET類別宣告程式碼。

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>JSON 2 Class</title>
    <script type="text/javascript" src="jquery-1.4.2.js"></script>
    <script type="text/javascript">
        $(function () {
            if ($("#txtJson").val() == "") {
                var obj = {
                    Name: "Jeffrey",
                    RegTime: new Date(),
                    Level: 99,
                    Records: [1024, 9999, 32767],
                    Rank: { Code: "DKNT", Name: "DarkNight" },
                    Skills: [
                    { Name: "Sword", Level: 1 },
                    { Name: "Steal", Level: 2 },
                    { Name: "MouthCannon", Level: 99 }
                    ]
                };
                var json = JSON.stringify(obj);
                $("#txtJson").val(json);
            }
        });
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <textarea id="txtJson" runat="server" rows="6" cols="60"></textarea>
    <br />
    <asp:Button ID="btnGo" runat="server" Text="Convert" onclick="btnGo_Click" />
    <br />
    <textarea id="txtClass" runat="server" rows="18" cols="60"></textarea>
    </form>
</body>
</html>

執行結果如下。在這個範例中,Javascript的Rank屬性是具有Code, Name屬性的物件,而Skills則是個物件陣列,陣列元素物件具有Name, Level兩個屬性。因此,在產生的.NET Class宣告中,除了主類別CRoot用來承接JSON轉換結果外,還要多宣告CRank跟CSkills兩個子類別。

好,看看程式怎麼寫:

using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;
using System.Text;
 
public partial class JSON2Class_Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
 
    }
    protected void btnGo_Click(object sender, EventArgs e)
    {
        JObject jo =
            JsonConvert.DeserializeObject<JObject>(txtJson.Value);
        JsonClassParser.RegisterClass(jo, "Root");
        txtClass.Value = JsonClassParser.GenClassCode();
    }
}
 
public class JsonClassParser
{
    public class RawClass
    {
        public string ClassName;
        public string PropertyHash;
        public Dictionary<string, string> Properties;
    }
 
    //蒐集所有類別
    public static List<RawClass> Library
        = new List<RawClass>();
    private static string getCLSType(JTokenType t)
    {
        switch (t)
        {
            case JTokenType.Boolean:
                return "bool";
            case JTokenType.Bytes:
                return "byte[]";
            case JTokenType.Date:
                return "DateTime";
            case JTokenType.Float:
                return "decimal";
            case JTokenType.Integer:
                return "int";
            case JTokenType.String:
                return "string";
            default:
                throw new ApplicationException(
                    t.ToString() + " is not supported");
        }
    }
 
    //註冊類別
    public static string RegisterClass(JObject jo, string className) 
    {
        RawClass c = new RawClass();
        c.ClassName = "C" + 
            className ??
            Path.GetFileNameWithoutExtension(
            Path.GetTempFileName());
        //將所有類別組成Hash字串,用以比對類別是否相同
        c.PropertyHash =
            string.Join(",",
            jo.Properties().Select(o => o.Name).ToArray());
        c.Properties = new Dictionary<string, string>();
        foreach (JProperty p in jo.Properties())
        {
            string t = "";
            switch (p.Value.Type)
            {
                case JTokenType.Object:
                    t = RegisterClass((JObject)jo[p.Name], p.Name);
                    break;
                case JTokenType.Array:
                    JArray ary = (JArray)jo[p.Name];
                    string typeName = null;                    
                    foreach (JToken jv in ary)
                    {
                        if (jv.Type == JTokenType.Array)
                            throw new ApplicationException(
                                "Array of array is not supported!");
                        string s =
                            jv.Type == JTokenType.Object ?
                            RegisterClass((JObject)jv, p.Name) :
                            getCLSType(jv.Type);
                        if (typeName == null)
                            typeName = s;
                        else if (typeName != s)
                            throw new ApplicationException(
                                "Complex array is not supported!");
                    }
                    t = typeName + "[]";
                    break;
 
                case JTokenType.Boolean:
                case JTokenType.Date:
                case JTokenType.Float:
                case JTokenType.Integer:
                case JTokenType.String:
                    t = getCLSType(p.Value.Type);
                    break;
                default:
                    throw new ApplicationException(
                    p.Type.ToString() + " is not supported!");
            }
            c.Properties.Add(p.Name, t);
        }
        //檢查是否已有重覆類別存在
        var q = (from o in Library
                where o.PropertyHash == c.PropertyHash
                select o).SingleOrDefault();
        //不存在時新增
        if (q == null)
        {
            Library.Add(c);
            return c.ClassName;
        }
        else //存在時傳回既有類別
            return q.ClassName;
    }
 
    public static string GenClassCode()
    {
        StringBuilder sb = new StringBuilder();
        foreach (RawClass rc in Library)
        {
            sb.AppendFormat("public class {0} {{\r\n", rc.ClassName);
            foreach (string p in rc.Properties.Keys)
            {
                sb.AppendFormat("  public {0} {1} {{ get; set; }}\r\n",
                    rc.Properties[p], p);
            }
            sb.AppendLine("}");
        }
        return sb.ToString();
    }
}

雖然演算邏輯有點小複雜,但還是能在150列內將程式寫完,C#真是個好語言呀!!

程式先藉用JSON.NET的JObject動態物件模型逐一找出屬性,分析其型別,當型別為物件時,則用遞迴技巧再針對物件分析生出對應的子類別。當型別為陣列時,則分析陣列元素型別,決定宣告成何種陣列? 不過為避免過度複雜,我設了限制--陣列所有元素必須為同一型別! 當然,陣列元素型別也可以是物件,一樣用遞迴方式產生對應.NET子類別,但一樣受陣列元素單一型別的限制。另外,同型別物件可能被分析多次,我透過比對所有屬性名稱判斷類別是否為同一個。

程式未經大量測試,但初步試玩OK。有興趣的人可以拿回去玩玩,發現有Bug請再告知。


Comments

Be the first to post a comment

Post a comment