NodeNetwork icon indicating copy to clipboard operation
NodeNetwork copied to clipboard

Saving and Loading

Open ccgould opened this issue 5 years ago • 33 comments

Is there a way to save the nodes and load them?

ccgould avatar Jul 15 '18 21:07 ccgould

Yes I wanted to know the same thing. That would be great.

MMJM avatar Jul 15 '18 21:07 MMJM

I have experimented with various ways to serialize the viewmodels, but this turn out to be much harder than it seems. The build-in serialization frameworks in .net do not play nice with unexpected types, circular dependencies, needs a seperate constructor or setup method, ... I also tried Json.net, but that doesn't work with ReactiveUI as the bindings break due to order of deserialization, double initialization of reactive properties, ... If you want more details I have a 3 page document with the approaches I tried and the problems I encountered.

Overall, right now the easiest, most reliable way to do saving would be to either provide a custom serializer implementation for your types, or to convert your viewmodels into models and serialize those (this is what I do in my applications). I might make another attempt someday, but I can't make any promises there.

Wouterdek avatar Jul 16 '18 07:07 Wouterdek

@Wouterdek I am very new to WPF and I am using caliburn mirco with your tool could you make an example of this am trying to figure out things as I go but its a bit hard am trying to break down your lua demo I need the same thing for a XML structure and am tearing my hair out lol

ccgould avatar Jul 16 '18 16:07 ccgould

@ccgould A quick search seems to suggest that caliburn micro and reactiveui do work together, however since I haven't used caliburn I cannot help you there. If you want more instructional guides about NodeNetwork, you can find those here.

Wouterdek avatar Jul 16 '18 20:07 Wouterdek

@Wouterdek Can you share what you have tried out? Also is it possible to give a small example of what you do in your projects so we have something to work from?

Thanks

MMJM avatar Jul 16 '18 22:07 MMJM

I convert my viewmodels to models and back using seperate, dedicated classes. What that class does exactly depends on what your model looks like, but here is a short overview:

// Add an instance of this class to your network viewmodel as a property
// Create similar converter class(es) for your node classes
class NetworkConverter {
    NetworkConverter(NetworkViewModel vm){ ... }

    Network BuildModel()
    {
        // Create node models
        var nodeModels = new Dictionary<NodeViewModel, NodeModel>();
        foreach(var node in vm.Nodes)
        {
            var nodeModel = node.Converter.BuildModel();
            nodeModels[node] = nodeModel;
        }

        // Create connection models
        var connectionModels = new List<Connection>();
        foreach(var connection in vm.Connections)
        {
            // Take the connection input and output, take their corresponding parent node vms,
            // look them up in the nodeModels dictionary, 
            // take the input/output models from the node model
            // and create a connection model using those.
        }

        // Save pure UI data, such as node positions, collapse state, ...
        string metadata = SerializeMetaData(vm);

        return new Network(nodeModels.Values.ToList(), connections, metadata)
    }

    NetworkViewModel LoadModel(Network net)
    { 
        // Create node viewmodels, doing the opposite of what is above.
        
        // Create connection viewmodels, doing the opposite of what is above.
        
        // Load pure UI metadata
        LoadMetaData(vm, net.Metadata);
    }

    private string SerializeMetaData(NetworkViewModel vm)
    { }

    private void LoadMetaData(NetworkViewModel vm, string metadata)
    { }
}

A nice advantage to this strategy is that you can make your models immutable, which makes them easier/safer to use in a database context. As these models are snapshots of your viewmodels, you can also use them to implement the memento pattern, which is useful for stuff like undo/redo.

Wouterdek avatar Jul 17 '18 07:07 Wouterdek

You can find an overview of the things I've tried here

Wouterdek avatar Jul 17 '18 08:07 Wouterdek

@Wouterdek mm interesting I will give this a try and let you know also thanks for responding not many devs respond nowadays

ccgould avatar Jul 17 '18 11:07 ccgould

Hi! Any news about saving/loading?

danvervlad avatar Mar 07 '19 09:03 danvervlad

There have been no changes relating to saving/loading. I recommend writing your own system to transform your viewmodels to models which can be serialized.

Wouterdek avatar Mar 07 '19 10:03 Wouterdek

