浓缩版:请跳转至 总结 部分。

JSON 数据的准备

这里使用的 JSON 数据,必须是对象数组。该对象数组里面的元素对象要使用一样的字段,且包含至少一个唯一值字段。

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// nodes.json
[
{
"id": 1,
"name": "412总站",
"coord": [0, 0]
},
{
"id": 2,
"name": "融通路华翠北路口",
"coord": [-100.0, 0.0]
},
{
"id": 3,
"name": "海八路华翠北路口",
"coord": [-100.0, -20.0]
}
]

显然,这个 JSON 数据可以视作一张数据表,每一个元素对象可以视作数据表的一条记录(行),字段可以视作表的字段(列):

id name coord
1 412总站 [0, 0]
2 融通路华翠北路口 [-100.0, 0.0]
3 海八路华翠北路口 [-100.0, -20.0]

JSON 数据在 C# 的加载

加载分三部走:

  1. 引入 Newtonsoft.Json 命名空间(来自 Newtonsoft.Json 的 NuGet包);
  2. 定义一个 JSON 数据类,让它的实例表示 JSON 数据的对象;
  3. 定义加载该 JSON 数据的方法,并把它放在 DataLoader 类。

引入 Newtonsoft.Json 命名空间

使用该 using 指令即可:

1
using Newtonsoft.Json;

定义 JSON 数据类

新建一个类,它的字段要包含 JSON 数据中所有的字段,并且要选择合适的字段类型.

对前面举例过的 nodes.json 数据,可以定义如下的 Node 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Node
{
[JsonProperty("id")]
public int Id { get; set; }

[JsonProperty("name")]
public string Name { get; set; } = "";

private decimal[] _coord = new decimal[2]; // 私有字段用来保护 Coord 属性

[JsonProperty("coord")]
public decimal[] Coord
{
get => (decimal[])_coord.Clone();
set
{
if (value == null || value.Length != 2)
_coord = new decimal[2];
else
_coord = value;
}
}
}

可以看出,我们要在每个字段前面加上 [JsonProperty("<JSON字段名>")] 特性,指定 C# 字段名在 JSON 数据中的对应名称。

关于字段类型的声明:

  • 如果是数字类型,直接用 类型名称 字段名 { get; set; } 即可。

  • 如果是字符串类型,可以用 string 字段名 { get; set; } = ""; 声明,尽量避免用 string?

  • 如果是数组或集合类型(对应 JSON 的数组类型),可以用 类型名称 字段名 { get; set; } = [] 声明。

关于字段的数组或集合类型的选择:

  • 使用数组 T[] 的情况:固定长度,所有元素同类型。

  • 使用列表 List<T> 的情况:长度不固定,所有元素同类型。

切记不要用元组!!因为 Newtonsoft.Json 暂时不支持反序列化元组类型字段。

这里可以看出要注意的地方:JSON 数组类型一定要是用同类型元素!!因为 C# 数组和列表都只支持同类型元素。

定义 JSON 数据的加载方法

加载的目的就是将 JSON 数据映射到 C# 字典(“ID: 对象”的键值对)。

于是,我们需要新建一个 DataLoader 类,里面每一个静态方法都对应着一个 JSON 转换为 C# 字典的过程。

加载 nodes.json 数据的方法可以写成:

1
2
3
4
5
6
7
8
9
10
11
// DataLoader.cs
internal class DataLoader // 输入 <地图名称> ,输出 <数据字典>
{
public static Dictionary<int, Node> LoadNodes()
{
string json = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", Program.CurrentMap, "nodes.json")); // 从 JSON 位置获取它转化为字符串
List<Node> nodesList = JsonConvert.DeserializeObject<List<Node>>(json) ?? []; // 转为列表
Dictionary<int, Node> nodesDict = nodesList.ToDictionary(node => node.Id, node => node); // 转为字典
return nodesDict;
}
}

此时,这个函数就会返回一个对应这个 JSON 数据的 C# 字典,可以将其赋值给某个全局变量:

1
2
// class Program, Program.cs
Dictionary<int, Node> nodes = DataLoader.LoadNodes();

从方法体可看出,JsonConvert.DeserializeObject<List<JSON数据类> 可以直接把 JSON 文本映射到 C# 对象列表,其中这个 JSON数据类 在之前 定义 JSON 数据类 部分已经定义好了。然后,我们用 ToDictionary 方法把这个列表转换为字典,利于对象的索引。

JSON 数据在 C# 的访问

对前面给出的 nodes.json 访问它 id=3 的记录的 name 值,在 C# 可以表示为

1
2
3
// a function, class Mainwindow, MainWindow.xaml.cs
string targetName = nodes[3].Name; // 访问 id=3 的记录的 name 值
nodes[3].Name = "新名称"; // 修改 id=3 的记录的 name 值

可以看出,字典对应的全局变量[ID].字段名 就可以访问到某个 JSON 对象中某个字段的值。

JSON 数据从 C# 的保存

保存的目的就是重新把 C# 字典(“ID: 对象”的键值对)映射到 JSON 数据。

于是,我们创建一个 DataSaver 类,里面每一个静态方法都对应着一个 C# 字典转换为 JSON 的过程。

保存 nodes.json 数据的方法可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
// DataSaver.cs
class DataSaver // 保存数据到文件
{
public static void SaveNodes()
{
if (Program.CurrentMap != null)
{
var nodesList = Program.Nodes.Values.ToList();
string json = JsonConvert.SerializeObject(nodesList, Formatting.Indented);
File.WriteAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", Program.CurrentMap, "nodes.json"), json);
}
}
}

可以看出,这个方法先把数据字典转为列表,然后通过方法 JsonConvert.SerializeObject 把列表转为 JSON 文本,并保存到文件。

需要注意的是,如果在数据类中定义了不需要导出为 JSON 的属性,需要给这些属性添加 [JsonIgnore] 特性,比如在 Node 类中,GeoCoord 字段不需要导出:

1
2
3
// class Node, Node.cs
[JsonIgnore]
public SKPoint GeoCoord => Utils.CoordJSONToSkia(Coord);

总结

准备数据:[{对象1}, {对象2}, {对象3},...](对象共用字段且含 id 字段)。

加载数据:

  • 定义数据类:属性前加上 [JsonProperty("<JSON字段名>")] 特性,属性如果是集合类型只能选数组或列表。

  • 定义加载方法:

    • 先用 File.ReadAllText(JSON位置) 将其转为字符串
    • 然后用 JsonConvert.DeserializeObject<List<数据类>> 将其转为列表
    • 最后用 ToDictionary(对象 => 对象.id, 对象 => 对象) 将其转为字典。

访问数据:字典对应的全局变量[ID].字段名

保存数据:

  • 定义保存方法:

    • 先用 Program.Nodes.Values.ToList() 将数据字典转为列表
    • 然后用 JsonConvert.SerializeObject 将其转为 JSON 文本
    • 最后用 File.WriteAllText(JSON位置, JSON文本) 保存到文件。
  • 注意不要导出不需要的字段要加上 [JsonIgnore] 特性。