WPF中基于MVVM实现未知JSON的可编辑TreeView展示问题
我之前刚好做过类似的需求,这种动态JSON绑定TreeView的场景确实容易踩坑——直接绑JToken肯定不行,因为WPF根本不知道怎么解析它的结构,也不知道该显示什么内容,所以才会出现你看到的显示System.Windows.HierarchicalDataTemplate的错误。
不过用一个ViewModel包装类就能搞定所有问题:包括动态层级显示、字符串编辑、重复键处理,完全适配MVVM模式。下面是具体的实现步骤:
1. 创建JSON节点的ViewModel包装类
首先定义一个JsonTreeNodeViewModel类,它会把原生的JToken(JObject/JArray/JValue)包装成MVVM友好的对象,处理节点显示、子节点加载和编辑逻辑,同时保留对原JToken的引用,方便修改后写回JSON:
using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; using Newtonsoft.Json.Linq; public class JsonTreeNodeViewModel : INotifyPropertyChanged { private readonly JToken _originalToken; private string _editableValue; public JsonTreeNodeViewModel(JToken token, string displayName = null) { _originalToken = token; DisplayName = displayName ?? GetDefaultDisplayName(token); Children = LoadChildren(token); InitializeEditableValue(); } // 节点显示名称(键名、数组索引或值类型标识) public string DisplayName { get; } // 子节点集合,绑定TreeView的ItemsSource public IEnumerable<JsonTreeNodeViewModel> Children { get; } // 可编辑的字符串值(仅当节点是字符串类型JValue时可用) public string EditableValue { get => _editableValue; set { if (_editableValue != value) { _editableValue = value; OnPropertyChanged(); // 修改后直接同步回原JToken if (_originalToken is JValue jValue) { jValue.Value = value; } } } } // 判断当前节点是否是可编辑的字符串 public bool IsEditableString => _originalToken is JValue jValue && jValue.Type == JTokenType.String; private string GetDefaultDisplayName(JToken token) { return token switch { JProperty prop => prop.Name, // JObject的属性键名 JArray array => $"Array ({array.Count} items)", // 数组节点显示数量 JValue value => value.Type.ToString(), // 值类型节点显示类型 JObject obj => "Object", // 空对象或无键名的对象 _ => token.Type.ToString() }; } private IEnumerable<JsonTreeNodeViewModel> LoadChildren(JToken token) { var children = new List<JsonTreeNodeViewModel>(); switch (token) { case JObject obj: // 遍历所有属性,包括重复键(如果解析时允许的话) foreach (JProperty prop in obj.Properties()) { children.Add(new JsonTreeNodeViewModel(prop.Value, prop.Name)); } break; case JArray array: // 数组节点按索引显示子节点 for (int i = 0; i < array.Count; i++) { children.Add(new JsonTreeNodeViewModel(array[i], $"[{i}]")); } break; // 值类型节点没有子节点 default: break; } return children; } private void InitializeEditableValue() { // 只有字符串类型的JValue才初始化可编辑值 if (_originalToken is JValue jValue && jValue.Type == JTokenType.String) { _editableValue = jValue.Value.ToString(); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
2. 更新你的ViewModel
修改SomeViewModel,把Tree属性改成JsonTreeNodeViewModel类型,然后在PopulateTreeView方法里把解析后的JToken转换成包装类实例。注意如果你的JSON存在重复键,解析时必须开启AllowDuplicateNames:
using Newtonsoft.Json; using Newtonsoft.Json.Linq; public class SomeViewModel { public JsonTreeNodeViewModel Tree { get; set; } private void PopulateTreeView() { var jsonReader = new JsonReader(); var jsonText = jsonReader.Read(@"C:\imaginary_path\example.json"); // 开启允许重复键(如果你的JSON有这种场景) var loadSettings = new JsonLoadSettings { AllowDuplicateNames = true }; var rootToken = JToken.Parse(jsonText, loadSettings); // 这里就是你要的"magic":把根JToken包装成ViewModel Tree = new JsonTreeNodeViewModel(rootToken); } // 保存修改后的JSON到文件 public void SaveModifiedJson() { var formattedJson = Tree._originalToken.ToString(Formatting.Indented); // 替换成你的保存逻辑 // jsonWriter.Write(formattedJson, @"C:\imaginary_path\example.json"); } }
3. 修改XAML的TreeView模板
现在需要更新HierarchicalDataTemplate,让TreeView根据节点类型显示不同内容:可编辑字符串显示TextBox,其他节点显示名称并展开子节点。另外因为TreeView的ItemsSource要求是集合,所以需要一个转换器把单个根节点转成集合:
<ScrollViewer> <TreeView ItemsSource="{Binding Tree, Converter={StaticResource SingleItemToListConverter}}"> <TreeView.Resources> <!-- 转换器:把单个根节点转为集合,适配TreeView的ItemsSource要求 --> <local:SingleItemToListConverter x:Key="SingleItemToListConverter"/> <!-- 布尔值转可见性的转换器(WPF自带的也可以用) --> <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/> <HierarchicalDataTemplate DataType="{x:Type local:JsonTreeNodeViewModel}" ItemsSource="{Binding Children}"> <StackPanel Orientation="Horizontal"> <!-- 非可编辑节点显示名称 --> <TextBlock Text="{Binding DisplayName}" Margin="0,0,4,0" Visibility="{Binding IsEditableString, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Inverse}"/> <!-- 可编辑字符串显示TextBox,实时同步修改 --> <TextBox Text="{Binding EditableValue, UpdateSourceTrigger=PropertyChanged}" Width="250" Visibility="{Binding IsEditableString, Converter={StaticResource BooleanToVisibilityConverter}}"/> </StackPanel> </HierarchicalDataTemplate> </TreeView.Resources> </TreeView> </ScrollViewer>
对应的转换器代码:
using System; using System.Collections.Generic; using System.Globalization; using System.Windows.Data; public class SingleItemToListConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return value == null ? new List<object>() : new List<object> { value }; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
关键细节说明
- 重复键处理:解析JSON时开启
AllowDuplicateNames=true,包装类会遍历所有JProperty,不管键是否重复,都会作为独立子节点显示。 - 双向绑定与写回:修改TextBox的内容时,
EditableValue的setter会自动同步回原JToken的JValue,调用SaveModifiedJson时直接序列化根JToken即可保存所有修改。 - 动态适配结构:不管你的JSON层级多深、类型多复杂,包装类都会自动递归加载子节点,完全不需要提前定义任何固定结构。
这样就能完美解决你的问题了——TreeView会正确显示JSON的层级结构,字符串值可以实时编辑,修改后的内容能写回JSON,还能处理重复键的场景。
内容的提问来源于stack exchange,提问作者Mr Squidr

