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日:初始版本