https://github.com/Traderain/Wolven-kit/blob/master/WolvenKit/MainController.cs#L220 Have you tried json.net like this? It keeps the references and such for me and works fairly well.

Traderain avatar Apr 11 '19 10:04 Traderain

Hi Wouterdek, I see this project (Winform) used BinaryReader/Writer to store, have you read it? https://github.com/komorra/NodeEditorWinforms I don't read it in depth but I think it may help you.

zhenyuan0502 avatar Apr 17 '19 08:04 zhenyuan0502

hi, Wouterdek, i wrote a tool based on your repository and serialize/deserialize the nodes and connections by my self. now the question is when massive nodes was loaded, it takes a long time. i'm new to WPF and ReactiveUI.If you have any suggestions , i'll greatful very much

seraphim0423 avatar Apr 23 '19 12:04 seraphim0423

Hi Wouterdek, I see this project (Winform) used BinaryReader/Writer to store, have you read it? https://github.com/komorra/NodeEditorWinforms I don't read it in depth but I think it may help you.

BinaryWriter provides methods to write bytes/ints/bools/doubles/strings/... but not arbitrary complex datatypes. The implementation used in the library you linked seems to come down to just doing the serialization manually. I think its easier to abstract the persistent state into a model class (as you are supposed to in Model-View-ViewModel code), and serialize this class automatically. This also has other advantages that come with decoupling of viewmodel and view.

hi, Wouterdek, i wrote a tool based on your repository and serialize/deserialize the nodes and connections by my self. now the question is when massive nodes was loaded, it takes a long time. i'm new to WPF and ReactiveUI.If you have any suggestions , i'll greatful very much

Try to run the loading code while profiling to see what runs slowly (Guide) Make sure you are using SuppressChangeNotifications on the Nodes collection when adding a bunch of nodes. (API)

Wouterdek avatar Apr 23 '19 13:04 Wouterdek

I've implemented import/export for the scene in my project. Currently, I create custom lite model classes for every node and connections and place it in scene. After I've just serialized scene to binary. Import and export are working perfectly. You can even import scene several times on the canvas. But what I've noticed - creating of 100 nodes and their connections takes too much time ~10-30sec. Is it going to be improved? Of course, native import/export functionality will be great to have. Is it in plans?

danvervlad avatar Jul 23 '19 08:07 danvervlad

hi, Wouterdek, i wrote a tool based on your repository and serialize/deserialize the nodes and connections by my self. now the question is when massive nodes was loaded, it takes a long time. i'm new to WPF and ReactiveUI.If you have any suggestions , i'll greatful very much

Have you fixed slow nodes creation?

danvervlad avatar Jul 23 '19 08:07 danvervlad

But what I've noticed - creating of 100 nodes and their connections takes too much time ~10-30sec.

How are you adding these? One possible cause may be that you are adding them one by one. If you create a list of all nodes you want to add and then add that list in one call to the network, that should give better performance. Same thing for the connections.

Of course, native import/export functionality will be great to have. Is it in plans?

No, apart from the practical difficulties and development cost of the viewmodel serialization (detailed above) I think it makes more sense to serialize models, which are different in each application.

Wouterdek avatar Jul 23 '19 08:07 Wouterdek

I'm creating nodes in task, but adding in UI thread like this

        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)delegate()
        {
            var suppressChangeNotificationsToken = networkViewModel.SuppressChangeNotifications();
            networkViewModel.Nodes.AddRange(addedNodes);
            networkViewModel.Connections.AddRange(addedConnections);
            suppressChangeNotificationsToken.Dispose();
            SetValuesInEditors(addedNodesPairs);
        });

So, creating of 80 nodes and 107 connection takes ~3 sec, but after adding them to networkViewModel it takes 1 minute 18 sec with unreasonable UI to render. My laptop: I7-6700HQ 2.6Ghz, RAM 16 Gb, SSD.

Maybe It's because of graph validation? Could it be turned off?

danvervlad avatar Jul 26 '19 14:07 danvervlad

@danvervlad The repository contains a project called StressTest which has a button which creates and adds 100 nodes and 99 connections. On my laptop this completes in about 6 seconds. This uses pretty simple nodes though, and no verification. It is hard for me to find out what exactly causes your issue without more information. It could be verification, value propagation, or something else. What version of NodeNetwork are you using? What happens if you only add the nodes, and no connections? Have you tried running a profiler that shows the hotspots that cause the slowdown? Could you perhaps share a small example project that demonstrates this slowdown? Also I suggest creating a separate issue for this topic.

