65.9K
CodeProject 正在变化。 阅读更多。
Home

JsonEdit:快速 JsonData 编辑器控件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2021年1月26日

CPOL

5分钟阅读

viewsIcon

11795

downloadIcon

337

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

  1. “Schema生成Token”
    从选定的Schema构建一个新的根令牌,并用默认值填充。
  2. “编辑Token”
    将选定的Schema附加到选定的Token(如下所示),并将Token推送到JsonEdit中。
  3. “Dts读取Schema”
    JsonEdit附加的Schema从JsonEdit编辑的Token读取到当前的Schema数据记录中。
    (您可以使用此方法将Schema传输到另一个记录。)
  4. “Dts读取Token”
    JsonEdit编辑的Token读取到当前的Token数据记录中。
  5. “加载Dts”
    清除Dataset并从驱动器重新加载它。
  6. “保存Dts”
    Dataset保存到驱动器。
  7. “显示节点JSON”
    弹出一个Messagebox显示与所选Treenode关联的子令牌。
  8. “显示节点Schema”
    显示与所选Treenode关联的子Schema。
  9. “测试”
    我忘了它现在做什么了。
  10. (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日:初始版本
JsonEdit:快速JSON数据编辑器控件-CodeProject - 代码之家
© . All rights reserved.