Blazor-Diagrams / Blazor.Diagrams

A fully customizable and extensible all-purpose diagrams library for Blazor
https://blazor-diagrams.zhaytam.com
MIT License
983 stars 192 forks source link

How to store diagram to database? #148

Closed IbrahimTimimi closed 2 years ago

IbrahimTimimi commented 2 years ago

I want to save diagram to database and then display the diagram again when needed. How to do that?

IbrahimTimimi commented 2 years ago

I managed to make it work. Now I can store the diagram to database and show again. My approach is pretty basic, trying to learn my way.

Models for node and link list.

   public class NodeObj
    {
        public string NodeId { get; set; }
        public string Data { get; set; }
        public Point Position { get; set; }
        public int Type { get; set; }
    }

    public class LinkObj
    {
        public string SourceId { get; set; }
        public string TargetId { get; set; }
        public PortAlignment SourceAlignment { get; set; }
        public PortAlignment TargetAlignment { get; set; }
    }

Save the diagram:

private void SaveModal()
    {

        var list = GetOrder(); // see my previous issue
        obj.NodeList.Clear();
        int count = 1;

        foreach (var id in list)
        {
            if (_diagram.Nodes.Any(x => x.Id == id))
            {
                var myNode = _diagram.Nodes.Where(x => x.Id == id).Single();
                var nodedata = new NodeObj()
                {
                    Data = (myNode is IfConditionNode nodeif) ? nodeif.GetData() : string.Empty,
                    NodeId = myNode.Id,
                    Position = myNode.Position,
                    Type = (myNode is IfConditionNode nodeif2) ? 1 : 0
                };

                obj.NodeList.Add(count, nodedata);
                count++;
            }
        }

        obj.LinkList = new List<LinkObj>();

        foreach (var item in _diagram.Links)
        {
            obj.LinkList.Add(new LinkObj()
            {
                SourceId = item.SourceNode.Id,
                TargetId = item.TargetNode.Id,
                SourceAlignment = item.SourcePort.Alignment,
                TargetAlignment = item.TargetPort.Alignment
            });
        }

        obj.SerializeAll();

        var local = db.Set<DeviceWorkflow>().Local
               .FirstOrDefault(x => x.Id == this.obj.Id);

        if (local != null)
            db.Entry(local).State = EntityState.Detached; // detach

        db.Entry(obj).State = EntityState.Modified;

        db.SaveChanges();
    }

Display diagram:

        _diagram.Nodes.Clear();
        _diagram.Links.Clear();
        foreach (KeyValuePair<int,NodeObj> item in obj.NodeList)
        {
            var position = item.Value.Position;
            var data = item.Value.Data;
            var node = item.Value.Type == 0 ? new NodeModel(position) : new IfConditionNode(position);
            node.RefId = item.Value.NodeId;
            node.AddPort(PortAlignment.Top);
            node.AddPort(PortAlignment.Bottom);
            _diagram.Nodes.Add(node);
        }

        if (obj.LinkList != null)
        {
            foreach (var item in obj.LinkList)
            {
                _diagram.Links.Add(new LinkModel(
                     _diagram.Nodes.Where(x => x.RefId == item.SourceId).Single().GetPort(item.SourceAlignment),
                     _diagram.Nodes.Where(x => x.RefId == item.TargetId).Single().GetPort(item.TargetAlignment)));
            }
        }
zHaytam commented 2 years ago

This is the best way to do it honestly. At least until the library has something built-in, which will have more data than your backend will probably need. Your solution is what I personally use, and what others should probably use unless the point is just to save/load quickly.

PS: I'm still working on the built-in solution.

IbrahimTimimi commented 2 years ago

Hi @zHaytam. I'm very pleased to hear that especially from a big programmer like you. Thanks.

In response, I will update on my progress on this and explain more on what I have done.

I'm using the diagrams as workflow in my project thats why I will call them workflow, but it can be flowchart or any other type of diagram. I created the models below that will store minimal data of the serialized nodes and links to database and be able to re-create the diagrams again.

    public class NodeObj
    {
        public string NodeId { get; set; }
        public string Data { get; set; }
        public Point Position { get; set; }
        public NodesEnum Type { get; set; }
    }

    public class LinkObj
    {
        public string SourceId { get; set; }
        public string TargetId { get; set; }
        public PortAlignment SourceAlignment { get; set; }
        public PortAlignment TargetAlignment { get; set; }
    }

All the properties are straighforward except the NodesEnum which will store the type of node it is. This to me will come in handy when trying to create instances of the custom nodes especially when you have many.

public enum NodesEnum
    {
        StartNode,
        StopNode,
        MessageBoxNode,
        IfConditionNode,
        DelayNode,
    }

When the application is started, I load all the workflows from the database and deserialize into list of workflows that contain List<NodeObj> and List<LinkObj> , NodeList and LinkList respectively. The workflow model is as below:

public class DeviceWorkflow
{
       [Key]
       [Column("Id")]
       public string Id { get; set; }

       ...

        [NotMapped]
        [JsonIgnore]
        [IgnoreDataMember]
        public List<NodeModel> Nodes { get; set; }

        [Column("NodeList")]
        public string NodeListJson { get; set; }

        [NotMapped]
        public SortedList<int, NodeObj> NodeList { get; set; }

        [Column("Links")]
        public string LinkListJson { get; set; }

        [NotMapped]
        public List<LinkObj> LinkList { get; set; }
}

Inside the workflow constructor, I deserialize NodeListJson and LinkListJson to their respective objects. Then I create the nodes using the following code.