Wouterdek avatar Jul 26 '19 15:07 Wouterdek

Yes, you are right. I've created separate issue for that https://github.com/Wouterdek/NodeNetwork/issues/43

danvervlad avatar Jul 29 '19 11:07 danvervlad

I submitted PR #74 yesterday, perhaps take a look hopefully it can solve your requirements for saving, its a Json string that is produced so should be able to be adapted to any other media for saving such as a database, initial save is done to a dictionary for fast switching and then committed to files upon exit of application, these files are then loaded into the application on start up.

ChrisPulman avatar May 12 '20 19:05 ChrisPulman

any news on this ? What is the current status ?

olluz avatar Jan 04 '21 12:01 olluz

Status is as described above. I still recommend the method described in https://github.com/Wouterdek/NodeNetwork/issues/9#issuecomment-405494795

Wouterdek avatar Jan 04 '21 12:01 Wouterdek

Moved

PlateMug avatar Apr 18 '21 15:04 PlateMug

@PlateMug Send me a runnable project and I'll take a look. Also, please create a separate issue for this question as to avoid cluttering this thread.

Wouterdek avatar Apr 18 '21 18:04 Wouterdek

Is there a way to save the nodes and load them? 请告诉我,大神

ZhuMaziqiang avatar Mar 08 '23 13:03 ZhuMaziqiang

Status is as described above. I still recommend the method described in #9 (comment)

I followed this instruction and created the following code. But it does not seem to work. I am not sure what properties to save from a connection view model to recreate it later when the nodes are loaded. Could you point me in a right direction to see what I am doing wrong? Thanks.

public class NetworkConverter
{
    private readonly NetworkViewModel _vm;

    public NetworkConverter(NetworkViewModel vm)
    {
        _vm = vm;
    }

    public Network BuildModel()
    {
        var nodeViewModels = _vm.Nodes.Items.Cast<NodeViewModelBase>().ToList();
        var nodeModels = new Dictionary<NodeViewModelBase, NodeModelBase>();
        foreach (NodeViewModel? node in _vm.Nodes.Items)
            if (node is NodeViewModelBase nodeModel)
                nodeModels[nodeModel] = nodeModel.Data;

        var connectionModels = new List<Connection>();
        foreach (var connection in _vm.Connections.Items)
        {
            var sourceVM = connection.Input.Parent as NodeViewModelBase;
            var targetVm = connection.Output.Parent as NodeViewModelBase;
            var inputVM = connection.Input;
            var outputVM = connection.Output;
            connectionModels.Add(new Connection(inputVM, outputVM));
        }

        List<NodeMetadata> metadata = new List<NodeMetadata>();
        foreach (NodeViewModel? node in _vm.Nodes.Items)
            if (node is NodeViewModelBase nodeModel)
            {
                var m = new NodeMetadata
                {
                    Postion = nodeModel.Position,
                    State = nodeModel.IsCollapsed
                };
                metadata.Add(m);
            }

        return new Network(nodeViewModels, nodeModels.Values.ToList(), connectionModels, metadata);
    }


    private NetworkViewModel LoadModel(Network net)
    {
        NetworkViewModel networkVm = new NetworkViewModel();
        foreach (var nodeVm in net.NodeViewModels)
        {
            nodeVm.Position = net.Metadata.First(x => x.Postion == nodeVm.Position).Postion;
            nodeVm.IsCollapsed = net.Metadata.First(x => x.Postion == nodeVm.Position).State;
        }

        networkVm.Nodes.AddRange(net.NodeViewModels);
        foreach (var connection in net.Connections)
        {
            var con = networkVm.ConnectionFactory(connection.Input, connection.Output);
            networkVm.Connections.Edit(x => x.Add(con));
        }

        return networkVm;
    }
}

Serialisation code.

var converter = new NetworkConverter(NetworkViewModel);
var res = converter.BuildModel();
var str = JsonConvert.SerializeObject(res, Formatting.Indented);

Output:

