Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Saving and Loading #9

Open
ccgould opened this issue Jul 15, 2018 · 33 comments
Open

Saving and Loading #9

ccgould opened this issue Jul 15, 2018 · 33 comments
Labels

Comments

@ccgould
Copy link

ccgould commented Jul 15, 2018

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

@ccgould ccgould closed this as completed Jul 15, 2018
@ccgould ccgould reopened this Jul 15, 2018
@S4RD7R
Copy link

S4RD7R commented Jul 15, 2018

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

@Wouterdek
Copy link
Owner

Wouterdek commented Jul 16, 2018

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.

@ccgould
Copy link
Author

ccgould commented Jul 16, 2018

@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

@Wouterdek
Copy link
Owner

@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.

@S4RD7R
Copy link

S4RD7R commented Jul 16, 2018

@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

@Wouterdek
Copy link
Owner

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
Copy link
Owner

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

@ccgould
Copy link
Author

ccgould commented Jul 17, 2018

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

@danvervlad
Copy link

Hi! Any news about saving/loading?

@Wouterdek
Copy link
Owner

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.

@Traderain
Copy link

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.

@zhenyuan0502
Copy link

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.

@seraphim0423
Copy link

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

@Wouterdek
Copy link
Owner

Wouterdek commented Apr 23, 2019

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)

@danvervlad
Copy link

danvervlad commented Jul 23, 2019

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
Copy link

danvervlad commented Jul 23, 2019

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?

@Wouterdek
Copy link
Owner

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.

@danvervlad
Copy link

danvervlad commented Jul 26, 2019

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?

@Wouterdek
Copy link
Owner

@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.

@danvervlad
Copy link

danvervlad commented Jul 29, 2019

Yes, you are right. I've created separate issue for that #43

@ChrisPulman
Copy link

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.

@olluz
Copy link

olluz commented Jan 4, 2021

any news on this ? What is the current status ?

@Wouterdek
Copy link
Owner

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

@PlateMug
Copy link

PlateMug commented Apr 18, 2021

Moved

@Wouterdek
Copy link
Owner

@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.

@ZhuMaziqiang
Copy link

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

@trrahul
Copy link

trrahul commented Apr 3, 2023

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
Copy link

trrahul commented Apr 4, 2023

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; }
}

@zp-95
Copy link

zp-95 commented Jun 26, 2023

Can you add an example about saving and loading?

@trrahul
Copy link

trrahul commented Jun 29, 2023

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);

@Xinmer
Copy link

Xinmer commented Aug 18, 2023

I can already save and load the Network, but when loading 100 Nodes will cause obvious stuttering, it takes about 10 seconds, is there any way to speed it up?

@glenwrhodes
Copy link

I've got a good, robust loading and saving working, including position. I'm trying to figure out how to save/load the size of the nodes though. I know in the NodeViewModel there's a Size property, and I can take that, serialize it and deserialize it, however when I set the value of the Size property programmatically, it doesn't resize the view. I've tried resizing the view manually by setting its Width and Height, which works, but then the drag handles don't let me resize the node to be smaller than the value I resized it to. I can only resize it to be larger. Any thoughts would be appreciated.

@Wouterdek
Copy link
Owner

@glenwrhodes
Try MinWidth/MinHeight, as found in the source code here

private void ApplyResize(DragDeltaEventArgs e, bool horizontal, bool vertical)

@Wouterdek Wouterdek reopened this Nov 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests