Search code examples
c#jsonwpf

How to convert a heavily nested dynamic json to a editable table in WPF form using C#


I am wondering if there is a simple and best way to convert a heavily nested json into a editable tables on a WPF form where the user can edit the table. After editing is done i want to convert the edited table back to json again. I am new to C# . I would really appreciate any suggestion on how to achieve this.

PS: The json is dynamic (new keys can be added or deleted and the structure might change).


Solution

  • This is pretty straightforward:

    • use types from System.Text.Json as the model
    • build a view model to wrap it (There is 3 main type: JsonArray, JsonObject and JsonValue all are derived from JsonNode)
    • to make it works fine with a TreeView and the HierarchicalDataTemplate, the view model must also know the key matching the JsonNode.
      The key can be root, an object property name, an array element index

    This is a first draft for a read-only version.
    The code is available here.

    A screenshot of the application

    The keys:

    public enum JsonKeyType
    {
        Root,
        Index,
        Parameter
    }
    
    public abstract class JsonKey
    {
        public abstract JsonKeyType KeyType { get; }
    }
    
    public class IndexKey : JsonKey
    {
        public IndexKey(int index)
        {
            Index = index;
        }
    
        public int Index { get; }
    
        public override JsonKeyType KeyType => JsonKeyType.Index;
    
        public override string ToString() => $"@{Index}";
    }
    
    public class PropertyKey : JsonKey
    {
        public PropertyKey(string key)
        {
            Key = key;
        }
    
        public string Key { get; }
    
        public override JsonKeyType KeyType => JsonKeyType.Parameter;
    
        public override string ToString() => Key;
    }
    
    public class RootKey : JsonKey
    {
        public static RootKey Instance { get; } = new();
    
        private RootKey()
        {
        }
    
        public override JsonKeyType KeyType => JsonKeyType.Root;
    
        public override string ToString() => "Root";
    }
    

    The view models:

    public abstract class JsonViewModel
    {
        public static JsonViewModel? From(JsonKey key, JsonNode? jsonNode)
        {
            return jsonNode switch
            {
                JsonArray jsonArray => new JsonArrayViewModel(key, jsonArray),
                JsonObject jsonObject => new JsonObjectViewModel(key, jsonObject),
                JsonValue jsonValue => new JsonValueViewModel(key, jsonValue),
                null => new JsonValueViewModel(key, null),
                _ => throw new UnreachableException()
            };
        }
    
        protected JsonViewModel(JsonKey key)
        {
            Key = key;
        }
    
        public JsonKey Key { get; }
    }
    
    public class JsonArrayViewModel : JsonViewModel
    {
        private readonly JsonArray _jsonArray;
    
        public JsonArrayViewModel(JsonKey key, JsonArray jsonArray) : base(key)
        {
            _jsonArray = jsonArray;
        }
    
        public IEnumerable<JsonViewModel?> Values => _jsonArray.Select((node, index) => From(new IndexKey(index), node));
    }
    
    public class JsonObjectViewModel : JsonViewModel
    {
        private readonly JsonObject _jsonObject;
    
        public JsonObjectViewModel(JsonKey key, JsonObject jsonObject) : base(key)
        {
            _jsonObject = jsonObject;
        }
    
        public IEnumerable<JsonViewModel?> Values => _jsonObject.Select(kvp => From(new PropertyKey(kvp.Key), kvp.Value));
    }
    
    public class JsonValueViewModel : JsonViewModel
    {
        public JsonValueViewModel(JsonKey key, JsonValue? jsonValue) : base(key)
        {
            if (jsonValue is null)
            {
                Value = "null";
                ValueKind = JsonValueKind.Null;
            }
            else
            {
                var jsonElement = jsonValue.GetValue<JsonElement>();
    
                Value = jsonElement.GetRawText();
                ValueKind = jsonElement.ValueKind;
            }
        }
    
        public string Value { get; }
    
        public JsonValueKind ValueKind { get; }
    }
    

    The view:

    <TreeView ItemsSource="{Binding}">
        <TreeView.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:JsonArrayViewModel}" ItemsSource="{Binding Values}">
                <TextBlock Text="{Binding Key}" Foreground="Gray" />
            </HierarchicalDataTemplate>
            <HierarchicalDataTemplate DataType="{x:Type local:JsonObjectViewModel}" ItemsSource="{Binding Values}">
                <TextBlock Text="{Binding Key}" Foreground="Gray" />
            </HierarchicalDataTemplate>
            <DataTemplate DataType="{x:Type local:JsonValueViewModel}">
                <StackPanel Orientation="Horizontal"
                            ToolTip="{Binding ValueKind}">
                    <TextBlock Text="{Binding Key, StringFormat='{}{0}: '}" Foreground="Gray">
                        <TextBlock.Style>
                            <Style TargetType="TextBlock">
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding Key.KeyType}" Value="{x:Static local:JsonKeyType.Index}">
                                        <Setter Property="FontStyle" Value="Italic" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </TextBlock.Style>
                    </TextBlock>
                    <TextBlock Text="{Binding Value}" />
                </StackPanel>
            </DataTemplate>
        </TreeView.Resources>
    </TreeView>