{
  "NodeViewModels": [
    {},
    {}
  ],
  "Models": [
    {
      "Data1": null,
      "Data2": null
    },
    {
      "Data1": null,
      "Data2": null
    }
  ],
  "Connections": [
    {
      "Input": {},
      "Output": {},
      "SourceVm": null,
      "TargetVm": null
    }
  ],
  "Metadata": [
    {
      "Postion": "0,30",
      "State": false
    },
    {
      "Postion": "299,207",
      "State": false
    }
  ]
}

trrahul avatar Apr 03 '23 12:04 trrahul

Well, I have solved it. This serializes the nodes and the connections to JSON, and I am able to redraw the nodes from the JSON.

public class NetworkConverter
{
    private readonly NetworkViewModel _vm;

    public NetworkConverter(NetworkViewModel vm)
    {
        _vm = vm;
    }

    public Network BuildModel()
    {
        List<NodeViewModelBase> nodeViewModels = _vm.Nodes.Items.Cast<NodeViewModelBase>().ToList();
        List<SerializedNode> serializedNodes = new List<SerializedNode>();
        List<SerializedConnection> serializedConnections = new List<SerializedConnection>();
        foreach (NodeViewModelBase modelBase in nodeViewModels)
        {
            SerializedNode node = new SerializedNode
            {
                Id = modelBase.Id,
                ModelData = modelBase.Data,
                Type = modelBase.GetType().FullName!,
                Postion = modelBase.Position,
                State = modelBase.IsCollapsed
            };
            serializedNodes.Add(node);
        }

        foreach (var connection in _vm.Connections.Items)
            serializedConnections.Add(new SerializedConnection
            {
                From = (connection.Output.Parent as NodeViewModelBase)!.Id,
                To = (connection.Input.Parent as NodeViewModelBase)!.Id,
                InputName = connection.Input.Name,
                OutputName = connection.Output.Name
            });
        
        return new Network(serializedNodes, serializedConnections);
    }


    public NetworkViewModel LoadModel(Network net)
    {
        List<NodeViewModelBase> nodes = new List<NodeViewModelBase>();
        foreach (var nodeVm in net.SerializedNodes)
        {
            var type = Type.GetType(nodeVm.Type);
            if (type == null)
                throw new Exception("Type not found");
            var node = (NodeViewModelBase)Activator.CreateInstance(type);
            node.Id = nodeVm.Id;
            node.Data = nodeVm.ModelData;
            nodes.Add(node);
            node.Position = nodeVm.Postion;
            node.IsCollapsed = nodeVm.State;
        }


        _vm.Nodes.AddRange(nodes);
        foreach (var connection in net.SerializedConnections)
        {
            var from = nodes.FirstOrDefault(x => x.Id == connection.From);
            var to = nodes.FirstOrDefault(x => x.Id == connection.To);
            if (from == null || to == null)
                throw new Exception("Node not found");
            var fromOutput = from.Outputs.Items.FirstOrDefault(x => x.Name == connection.OutputName);
            var toInput = to.Inputs.Items.FirstOrDefault(x => x.Name == connection.InputName);
            if (fromOutput == null || toInput == null)
                throw new Exception("Input or Output not found");
            var con = _vm.ConnectionFactory(toInput, fromOutput);
            _vm.Connections.Edit(x => x.Add(con));
        }

        return _vm;
    }
}

public class SerializedConnection
{
    public Guid From { get; set; }
    public Guid To { get; set; }
    public string InputName { get; set; }
    public string OutputName { get; set; }
}

public class SerializedNode
{
    public string Type { get; set; }
    public Guid Id { get; set; }
    public Point Postion { get; set; }
    public bool State { get; set; }
    public NodeModelBase ModelData { get; set; }
}

trrahul avatar Apr 04 '23 07:04 trrahul

Can you add an example about saving and loading?

zp-95 avatar Jun 26 '23 08:06 zp-95

Can you add an example about saving and loading?

I decided not to use this library after a couple of experiments and I don't have any code to refer now. I have a vague memory and it goes like this.

var vm = your NetworkViewModel;
var converter = new NetworkConverter(vm);
var serialisedNetwork = converter.Build();
File.Write(JsonSerializer.Serialize(serializedNetwork);

trrahul avatar Jun 29 '23 11:06 trrahul