最近用到 Echarts 之類的圖表顯示插件, 它基於不同的 Json 結構可以得到不同的圖表, 我們從 http 請求來的數據一般就是 Json 的, 肯定就想通過圖形式的數據映射來完成圖表的顯示了, 不過首先C#它不是JS這種對 Json 結構友好的語言, 要對一個Json節點進行更新的時候, 需要把Json轉換成數據結構, 然后找到相應點, 再用新的Json進行節點讀取, 生成新的結構, 然后再從根節點讀取Json的字符串出來, 才能完成數據映射......
然后就是復雜結構映射, 比如一個Object和Array進行反復嵌套的情況, 要篩選出來某些數據或者結構, 再映射到其它結構中去的話, 會非常困難, 如果考慮用UE的藍圖的話, 有可能可以做到, Unity這個光是自己做可視化界面以及無限展開的映射選項, 就已經要命了......
先說一個數據映射的過程, 因為使用的是LitJson, 它的擴展性比較強, 數據映射的函數正是用到了 JsonData 的 ReadValue 的方式, 不過我們稍微改動了一下 :
/// <summary> /// 核心數據映射邏輯, 跟LitJson中的有差別, 簡單數據通過強制轉換得到, 並保留原有數據類型不變 /// </summary> /// <param name="jsonData"></param> /// <param name="reader"></param> /// <param name="json"></param> /// <returns></returns> private static IJsonWrapper ReadValue(JsonData jsonData, JsonReader reader, string json = null) { try { reader.Read(); // the json maybe error with read while read prime data if(reader.Token == JsonToken.ArrayEnd || reader.Token == JsonToken.Null) return null; IJsonWrapper instance = jsonData; if(reader.Token == JsonToken.String) { instance.SetString((string)reader.Value); return instance; } if(reader.Token == JsonToken.Double) { instance.SetDouble((double)reader.Value); return instance; } if(reader.Token == JsonToken.Int) { instance.SetInt((int)reader.Value); return instance; } if(reader.Token == JsonToken.Long) { instance.SetLong((long)reader.Value); return instance; } if(reader.Token == JsonToken.Boolean) { instance.SetBoolean((bool)reader.Value); return instance; } if(reader.Token == JsonToken.ArrayStart) { instance.SetJsonType(JsonType.Array); while(true) { IJsonWrapper item = ReadValue(new JsonData(), reader); if(item == null && reader.Token == JsonToken.ArrayEnd) break; ((IList)instance).Add(item); } } else if(reader.Token == JsonToken.ObjectStart) { instance.SetJsonType(JsonType.Object); while(true) { reader.Read(); if(reader.Token == JsonToken.ObjectEnd) break; string property = (string)reader.Value; ((IDictionary)instance)[property] = ReadValue(new JsonData(), reader); } } } catch { } if(reader.Token == JsonToken.None) { JsonDataSetPrime(jsonData, json); } return jsonData; } public static string ToRawString(string str) { if(string.IsNullOrEmpty(str)) { return string.Empty; } var rawStr = str; if(rawStr.StartsWith("\"") && rawStr.EndsWith("\"")) { rawStr = rawStr.Substring(1, rawStr.Length - 2); } return rawStr; } public static void JsonDataSetPrime(JsonData jsonData, string json) { var rawType = jsonData.GetJsonType(); DataTable dataTable = json; DataTable rawData = ToRawString((string)dataTable); switch(rawType) { case JsonType.Int: { ((IJsonWrapper)jsonData).SetInt((int)rawData); } break; case JsonType.Long: { ((IJsonWrapper)jsonData).SetLong((long)rawData); } break; case JsonType.Double: { ((IJsonWrapper)jsonData).SetDouble((double)rawData); } break; case JsonType.Boolean: { ((IJsonWrapper)jsonData).SetBoolean((bool)rawData); } break; case JsonType.String: { ((IJsonWrapper)jsonData).SetString((string)rawData); } break; } }
這個 ReadValue 就是置換 JsonData 里的內容的, 使用 try catch 是因為它轉換的邏輯是從上往下的, 節點的類型是由前置讀取的類型給出的, 所以如果是一個Json對象, 它的字符串是合法的就能正確讀取, 比如:
{"aa":100}
可是如果修改對象是普通值類型, 比如要修改的是 100 這個對應的JsonData, 傳入的Json可能是這樣的:
"200"
它就不是一個合法的Json, 是無法正常讀取的, 所以會拋出異常, 並且導致 JsonReader 的類型是 None, 所以 JsonDataSetPrime() 方法就強制給JsonData賦值了. 這樣就封裝了節點數據置換的方法了.
然后做一個數據映射的路徑記錄, 就能把數據映射到模板里面去了, 比如下圖, 左邊是數據, 右邊是一個 Echarts 圖表的模板 :
獲取它的路徑就很簡單, 得到 From:xAxis/type To:title/text 這樣的結果, 那么只需要獲取到左邊的節點, 然后ToJson, 用右邊相應節點的JsonData來讀取一遍即可.
然后就是有些復雜的映射, 比如 Object(Dictionary) 到 Array 的映射, Array 中嵌套的 Object/Array 對應的結構的映射, 可以從下面的截圖看出來 :
想要把 yAxis 這個 Object 映射到 data 這個 Array 里面去, 可以映射 Key 或者 Value 的方式 :
這里是把 yAxis 的 Keys 映射成 Array了. 或者也可以用 Values 映射, 只要結構能夠對的上就行了. 代碼邏輯跟下面相同, 只是把對應的數值取出來罷了:
JsonData fromTag; var list = new List<string>(fromTag.Keys); var json = LitJson.JsonMapper.ToJson(list);
因為 Object 的 Key 一定是 string 類型的, 所以可以簡單的創建 List 對象, 可是如果是 Value 這種復雜對象, 就不能簡單構建了!!! 這些映射可以通過簡單的數據結構就能保存, 比如上面的我的結構是這樣的 :
public enum MappingLogic { Replace, ObjectToArray_Keys, ObjectToArray_Values } public class MappingInfo { public string from; public string to; public MappingLogic mappingLogic = MappingLogic.Replace; public int order = 100; // 數值越小越先執行 } // Json 文件 [{"from":"yAxis","to":"xAxis/data","mappingLogic":1,"order":100}]
[ 從 yAxis 節點獲取 Keys 映射到 xAxis/data 節點中去 ]
嵌套式的映射 :
要將左邊的 data (Array) 隊列中的 Object 結構中的 xAxis 映射到右邊的 data(Array)結構中去, 得到下圖 :
其實可以看出, 復雜結構就復雜在 Array 的操作上, Object 的每個對象都是有 Key 的, 而 Array 需要通過自己獲得 index 來進行映射(普通映射), 如果作為結構映射, 又需要通過 Foreach 進行結構厲遍, 看一下怎樣通過代碼完成映射結構的 :
先看生成的映射文件
[{ "from": "series/[array:0]/markLine/data/[array:Foreach]/xAxis", "to": "series/[array:0]/data", "mappingLogic": 0, "order": 100 }]
from 的結構已經不是簡單的節點路徑了, 到 data/[array:Foreach] 這個節點, 表示的就是對於 data 這個節點的 array 需要進行厲遍, 然后再獲取每個厲遍的 xAxis 節點的值, 映射到右邊的 data 節點去,
代碼結構 :
public const string Array = "array"; public const string Foreach = "Foreach"; public static void DoMap(JsonData from, JsonData to, MappingInfo info) { var fromTag = GetHierarchy(from, info.from, true); var json = "{}"; switch(info.mappingLogic) { case MappingLogic.ObjectToArray_Keys: { var list = new List<string>(fromTag.Keys); json = LitJson.JsonMapper.ToJson(list); } break; case MappingLogic.ObjectToArray_Values: { var list = new List<string>(); fromTag.ForeachDictionary((_key, _data) => { list.Add(_data.ToString()); }); json = LitJson.JsonMapper.ToJson(list); } break; default: { json = fromTag.ToJson(); } break; } WrapJsonData(to, info.to, json, true, false); } // it must be root json node public static void WrapJsonData(JsonData jsonData, string hierarchy, string json, bool clear = true, bool keepRawData = true) { jsonData = GetHierarchy(jsonData, hierarchy, keepRawData); WrapJsonData(jsonData, json, clear); } public static void WrapJsonData(JsonData jsonData, string json, bool clear = true) { JsonReader reader = new JsonReader(json); if(clear) { jsonData.Clear(); jsonData.ClearJsonCache(); } ReadValue(jsonData, reader, json); } public static void WrapJsonData(JsonData jsonData, string json, bool clear = true) { JsonReader reader = new JsonReader(json); if(clear) { jsonData.Clear(); jsonData.ClearJsonCache(); } ReadValue(jsonData, reader, json); } public static JsonData GetHierarchy(JsonData root, string hierarchy, bool keepRawData = true) { var node = root; if(string.IsNullOrEmpty(hierarchy) == false) { var layers = hierarchy.Split('/'); if(layers != null && layers.Length > 0) { for(int i = 0; i < layers.Length; i++) { var target = layers[i]; int index = -1; if(node.IsArray && IsArrayIndex(target, ref index)) { node = node[index]; } else if(node.IsArray && IsArrayForeach(target, ref index)) { var tempNode = new JsonData(); foreach(JsonData data in node) { var lastHierarchy = string.Join("/", layers, i + 1, layers.Length - i - 1); tempNode.Add(GetHierarchy(data, lastHierarchy, true)); } if(keepRawData) { node = tempNode; } else { WrapJsonData(node, tempNode.ToJson(), true); } break; } else { node = node[target]; } } } } return node; } public static bool IsArrayIndex(string pattern, ref int index) { string element = ""; if(IsArrayElement(pattern, ref element)) { if(int.TryParse(element, out index)) { return true; } } return false; } public static bool IsArrayForeach(string pattern, ref int index) { string element = ""; if(IsArrayElement(pattern, ref element)) { if(string.Equals(element, Foreach, System.StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public static bool IsArrayElement(string pattern, ref string element) { if(string.IsNullOrEmpty(pattern) == false) { if(pattern.StartsWith("[") && pattern.EndsWith("]")) { var sp = pattern.Substring(1, pattern.Length - 2).Split(':'); if(sp != null && sp.Length > 1) { if(string.Equals(sp[0], Array, System.StringComparison.OrdinalIgnoreCase)) { element = sp[1]; return true; } } } } return false; }
現在主要的就是這些了, 有了半圖形化的界面方式, 點擊連線就能生成映射數據了, 並且從普通數據的類型保持, 到簡單嵌套類型的數據映射, 它基本上能完成90%的需求了, 並且很貼心的添加了代碼生成:
減少了大量的工作量, 未來可期...
對了, 在 EditorWindow 下怎樣實現畫線, 這里用了一個取巧的方法, 每個元素做成一個Toggle, 當左邊的某個元素被點擊之后, 就設定當前鼠標位置為起始位置(它們都在Scroll里, 需要偏移位置) :
leftPoint = (UnityEngine.Event.current.mousePosition) + new Vector2(0, _jsonStringScroll1.y);
然后通過跟當前鼠標位置做連線(有個 Drawing 的腳本, wiki 可找到) :
if(_selectedLeft != null) { Drawing.DrawArrow(leftPoint - new Vector2(0, _jsonStringScroll1.y), UnityEngine.Event.current.mousePosition, Color.gray, 2, 10); }
---------------------------------------------------------------------
今天發現對於結構映射沒有很好的方法, 比如下面這樣的 :
右邊給出了結構, 左邊給出了數據, 因為它們的命名不同, 所以需要根據右邊的結構來對左邊的數據進行映射, 就不能簡單地把左邊替換到右邊去了.
只能額外添加一個映射功能了, 添加了一個 MappingLogic.StructureMapping 的映射邏輯, 這里 默認為模板中的第一條數據為結構, 從數據中對它進行映射:
public static void DoMap(JsonData from, JsonData to, MappingInfo info) { var fromTag = info.mappingLogic != MappingLogic.Write ? GetHierarchy(from, info.from, true) : null; var json = "{}"; switch(info.mappingLogic) { case MappingLogic.ObjectToArray_Keys: { var list = new List<string>(fromTag.Keys); json = LitJson.JsonMapper.ToJson(list); } break; case MappingLogic.ObjectToArray_Values: { var list = new List<string>(); fromTag.ForeachDictionary((_key, _data) => { list.Add(_data.ToString()); }); json = LitJson.JsonMapper.ToJson(list); } break; case MappingLogic.StructureMapping: { var targetNode = GetHierarchy(to, info.to); var structure = targetNode[0]; var fromVar = GetVarName(info.from); var toVar = GetVarName(info.to); for(int i = 0, imax = fromTag.Count; i < imax; i++) { JsonData sourceData = fromTag[i]; JsonData destData = targetNode.Count > i ? targetNode[i] : targetNode[targetNode.Add(Clone(structure))]; destData[toVar] = sourceData[fromVar]; } return; } break; case MappingLogic.Write: { var targetNode = GetHierarchy(to, info.to); WrapJsonData(targetNode, info.writeData, true); return; } break; default: { json = fromTag.ToJson(); } break; } WrapJsonData(to, info.to, json, true, false); }
而在獲取目標節點的時候直接對結構節點返回了:
public static JsonData GetHierarchy(JsonData root, string hierarchy, bool keepRawData = true) { var node = root; if(string.IsNullOrEmpty(hierarchy) == false) { var layers = hierarchy.Split('/'); if(layers != null && layers.Length > 0) { for(int i = 0; i < layers.Length; i++) { var target = layers[i]; int index = -1; if(node.IsArray && IsArrayIndex(target, ref index)) { node = node[index]; } else if(node.IsArray && IsArrayForeach(target, ref index)) { if(IsStructureMapping(target)) { return node; // 返回了 } var tempNode = new JsonData(); foreach(JsonData data in node) { var lastHierarchy = string.Join("/", layers, i + 1, layers.Length - i - 1); tempNode.Add(GetHierarchy(data, lastHierarchy, true)); } if(keepRawData) { node = tempNode; } else { WrapJsonData(node, tempNode.ToJson(), true); } break; } else { node = node[target]; } } } } return node; }
當然這個必然是在 Array 節點下才需要的映射功能, 要不然直接映射就行了.
映射的代碼比較簡單:
case MappingLogic.StructureMapping: { var targetNode = GetHierarchy(to, info.to); var structure = targetNode[0]; var fromVar = GetVarName(info.from); var toVar = GetVarName(info.to); for(int i = 0, imax = fromTag.Count; i < imax; i++) { JsonData sourceData = fromTag[i]; JsonData destData = targetNode.Count > i ? targetNode[i] : targetNode[targetNode.Add(Clone(structure))]; destData[toVar] = sourceData[fromVar]; } return; } break;
這里只需要把兩個相關根節點找出來, 然后把模板節點進行克隆, 然后根據映射關系設置即可.
這樣的映射沒有改變原有結構, 所以即使進行多層映射也是沒有問題的