最近用到 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;
这里只需要把两个相关根节点找出来, 然后把模板节点进行克隆, 然后根据映射关系设置即可.
这样的映射没有改变原有结构, 所以即使进行多层映射也是没有问题的