3

I am trying to present an unknown JSON with TreeView. I will also need to edit the string inside these JSON objects and write them back so I need some form of a dictionary but the keys can repeat from one side of hierarchy to the other ("response" in the JSON example). I've researched and researched but I am unable to find something that works. Most of the questions are also asked for winforms or without use of MVVM which I'm unable to translate to what I am after.

JSON example:

{
  "universal": {
    "regionalSettings": {
      "culture": {
        "en-GB": "United Kingdom (en-GB)",
        "mk-MK": "Македонија (mk-MK)",
        "sq-MK": "Shqipëria (sq-MK)",
      },
      "language": {
        "en-GB": "English (United Kingdom)",
        "mk": "Македонски",
        "sq": "Shqipe",
      },
      "timeZone": {
        "Europe/Amsterdam": "Europe/Amsterdam",
        "Europe/Andorra": "Europe/Andorra",
        "Europe/Astrakhan": "Europe/Astrakhan",
        "Europe/Athens": "Europe/Athens",
        "Europe/Belgrade": "Europe/Belgrade",
        "Europe/Berlin": "Europe/Berlin",
        "Europe/Bratislava": "Europe/Bratislava",
        "Europe/Brussels": "Europe/Brussels",
      }
    }
  }
}

View snippet:

                <ScrollViewer>
                    <TreeView>
                        <HierarchicalDataTemplate ItemsSource="{Binding Tree}" />
                    </TreeView>
                </ScrollViewer>

ViewModel snippet:

public class SomeViewModel
{

    public JToken Tree { get; set; }


    private void PopulateTreeView()
    {
        var jsonReader = new JsonReader();
        var jsonText = jsonReader.Read("C:\\imaginary_path\\example.json");
        var token = JToken.Parse(jsonText);
        
        // where magic should happen

        Tree = token;
    }
}

"where magic should happen" marks where I'm missing code. There I need to somehow hierarchically represent the json and make in into a format that I can pass into TreeView and at the same time also include strings (in the object, not in the TreeView itself as I will need to display those strings when particular item in a tree is selected), currently I'm passing JObject which makes the TreeView display root that says "System.Windows.HierarchicalDataTemplate".

I am sincerely thanking everyone that can help in advance.

9
  • 1) .NET Core or Framework? 2) Do you want to just view json with TreeView but not parse it into a data model, right? E.g. visual json viewer/editor? 3) why existing viewers/editors not suitable for you? Commented Aug 30, 2020 at 15:32
  • @aepot 1) .NET Core and I'm using caliburn micro as a framework. 2) that is correct. 3) I'm not sure what you mean by existing viwers/editors. I'm making a localization tool which once you select a particular item from treeview would display it's text in multiple languages. Do you have anything particular in mind? Commented Aug 31, 2020 at 6:30
  • Why do you need a TreeView here? Do you need some groups for localized phrases? I guess that localizations can have some unique IDs. UI can use that ID as key to get the localized phrase e.g. ID of string type and match placeholder name. Thus the list of localizations will be linear and TreeView will look as a kind of redundancy. How do you think? Grouping by dialog is not suggested here because multiple dialogs can utilize the same localization phrase. Are there any other reasons for groups of phrases? Commented Aug 31, 2020 at 8:23
  • I would like to have TreeView because of non-unique IDs of keys. Another reason is because I could then organise phrases that are grouped together in the UI together (for example one group for "menu" that would then split into "help menu", "about us"...) Commented Aug 31, 2020 at 8:32
  • 1
    I updated the "JSON example" above, it's now an representation of (one of the) many different JSONs. In other words a different JSON that I have would hierarchically look completely different. Commented Aug 31, 2020 at 9:08

2 Answers 2

5

As we concluded in comments, some Json Editor may be suitable to achive the target.

I've implemented an example, how to read/write json with .NET Core System.Text.Json (documentation) and navigate through it with a TreeView.

I'm not using Calibrum.Micro and not familiar with it, but did it without any external libraries. I've added two helper classes to the solution.

public class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Predicate<object> _canExecute;

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
        => (_execute, _canExecute) = (execute, canExecute);

    public bool CanExecute(object parameter)
        => _canExecute == null || _canExecute(parameter);

    public void Execute(object parameter)
        => _execute(parameter);
}

Tha data Model implementation

public class TreeNode : NotifyPropertyChanged
{
    public static MainViewModel MainVM { get; set; }

    private bool _isSelected;
    private string _name;
    public bool IsSelected 
    { 
        get => _isSelected; 
        set
        {
            _isSelected = value;
            if (_isSelected) MainVM.SelectedItem = this;
        } 
    }
    public string Name 
    { 
        get => _name; 
        set
        {
            _name = value;
            OnPropertyChanged();
        } 
    }
}

public class TreeObject : TreeNode
{
    private ObservableCollection<TreeNode> _children;

    public ObservableCollection<TreeNode> Children
    { 
        get => _children; 
        set
        {
            _children = value;
            OnPropertyChanged();
        } 
    }
}

public class TreeValue : TreeNode
{
    private string _value;
    public string Value
    { 
        get => _value; 
        set
        {
            _value = value;
            OnPropertyChanged();
        } 
    }
}

View Model for MainWindow

public class MainViewModel : NotifyPropertyChanged
{
    private ObservableCollection<TreeNode> _treeItems;
    private TreeNode _selectedItem;
    private ICommand _loadCommand;
    private ICommand _saveCommand;

    public ObservableCollection<TreeNode> TreeItems
    {
        get => _treeItems;
        set
        {
            _treeItems = value;
            OnPropertyChanged();
        }
    }

