JsonEdit:快速 JsonData 编辑器控件





5.00/5 (4投票s)
JSchema-支持,易于使用的UI,智能行布局
JsonEdit UI-概念
JsonEdit
旨在显示和编辑Newtonsoft的JToken
对象。
前提条件是将JSchema
对象附加到该令牌,该对象定义了令牌的结构。
参见代码实现此目的
// (JToken tk, JSchema schema)
tk.AttachSchema(schema);
jEditor1.Token = tk;
然后控件可能显示以下内容
根节点显示[2]
,这是底层Json代码中Array
元素的表示,其中包含两个(复杂的)元素。
与之相反,下一个树节点-{4}
-表示一个Object
元素,其中包含四个属性。
注意:已选择根节点,但焦点在输入文本框上。
嗯-在JSON中,可编辑的数据仅显示为“原始”
可以是属性值(例如,节点#3-5)或数组元素(节点#7-9)。
但是,对于数组节点,文本框被锁定,无法进行文本输入。
但是,您可以(通过锁定的但有焦点的文本框)通过'Arrow-Up/Down'
在树视图选择中导航到可编辑节点。
或者您按'+'
后者会在JArray
中插入一个新的(复杂的)元素,并根据底层JSchema
中定义的默认值填充它。
如前所述,您可以导航(通过鼠标或向下箭头)到您不喜欢的条目,并对其进行编辑
插入数组元素适用于Schema中定义的任何数组,并且生成元素的输出取决于特定SubSchema
中给出的"items"
定义。
在此,我向名为“powers
”的数组属性添加了一个元素。由于其元素定义为原始类型,因此我可以输入文本
当然,您也可以删除JArray
元素:选择* * *元素* * *(而不是数组本身!)然后按'Ctrl-Del'
. . .
最后要提到的是:JsonEdit
不仅可以编辑给定的JToken
,还可以根据给定的Schema从头开始创建一个。
幕后魔法
我创建了一些巧妙的扩展方法,它们为JToken
提供了一种“附加属性容器”。
在该“附件”内,我将令牌与其Schema关联起来。
当显示(根)令牌时,我并行地递归遍历Schema和令牌,并为每个子令牌附加其子Schema。
(此外,我还将一个TreeNode
附加到它们上,以便将所有内容整合在一起。)
这就是为什么当用户选择一个数组Treenode
并按'+'
时,我可以添加一个合适的数组元素(甚至是复杂的元素)。
在这篇文章中,我省略了这部分代码-如果您愿意,请查看源代码。
演示应用程序
我将一些示例数据放入了类型化的Dataset
中,并绑定了一些Datagridview
。因此,您可以选择准备好的JSchema
,并且对于每个Schema,您可以从几个准备好的Token
中进行选择。
下面,当前的Schema和Token以原始JSON代码“按原样”显示。
在右侧,JsonEdit
-UserControl
以尽可能直观的方式执行数据输入,并提供用户指导。
让我们一步步看看所有您可以点击的Button
- “Schema生成Token”
从选定的Schema构建一个新的根令牌,并用默认值填充。 - “编辑Token”
将选定的Schema附加到选定的Token(如下所示),并将Token推送到JsonEdit
中。 - “Dts读取Schema”
将JsonEdit
附加的Schema从JsonEdit
编辑的Token读取到当前的Schema数据记录中。
(您可以使用此方法将Schema传输到另一个记录。) - “Dts读取Token”
将JsonEdit
编辑的Token读取到当前的Token数据记录中。 - “加载Dts”
清除Dataset
并从驱动器重新加载它。 - “保存Dts”
将Dataset
保存到驱动器。 - “显示节点JSON”
弹出一个Messagebox
显示与所选Treenode
关联的子令牌。 - “显示节点Schema”
显示与所选Treenode
关联的子Schema。 - “测试”
我忘了它现在做什么了。 - (Schema)“提交编辑”
将Schema从Schema文本框写入当前的Schema数据记录。
注意:当您尝试将无效JSON存储到Dataset
中时,会显示错误消息。
此外,当文本框输入根据当前子Schema验证失败时,ErrorProvider
会闪烁。
(尽管如此,我还是将一些无效的JSchema
样本混入到数据中,以测试我的换行算法的错误消息。)
换行算法
对我来说,常见的JSON列表,例如由Newtonsoft.Json.JToken.ToString()
提供的列表,都非常长。因此,我搜索(并找到了)一种方法,通过我自己的换行算法来减少行数,我可以在其中定义一个maxLineLength
。
参见对比示例。
长版本
{
"$schema": "https://json-schema.fullstack.org.cn/draft-07/schema#",
"definitions": {
"address": {
"type": "object",
"properties": {
"street_address": {
"type": "string"
},
"city": {
"type": "string"
},
"state": {
"type": "string"
}
},
"required": [
"street_address",
"city",
"state"
]
}
},
"type": "object",
"properties": {
"SomeNumbers": {
"type": "array",
"items": {
"type": "number",
"default": 1
}
},
"Addresses": {
"type": "array",
"items": {
"$ref": "#/definitions/address"
}
},
"ZeitRabat": {
"type": "integer"
},
"complex_element": {
"type": "object",
"properties": {
"street_address": {
"type": "string",
"default": "Broadway"
},
"city": {
"type": "string"
},
"state": {
"type": "string"
},
"numb": {
"type": "integer",
"default": 1
}
},
"required": [
"street_address",
"city",
"state"
]
}
}
}
应用maxLineLength = 70
换行后的相同内容
{
"$schema": "https://json-schema.fullstack.org.cn/draft-07/schema#",
"definitions": {
"address": {
"type": "object",
"properties": {
"street_address": {"type": "string"},
"city": {"type": "string"},
"state": {"type": "string"}
},
"required": ["street_address", "city", "state"]
}
},
"type": "object",
"properties": {
"SomeNumbers": {
"type": "array",
"items": {"type": "number", "default": 1}
},
"Addresses": {
"type": "array",
"items": {"$ref": "#/definitions/address"}
},
"ZeitRabat": {"type": "integer"},
"complex_element": {
"type": "object",
"properties": {
"street_address": {
"type": "string",
"default": "Broadway"
},
"city": {"type": "string"},
"state": {"type": "string"},
"numb": {"type": "integer", "default": 1}
},
"required": ["street_address", "city", "state"]
}
}
}
对我来说,JSON代码看起来是
"properties": {
"street_address": {
"type": "string"
},
"city": {
"type": "string"
},
"state": {
"type": "string"
}
},
还是
"properties": {
"street_address": {"type": "string"},
"city": {"type": "string"},
"state": {"type": "string"}
},
换行代码
该算法对JSON一无所知。它只关注括号的结构-即出现在行尾的括号。
因为JToken.ToString()
会在每个开括号或闭括号后进行换行。
我展示代码是因为一些花哨的功能可能对某些读者来说很有趣
- 使用
IEnumerator<String>
对象作为一种“行读取器” - 使用匿名方法将繁琐的
string
操作从主算法中外包出来(您可以将其视为“单层抽象”)。 - 使用(较大的)匿名方法,该方法会递归调用自身。
通过匿名方法执行递归非常方便:您可以访问边界条件,例如maxLineLength
,上述“行读取器”以及匿名帮助方法。
这有助于将算法保留在一个方法内(封装,高局部一致性)。
public static string[] LineLayout(
this IEnumerable<string> structuredLines, int maxLineLength) {
/* recursively identify text-structures like:
* headLine {
* innerLine,
* innerLine,
* innerLine
* },
* (last comma optional)
*
* try convert to:
* headLine {innerLine, innerLine, innerLine},
*
* at least ensure propper innerLine-indentation
*/
var brackets = "{[}]".ToCharArray();
Func<string, char> lineEndBracket = s => { // return the bracket
// occurring in the last 2 Chars, otherwise \0
var i = s.LastIndexOfAny(brackets, s.Length - 1, Math.Min(s.Length, 2));
return i < 0 ? char.MinValue : s[i];
};
Func<char, char> getClosingBracket = openBr =>
brackets[Array.IndexOf(brackets, openBr) + 2];
structuredLines = structuredLines.Where(s => !string.IsNullOrWhiteSpace(s));
using (var lineEnumerator = structuredLines.GetEnumerator()) {
Func<string, int, string[]> layoutCore = null; // recursive anonymous function
layoutCore = (headLine, level) => {
var counter = headLine.Length + level * 2 - 3;
var innerLines = new List<string>();
Action<string> addInnerLine =
ln => { innerLines.Add(ln); counter += ln.Length + 1; };
while (lineEnumerator.MoveNext()) {
var line = lineEnumerator.Current.Trim();
var c = lineEndBracket(line);
if (c == char.MinValue) { addInnerLine(line); continue; } // no bracket:
// add innerLine and continue
if ("}]".Contains(c)) { // close-bracket: return head-,inner-, closing-lines.
// In 1 line, if possible
if (c != getClosingBracket(headLine.Last()))
throw new InvalidOperationException("[ } - Mismatch");
// this(!!) ist the point of return!
return MakeArray(headLine, innerLines, line,
collapse: counter + line.Length < maxLineLength - 1);
}
// opening-bracket: call layoutCore() recursively
var lns = layoutCore(line, level + 1);
if (lns.Length == 1) addInnerLine(lns[0]); // if one-liner returned
// add it to innerLines
else {
// recursion couldn't collapse lines. So this level cannot either
innerLines.AddRange(lns);
counter = maxLineLength; // ensure not to collapse lines
}
} // while
// Exception, because lineEnumerator should exhaust
// exactly with the last closing-Bracket.
throw new InvalidOperationException($"Start-Bracket misses counter-part.");
}; // end recursive anonyme function layoutCore()
if (!lineEnumerator.MoveNext()) return new string[] { }; //successful return,
// when empty document
var rslt = layoutCore(lineEnumerator.Current.Trim(), 0); // recursion-entry-point!!
if (!lineEnumerator.MoveNext()) return rslt; // successful return,
// when lineEnumerator is exhausted
if ("}]".Contains(structuredLines.Last().Last()))
throw new InvalidOperationException("End-Bracket misses counter-part.");
throw new InvalidOperationException("lines detected after document-end.");
} // using
} // LineLayout()
static private string[] MakeArray(
string headLine, List<string> innerLines, string closingLine, bool collapse) {
if (collapse) {
var oneLine = string.Concat(headLine, string.Join(" ", innerLines), closingLine);
return new string[] { oneLine };
}
var indentedInnerlines = innerLines.Select(s => " " + s);
return indentedInnerlines.Prepend(headLine).Append(closingLine).ToArray();
} // MakeArray
据我所见,代码“过度注释”了,所以对我来说没有什么需要解释的了。
换行窗体
为了试用换行算法,演示应用程序附带了第二个Form
。
您可以在主窗体上选择相同的Schema数据记录,但它们以原始格式和换行格式呈现。
(并且一些原始布局确实很糟糕)。
结论
JsonEdit
控件旨在用作小型自制开发人员工具的组件,或供可能处理非常原始数据的技术管理员使用。
(或者用于玩耍。但对于玩耍,互联网上有一些强大的Web工具可用)。
它可能不是万无一失的,当然也不是JSON功能齐全的。我不知道当前JSchema标准支持的所有花哨功能。
我担心我的生命太短,无法弄清楚所有这些东西,并使我的可怜的JsonEdittito适应它。
例如:JsonEdit
只能处理属性和数组"items"
,当它们只有一个"type"
(以及可选的附加null
)时。
而JSchema
规范允许对任何项目具有多个、任意的"type"
-如果我理解正确的话。
历史
- 2021年1月26日:初始版本