this.Nodes.Clear();
foreach (KeyValuePair<int, NodeObj> item in this.NodeList)
{
    // Get the type of node using fullname assembly
    // The NodeEnum has all the names of the custom nodes
        // e.g. Actuator.App.Workflow.StartNode
        // this will allow us to create instance of any node without using an if condition like I used earlier

    Type nodeType = Type.GetType($"Actuator.App.Workflow.{item.Value.Type}");

       // Create parameter object to create an instance.
       // Include the NodeModel constructors in your custom nodes
       // Check the StartNode model below
    var param = new Object[] { item.Value.Position, RenderLayer.HTML, null }; //
    var node = (NodeModel)Activator.CreateInstance(nodeType, param);

        // Custom override method for custom nodes
    node.LoadNodeData(item.Value.Data);

    // I added RefId property in NodeModel to keep track of the node
    // Also, I was not able to use the NodeModel Id because it was readonly
    node.RefId = item.Value.NodeId;

    // Add top port for all except the start node
    if (item.Value.Type != NodesEnum.StartNode)
        node.AddPort(PortAlignment.Top);

    // Add bottom port for all except the stop node
    if (item.Value.Type != NodesEnum.StopNode)
        node.AddPort(PortAlignment.Bottom);

    this.Nodes.Add(node);
}

Additionally, you can create override methods in NodeModel object to execute in your custom nodes. It will allow you to restore some additional node data or anything else you want to do with the nodes. I wanted to create an interface but this approached worked for me.

public class NodeModel : MovableModel
{
    ...
    public virtual void LoadNodeData(string data) { }
}

public class StartNode : NodeModel
{
    public StartModel obj { get; set; }

    // Must add these two constructor to allow to create instances using assembly fullname of the node model
    public StartNode(Point position = null) : base(position)
    {
        obj = new StartModel();
    }

    public StartNode(Point? position = null, RenderLayer layer = RenderLayer.HTML,
        ShapeDefiner? shape = null) : base(position)
    {
        obj = new StartModel();
    }

    public override void LoadNodeData(string data)
    {
       // do what you want
    }
}

Finally, you can open and save diagrams using the following methods.

public void OpenDiagram()
{
    if (obj == null)
        return;

    _diagram.Nodes.Clear();
    _diagram.Links.Clear();

    foreach (var node in obj.Nodes)
    {
        _diagram.Nodes.Add(node);
    }

        // Add the node links
    if (obj.LinkList != null)
    {
        foreach (var item in obj.LinkList)
        {
            _diagram.Links.Add(new LinkModel(
                 _diagram.Nodes.Where(x => x.RefId == item.SourceId).Single()
                                            .GetPort(item.SourceAlignment),
                 _diagram.Nodes.Where(x => x.RefId == item.TargetId).Single()
                                            .GetPort(item.TargetAlignment)));
        }
    }
}

private void SaveDiagram()
{
    // Get the list of nodes according to their order in the diagram
    // Check my previous issue for an idea
    var list = GetOrder();

    obj.NodeList.Clear();

    // The objective is to get all the nodes into NodeList
    // and links into LinkList

    if (myServices.DeviceWorkflows.Any(x => x.Id == obj.Id))
    {
        int index = myServices.DeviceWorkflows.FindIndex(x => x.Id == obj.Id);
        myServices.DeviceWorkflows[index].NodeList.Clear();
        myServices.DeviceWorkflows[index].Nodes.Clear();

        int count = 1;

        // loop through the ordered list of nodes by Id
        foreach (var id in list)
        {
            if (_diagram.Nodes.Any(x => x.Id == id))
            {
                var myNode = _diagram.Nodes.Where(x => x.Id == id).Single();

                // get the name of the node to store our enum value
                var nodeName = myNode.GetType().FullName.Split(".").Last();
                NodesEnum nodeEnum = (NodesEnum)System.Enum.Parse(typeof(NodesEnum), nodeName);

                var nodedata = new NodeObj()
                {
                    Data = myNode.GetNodeData(),
                    NodeId = myNode.Id,
                    Position = myNode.Position,
                    Type = nodeEnum
                };

                myServices.DeviceWorkflows[index].Nodes
                    .Add(_diagram.Nodes.Where(x => x.Id == id).Single());

                myServices.DeviceWorkflows[index].NodeList
                    .Add(count, nodedata);

                count++;
            }
        }
    }

    obj.LinkList = new List<LinkObj>();

    foreach (var item in _diagram.Links)
    {
        obj.LinkList.Add(new LinkObj()
        {
            SourceId = item.SourceNode.Id,
            TargetId = item.TargetNode.Id,
            SourceAlignment = item.SourcePort.Alignment,
            TargetAlignment = item.TargetPort.Alignment
        });
    }

    // Serialize the NodeList and LinkList
    obj.SerializeAll();

    var local = db.Set<DeviceWorkflow>().Local
           .FirstOrDefault(x => x.Id == this.obj.Id);

    if (local != null)
        db.Entry(local).State = EntityState.Detached; // detach

    db.Entry(obj).State = EntityState.Modified;

    db.SaveChanges();
}

Screenshot 2021-10-30 at 23-07-08

My implementation is not perfect but I hope it helps someone who just starting out like I was before.

Unfornutely, I won't be able to work on this workflow project after next week as will be leaving my job soon. I will hand it to my colleage. But I will continue to do a blazor diagram project on my free time and contribute something to the project. I have learnt a lot working on this amazing library.

Thank you very much @zHaytam.