You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF中基于MVVM实现未知JSON的可编辑TreeView展示问题

我之前刚好做过类似的需求,这种动态JSON绑定TreeView的场景确实容易踩坑——直接绑JToken肯定不行,因为WPF根本不知道怎么解析它的结构,也不知道该显示什么内容,所以才会出现你看到的显示System.Windows.HierarchicalDataTemplate的错误。

不过用一个ViewModel包装类就能搞定所有问题:包括动态层级显示、字符串编辑、重复键处理,完全适配MVVM模式。下面是具体的实现步骤:

解决方案:用ViewModel包装JToken实现动态树形结构

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 00:02:39