    public TreeNode SelectedItem
    {
        get => _selectedItem;
        set
        {
            _selectedItem = value;
            OnPropertyChanged();
        }
    }

    public ICommand LoadCommand => _loadCommand ??= new RelayCommand(parameter =>
    {
        TreeItems = new ObservableCollection<TreeNode>();
        JsonReaderOptions options = new JsonReaderOptions
        {
            AllowTrailingCommas = true,
            CommentHandling = JsonCommentHandling.Skip
        };

        Utf8JsonReader reader = new Utf8JsonReader(File.ReadAllBytes("example.json"), options);
        reader.Read();
        ReadJson(ref reader, TreeItems);
    });

    public ICommand SaveCommand => _saveCommand ??= new RelayCommand(parameter =>
    {
        JsonWriterOptions options = new JsonWriterOptions
        {
            Indented = true,
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
        };
        using Stream stream = File.Create("out.json");
        using Utf8JsonWriter writer = new Utf8JsonWriter(stream, options);
        writer.WriteStartObject();
        WriteJson(writer, TreeItems);
    });

    private void WriteJson(Utf8JsonWriter writer, ObservableCollection<TreeNode> items)
    {
        foreach (TreeNode node in items)
        {
            switch (node)
            {
                case TreeValue valueNode:
                    writer.WriteString(valueNode.Name, valueNode.Value);
                    break;
                case TreeObject objectNode:
                    writer.WriteStartObject(objectNode.Name);
                    WriteJson(writer, objectNode.Children);
                    break;
            }
        }
        writer.WriteEndObject();
    }
        
    private void ReadJson(ref Utf8JsonReader reader, ObservableCollection<TreeNode> items)
    {
        bool complete = false;
        string propertyName = "";
        while (!complete && reader.Read())
        {
            switch (reader.TokenType)
            {
                case JsonTokenType.PropertyName:
                    propertyName = reader.GetString();
                    break;
                case JsonTokenType.String:
                    items.Add(new TreeValue { Name = propertyName, Value = reader.GetString() });
                    break;
                case JsonTokenType.StartObject:
                    ObservableCollection<TreeNode> children = new ObservableCollection<TreeNode>();
                    items.Add(new TreeObject { Name = propertyName, Children = children });
                    ReadJson(ref reader, children);
                    break;
                case JsonTokenType.EndObject:
                    complete = true;
                    break;
            }
        }
    }

    public MainViewModel()
    {
        TreeItems = new ObservableCollection<TreeNode>();
        TreeNode.MainVM = this;
    }
}

View (whole markup)

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TreeView Margin="5" ItemsSource="{Binding TreeItems}">
            <TreeView.Resources>
                <HierarchicalDataTemplate DataType="{x:Type local:TreeObject}" ItemsSource="{Binding Children}">
                    <TextBlock Text="{Binding Name}"/>
                </HierarchicalDataTemplate>
                <DataTemplate DataType="{x:Type local:TreeValue}">
                    <TextBlock Text="{Binding Name}"/>
                </DataTemplate>
                <Style TargetType="TreeViewItem">
                    <Setter Property="IsSelected" Value="{Binding IsSelected}" />
                    <Setter Property="IsExpanded" Value="True"/>
                </Style>
            </TreeView.Resources>
        </TreeView>
        <StackPanel Margin="5" Grid.Column="1">
            <StackPanel Orientation="Horizontal">
                <Button Content="Load" Padding="5,0" Command="{Binding LoadCommand}"/>
                <Button Content="Save" Padding="5,0" Margin="5,0,0,0" Command="{Binding SaveCommand}"/>
            </StackPanel>
            <TextBlock Text="Name"/>
            <TextBox Text="{Binding SelectedItem.Name, UpdateSourceTrigger=PropertyChanged}"/>
            <ContentControl Content="{Binding SelectedItem}" Margin="0,5">
                <ContentControl.Resources>
                    <DataTemplate DataType="{x:Type local:TreeObject}">
                        <WrapPanel>
                            <TextBlock Text="{Binding Children.Count, StringFormat=Children count: {0}}"/>
                        </WrapPanel>
                    </DataTemplate>
                    <DataTemplate DataType="{x:Type local:TreeValue}">
                        <StackPanel Orientation="Vertical">
                            <TextBlock Text="Value"/>
                            <TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"/>
                        </StackPanel>
                    </DataTemplate>
                </ContentControl.Resources>
            </ContentControl>
        </StackPanel>
    </Grid>
</Window>

enter image description here

Sign up to request clarification or add additional context in comments.

4 Comments

Haven't implemented it fully yet but the part that I did works! Thank you very much
@aepot this is 90% of what I needed today! :) But my .json files have array-values in some cases. I'll try to figure out how to extend your code, but any hints are appreciated!
My input: { "Target": "Test-1", "Templates": [ "mapping.json", "Specs.json" ], "Queries": [ { "Template": "map1", "ConnectionString": "DSN=Test;Driver={Microsoft Text Driver (*.txt, *.csv)};", "SQLQuery": "SELECT mapID, client, resourceLevel FROM testmap.txt" }, { "Template": "route1", "ConnectionString": "DSN=Test;Driver={Microsoft Text Driver (*.txt, *.csv)};", "SQLQuery": "SELECT reqID, client, reqDate FROM testrequests.txt" } ] } ...but the form fields and out.json are flattened.
@Kenigmatic arrays are not supported. But you may improve the ReadJson and WriteJson methods.
0

I've used the example the above comment and tweak it to the new build checkout my work here https://github.com/Mark-Oasan/ArkApiPluginConfigEditor

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.