slaveOftime
blazordotnetdiagrams

The usage of Blazor.Diagrams

2024-06-11

Blazor.Diagrams demos

In the above image, it demostrated muptile features:

  1. Unified node editor
  2. Customized link style
  3. Extended nodes
  4. Selection/deleting/pasting/alignment

There are a lot of out of box features from Blazor.Diagrams nuget package also.

  • Grid widget
  • Overview widget
  • Selection/deleting
  • Drag/drop for movement
  • Ports/links connection

and more...

Host the diagram

<ContextMenuTrigger MenuId="dashboardMenu" CssClass="w-full h-full">
    <CascadingValue Value="diagram" IsFixed="true">
        <DiagramCanvas>
            <Widgets>
                <SelectionBoxWidget />
                @if (showGrid)
                {
                    <GridWidget Size="24" Mode="GridMode.Line" BackgroundColor="transparent" />
                }
                <NavigatorWidget Width="200" Height="120" Class="border-1px border-slate-200 bg-white absolute bottom-2 right-2 shadow-md" />
            </Widgets>
        </DiagramCanvas>
    </CascadingValue>
</ContextMenuTrigger>

public partial class Dashboard
{
    [Parameter] public long? Id { get; set; }
    [Parameter] public string? Name { get; set; }
    [Parameter] public string? Descrition { get; set; }

    [Inject] public required IModalService ModalService { get; set; }
    [Inject] public required PlcDbContext PlcDbContext { get; set; }
    [Inject] public required NavigationManager NavigationManager { get; set; }

    private readonly Dictionary<string, string> customizedLinkStyle = [];

    private bool isLoading;
    private bool showGrid;
    private bool isPointerReleased = true;
    private BlazorDiagram diagram = null!;
    private Db.Dashboard? dashboard;
    private Model? editingModel;

    protected override void OnInitialized()
    {
        var options = new BlazorDiagramOptions
        {
            AllowMultiSelection = true,
            AllowPanning = true,
            GridSnapToCenter = true,
            GridSize = 4,
            Zoom =
            {
                Enabled = true,
            },
            Links =
            {
                DefaultRouter = new NormalRouter(),
                DefaultPathGenerator = new SmoothPathGenerator()
            },
        };

        diagram = new BlazorDiagram(options);

        NodeModelBase.RegisterAllDerivedModels(diagram);

        diagram.UnregisterBehavior<SelectionBehavior>();
        diagram.UnregisterBehavior<DragMovablesBehavior>();
        diagram.RegisterBehavior(new SelectionBehavior2(diagram));
        diagram.RegisterBehavior(new DragMovablesBehavior(diagram));

        diagram.SelectionChanged += Diagram_SelectionChanged;
        diagram.PointerDown += Diagram_PointerDown;
        diagram.PointerUp += Diagram_PointerUp;

        diagram.Links.Added += Links_Added;
        diagram.Links.Removed += Links_Removed;
        diagram.Nodes.Removed += Nodes_Removed;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && Id.HasValue)
        {
            await Task.Delay(10); // Let the diagram layout to be fully ready
            await LoadDashboard();
        }
    }

    private void Diagram_SelectionChanged(SelectableModel model)
    {
        if (model.Selected)
        {
            editingModel = model;
        }
        else
        {
            editingModel?.Refresh();
            editingModel = null;
        }

        StateHasChanged();
    }

    private void Diagram_PointerDown(Model? arg1, Blazor.Diagrams.Core.Events.PointerEventArgs arg2)
    {
        isPointerReleased = false;
        StateHasChanged();
    }

    private void Diagram_PointerUp(Model? arg1, Blazor.Diagrams.Core.Events.PointerEventArgs arg2)
    {
        isPointerReleased = true;
        StateHasChanged();
    }

    private void Links_Added(BaseLinkModel link)
    {
        if (link is not StyledLinkModel linkModel)
        {
            link.Changed += Link_Changed;
        }
    }

    private void Links_Removed(BaseLinkModel obj)
    {
        if (obj is StyledLinkModel linkModel)
        {
            linkModel.Changed -= StyledLinkModel_Changed;
        }
        StateHasChanged();
    }

    private void Nodes_Removed(NodeModel obj)
    {
    }

    private void Link_Changed(Model link)
    {
        // Relapace default LinkModel with StyledLinkModel
        if (link is LinkModel linkModel && linkModel.IsAttached)
        {
            link.Changed -= Link_Changed;

            var styledLinkModel = new StyledLinkModel(linkModel.Source, linkModel.Target);

            diagram.Links.Remove(linkModel);
            diagram.Links.Add(styledLinkModel);
            diagram.SelectModel(styledLinkModel, true);

            styledLinkModel.Changed += StyledLinkModel_Changed;
            StyledLinkModel_Changed(styledLinkModel);
        }
    }

    private void StyledLinkModel_Changed(Model model)
    {
        if (model is StyledLinkModel styledLinkModel)
        {
            if (styledLinkModel.Dash != 0 || styledLinkModel.Color != StyledLinkModel.DefaultColor)
            {
                customizedLinkStyle[styledLinkModel.Id] = styledLinkModel.MakeStyleClass();
            }
            else
            {
                customizedLinkStyle.Remove(styledLinkModel.Id);
            }
            StateHasChanged();
        }
    }

    private void AddNode(ItemClickEventArgs e, NodeModel node)
    {
        node.Position = new Point(e.MouseEvent.ClientX - e.MouseEvent.OffsetX - diagram.Pan.X, e.MouseEvent.ClientY - e.MouseEvent.OffsetY - 80 - diagram.Pan.Y);
        diagram.Nodes.Add(node);
        diagram.SelectModel(node, true);
        StateHasChanged();
    }

    private async Task Save()
    {
        try
        {
            isLoading = true;
            StateHasChanged();

            if (dashboard == null)
            {
                dashboard = new Db.Dashboard();
                PlcDbContext.Dashboards.Add(dashboard);
            }

            dashboard.Name = Name ?? "PLC Dashboard";
            dashboard.Description = Descrition;
            dashboard.Layout = DashboardLayout.FromModels(diagram.Nodes, diagram.Links).SerializeAsJson();

            await PlcDbContext.SaveChangesAsync();

            Id = dashboard.Id;
            //await LoadDashboard(dashboard);

            NavigationManager.NavigateTo(NavigationManager.GetUriWithQueryParameter(Dashboards.SelectedIdQueryName, dashboard.Id));
        }
        catch (Exception ex)
        {
            isLoading = false;
            await ModalService.ShowSimpleDialog("Save dashboard failed", ex.Message, level: NotificationLevel.Error);
        }
        finally
        {
            isLoading = false;
        }
    }

    private async Task LoadDashboard(Db.Dashboard? dashboardFromDb = null)
    {
        if (!Id.HasValue) return;

        try
        {
            isLoading = true;
            StateHasChanged();

            diagram.Nodes.Clear();
            diagram.Links.Clear();

            dashboard = dashboardFromDb ?? await PlcDbContext.Dashboards.FirstOrDefaultAsync(x => x.Id == Id);
            if (dashboard != null)
            {
                Name = dashboard.Name;
                Descrition = dashboard.Description;

                DashboardLayout.FromJson(dashboard.Layout).ApplyToDiagram(diagram, StyledLinkModel_Changed);
            }
        }
        catch (Exception ex)
        {
            isLoading = false;
            await ModalService.ShowSimpleDialog("Load dashboard failed", ex.Message, level: NotificationLevel.Error);
        }
        finally
        {
            isLoading = false;
            StateHasChanged();
        }
    }

    private void CloseModelEditor()
    {
        editingModel?.Refresh();
        editingModel = null;
    }

    private void PasteSelectedItems(ItemClickEventArgs e)
    {
        var selectedItems = diagram.GetSelectedModels();
        if (selectedItems.Any())
        {
            var nodes = selectedItems.Where(x => x is NodeModelBase).Select(x => (x as NodeModelBase)!).ToList();
            var links = selectedItems.Where(x => x is BaseLinkModel).Select(x => (x as BaseLinkModel)!).ToHashSet()!;

            // If link is attached between selected nodes, we should also include it for copying
            foreach (var node in nodes)
            {
                var tryAddLink = (BaseLinkModel? link) =>
                {
                    if (link?.Source.Model is PortModel sourcePortModel && nodes.Contains(sourcePortModel.Parent) &&
                        link?.Target.Model is PortModel targetPortModel && nodes.Contains(targetPortModel.Parent))
                    {
                        links.Add(link);
                    }
                };

                foreach (var link in node.Links)
                {
                    tryAddLink(link);
                }

                foreach (var link in node.PortLinks)
                {
                    tryAddLink(link);
                }
            }

            var json = DashboardLayout.FromModels(nodes, links).SerializeAsJson();

            var firstNode = nodes.FirstOrDefault();

            // TODO: The delta needs to be optimized
            var deltaX = e.MouseEvent.ClientX - e.MouseEvent.OffsetX - diagram.Pan.X - firstNode?.Position.X;
            var deltaY = e.MouseEvent.ClientY - e.MouseEvent.OffsetY - diagram.Pan.Y - firstNode?.Position.Y;

            var layout = DashboardLayout.FromJson(json);
            foreach (var node in layout.Nodes)
            {
                var oldRefId = node.RefId;
                var newPosition = node.Position.Add(deltaX ?? 0, deltaY ?? 0);

                node.RefId = Guid.NewGuid();
                node.SetPosition(newPosition.X, newPosition.Y);

                // Replace with new RefId
                foreach (var link in layout.Links)
                {
                    if (link.SourceId == oldRefId) link.SourceId = node.RefId;
                    if (link.TargetId == oldRefId) link.TargetId = node.RefId;
                }
            }

            // Unselect all first, so we can select pasted items later
            diagram.UnselectAll();

            layout.ApplyToDiagram(
                diagram,
                mapNode: node =>
                {
                    diagram.SelectModel(node, false);
                    return node;
                },
                mapLink: link =>
                {
                    diagram.SelectModel(link, false);
                    return link;
                }
            );
        }
    }

    private void DeleteSelectedItems()
    {
        foreach (var item in diagram.GetSelectedModels())
        {
            if (item is NodeModel node) diagram.Nodes.Remove(node);
            else if (item is LinkModel link) diagram.Links.Remove(link);
        }
    }

    private void AlignToTop()
    {
        var nodes = diagram.GetSelectedModels().Where(x => x is NodeModelBase).Select(x => (NodeModelBase)x).ToList();
        if (nodes.Count > 1)
        {
            var y = nodes.Select(x => x.Position.Y).Min();
            foreach (var node in nodes)
            {
                node.SetPosition(node.Position.X, y);
            }
        }
    }

    private void AlignToBottom()
    {
        var nodes = diagram.GetSelectedModels().Where(x => x is NodeModelBase).Select(x => (NodeModelBase)x).ToList();
        if (nodes.Count > 1)
        {
            var y = nodes.Select(x => x.Position.Y + x.Size?.Height).Max();
            foreach (var node in nodes)
            {
                node.SetPosition(node.Position.X, y - node.Size?.Height ?? 0);
            }
        }
    }

    private void AlignToLeft()
    {
        var nodes = diagram.GetSelectedModels().Where(x => x is NodeModelBase).Select(x => (NodeModelBase)x).ToList();
        if (nodes.Count > 1)
        {
            var x = nodes.Select(x => x.Position.X).Min();
            foreach (var node in nodes)
            {
                node.SetPosition(x, node.Position.Y);
            }
        }
    }

    private void AlignToRight()
    {
        var nodes = diagram.GetSelectedModels().Where(x => x is NodeModelBase).Select(x => (NodeModelBase)x).ToList();
        if (nodes.Count > 1)
        {
            var x = nodes.Select(x => x.Position.X + x.Size?.Width).Max();
            foreach (var node in nodes)
            {
                node.SetPosition(x - node.Size?.Width ?? 0, node.Position.Y);
            }
        }
    }
}

Serialize diagram for saving

With System.Text.Json I can simplify the serialization and deserialization:

[JsonDerivedType(typeof(TextNodeModel), typeDiscriminator: nameof(TextNodeModel))]
[JsonDerivedType(typeof(TagNodeModel), typeDiscriminator: nameof(TagNodeModel))]
[JsonDerivedType(typeof(UrlNodeModel), typeDiscriminator: nameof(UrlNodeModel))]
[JsonDerivedType(typeof(ImageNodeModel), typeDiscriminator: nameof(ImageNodeModel))]
[JsonDerivedType(typeof(EmbedNodeModel), typeDiscriminator: nameof(EmbedNodeModel))]
public class NodeModelBase : NodeModel
{
    public Guid RefId { get; set; } = Guid.NewGuid();

    public static void RegisterAllDerivedModels(BlazorDiagram diagram)
    {
        diagram.RegisterComponent<TextNodeModel, TextNode>();
        diagram.RegisterComponent<TagNodeModel, TagNode>();
        diagram.RegisterComponent<UrlNodeModel, UrlNode>();
        diagram.RegisterComponent<ImageNodeModel, ImageNode>();
        diagram.RegisterComponent<EmbedNodeModel, EmbedNode>();
    }
}

Create a DashboardLayout class to manage all the saving related logic (currently only support Nodes and Links):

public class DashboardLayout
{
    public required IEnumerable<NodeModelBase> Nodes { get; init; }
    public required IEnumerable<LinkContext> Links { get; init; }


    public string SerializeAsJson()
    {
        return JsonSerializer.Serialize(this, jsonSerializerOptions);
    }

    public void ApplyToDiagram(Diagram diagram, Action<Model>? StyledLinkModel_Changed = null, Func<NodeModelBase, NodeModelBase>? mapNode = null, Func<BaseLinkModel, BaseLinkModel>? mapLink = null)
    {
        diagram.Nodes.Add(mapNode == null ? Nodes : Nodes.Select(mapNode));

        foreach (var link in Links)
        {
            var sourceNode = diagram.Nodes.Where(x => x is NodeModelBase baseNode && baseNode.RefId == link.SourceId).FirstOrDefault();
            var targetNode = diagram.Nodes.Where(x => x is NodeModelBase baseNode && baseNode.RefId == link.TargetId).FirstOrDefault();
            if (sourceNode != null && targetNode != null)
            {
                var sourcePort = sourceNode.GetPort(link.SourceAlignment);
                var targetPort = targetNode.GetPort(link.TargetAlignment);
                if (sourcePort != null && targetPort != null)
                {
                    var linkModel = new StyledLinkModel(sourcePort, targetPort)
                    {
                        Dash = link.LineDash,
                        Color = link.LineColor,
                        Animated = link.LineAnimated,
                    };

                    if (StyledLinkModel_Changed != null)
                    {
                        linkModel.Changed += StyledLinkModel_Changed;
                    }

                    if (link.LineShape.HasValue)
                    {
                        switch (link.LineShape.Value)
                        {
                            case LineShape.Curve:
                                linkModel.Router = new NormalRouter();
                                linkModel.PathGenerator = new SmoothPathGenerator();
                                break;
                            case LineShape.Orthogonal:
                                linkModel.Router = new OrthogonalRouter();
                                linkModel.PathGenerator = new StraightPathGenerator();
                                break;
                            default:
                                break;
                        }
                    }

                    if (!string.IsNullOrEmpty(link.SourceMarkerPath))
                    {
                        linkModel.SourceMarker = new LinkMarker(link.SourceMarkerPath, link.SourceMarkerWidth);
                    }
                    if (!string.IsNullOrEmpty(link.TargetMarkerPath))
                    {
                        linkModel.TargetMarker = new LinkMarker(link.TargetMarkerPath, link.TargetMarkerWidth);
                    }

                    diagram.Links.Add(mapLink == null ? linkModel : mapLink(linkModel));
                }
            }
        }
    }


    private static readonly JsonSerializerOptions jsonSerializerOptions = new()
    {
        IgnoreReadOnlyFields = true,
        IgnoreReadOnlyProperties = true,
    };

    public static DashboardLayout FromModels(IEnumerable<NodeModel> nodeModels, IEnumerable<BaseLinkModel> linkModels)
    {
        var nodes = new List<NodeModelBase>();
        var links = new List<LinkContext>();

        foreach (var link in linkModels)
        {
            if (link.Source.Model is PortModel sourcePort && sourcePort.Parent is NodeModelBase sourceNode &&
                link.Target.Model is PortModel targetPort && targetPort.Parent is NodeModelBase targetNode)
            {
                var linkContext = new LinkContext()
                {
                    SourceId = sourceNode.RefId,
                    SourceAlignment = sourcePort.Alignment,
                    TargetId = targetNode.RefId,
                    TargetAlignment = targetPort.Alignment,
                    LineShape = link.Router switch
                    {
                        NormalRouter _ => LineShape.Curve,
                        OrthogonalRouter _ => LineShape.Orthogonal,
                        _ => null,
                    },
                    SourceMarkerPath = link.SourceMarker?.Path,
                    SourceMarkerWidth = link.SourceMarker?.Width ?? 10,
                    TargetMarkerPath = link.TargetMarker?.Path,
                    TargetMarkerWidth = link.TargetMarker?.Width ?? 10,
                };

                if (link is StyledLinkModel styledLinkModel)
                {
                    linkContext.LineDash = styledLinkModel.Dash;
                    linkContext.LineColor = styledLinkModel.Color;
                    linkContext.LineAnimated = styledLinkModel.Animated;
                }

                links.Add(linkContext);
            }
        }

        foreach (var node in nodeModels)
        {
            if (node is NodeModelBase nodeModelBase)
            {
                if (nodeModelBase.RefId == Guid.Empty)
                {
                    nodeModelBase.RefId = Guid.NewGuid();
                }
                nodes.Add(nodeModelBase);
            }
        }

        return new DashboardLayout
        {
            Nodes = nodes,
            Links = links
        };
    }

    public static DashboardLayout FromJson(string json) => JsonSerializer.Deserialize<DashboardLayout>(json, jsonSerializerOptions)!;
}

public class LinkContext
{
    public required Guid SourceId { get; set; }
    public required Guid TargetId { get; set; }
    public PortAlignment SourceAlignment { get; init; }
    public PortAlignment TargetAlignment { get; init; }
    public LineShape? LineShape { get; set; }
    public int LineDash { get; set; }
    public bool LineAnimated { get; set; }
    public string? LineColor { get; set; }
    public string? SourceMarkerPath { get; set; }
    public double SourceMarkerWidth { get; set; } = 10;
    public string? TargetMarkerPath { get; set; }
    public double TargetMarkerWidth { get; set; } = 10;
}

public enum LineShape
{
    Curve,
    Orthogonal
}

Extended nodes and unified node editor

Take the ImageNode as the exsample:

@using Blazor.Diagrams.Core.Models

<div class="group relative bg-transparent">
    <img class="flex flex-col items-center justify-center border-1px @(Node.Selected ? "border-primary" : "border-transparent") object-contain select-none"
         style="width: @(Node.Width)px; height: @(Node.Height)px;" 
         ondragstart="event.preventDefault()"
         src="data:image/*;base64,@Node.ImageBase64" />

    @foreach (var port in Node.Ports)
    {
        var portCss = port.Alignment switch
        {
            PortAlignment.Top => "-top-2 left-[calc(50%-8px)]",
            PortAlignment.Right => "right-[-16px] top-[calc(50%-8px)]",
            PortAlignment.Bottom => "-bottom-2 left-[calc(50%-8px)]",
            PortAlignment.Left => "-left-4 top-[calc(50%-8px)]",
            _ => "",
        };
        <Blazor.Diagrams.Components.Renderers.PortRenderer @key="port" Port="port" Class=@($"w-[16px] h-[16px] rounded-full opacity-0 group-hover:opacity-50 absolute bg-primary {portCss}") />
    }
</div>
public partial class ImageNode
{
    [Parameter] public ImageNodeModel Node { get; set; } = null!;
}

[NodeParameter("Image Node")]
public class ImageNodeModel : NodeModelBase
{
    public ImageNodeModel()
    {
        AddPort(PortAlignment.Top);
        AddPort(PortAlignment.Right);
        AddPort(PortAlignment.Bottom);
        AddPort(PortAlignment.Left);
    }

    [NodeParameter<ImageBase64Uploader, string>("Image(<5MB)")]
    public string? ImageBase64 { get; set; }
    [NodeParameter]
    public int Width { get; set; } = 200;
    [NodeParameter]
    public int Height { get; set; } = 200;
}

Customized image uploader field: ImageBase64Uploader:

@inherits NodeParameterComponentBase<string>

@code {
    [Inject] public required IModalService ModalService { get; set; }

    private async Task OnFileUploaded(IBrowserFile file)
    {
        try
        {
            using var stream = new MemoryStream();
            using var fileStream = file.OpenReadStream(1024 * 1024 * 5);
            await fileStream.CopyToAsync(stream);

            Value = Convert.ToBase64String(stream.ToArray());
            ValueChanged?.Invoke(Value);
        }
        catch (Exception ex)
        {
            await ModalService.ShowSimpleDialog("Upload image failed", ex.Message, level: NotificationLevel.Error);
        }
    }
}

<InputFile OnChange="e => OnFileUploaded(e.File)"
           class="file-input file-input-sm file-input-bordered" />

All the editable properties are annotated with NodeParameter attribute. With this, we can use reflection to help to build the node editor:

@typeparam T

<h3 class="font-bold text-lg mb-2">@title</h3>
<div class="flex flex-col gap-2">
    @foreach (var (property, parameterAttr) in properties)
    {
        var label = parameterAttr.Name ?? property.Name;
        var value = property.GetValue(NodeModel);
        var setValue = (object? x) =>
        {
            property.SetValue(NodeModel, x);
            NodeModel.Refresh();
        };

        <NodeFieldWraper Label="@label">
            @if (parameterAttr.FieldRenderType != null)
            {
                var ps = new Dictionary<string, object?>()
                {
                    { nameof(NodeParameterComponentBase<int>.Value), value },
                    { nameof(NodeParameterComponentBase<int>.ValueChanged), setValue },
                };
                <DynamicComponent Type="parameterAttr.FieldRenderType" Parameters="ps" />
            }
            else
            {
                @if (property.PropertyType == typeof(string))
                {
                    @if (parameterAttr.Textarea)
                    {
                        <textarea class="join-item textarea textarea-bordered py-2 textarea-sm w-full" style="line-height: 1rem; white-space: nowrap; min-height: 100px;" placeholder="@label"
                                  value=@value
                                  @onchange="e => setValue(e.Value?.ToString())">
                        </textarea>
                    }
                    else
                    {
                        <input type="text" class="join-item input input-sm input-bordered w-full" placeholder="@label" 
                               value=@value 
                               @onchange="e => setValue(e.Value?.ToString())">
                    }
                }
                else if (property.PropertyType == typeof(bool) || property.PropertyType == typeof(bool?))
                {
                    <input type="checkbox" class="join-item toggle toggle-primary" checked="@((bool?)value == true)" @onchange="_ => setValue(!(bool?)value!)">
                }
                else if (property.PropertyType.IsEnum)
                {
                    <select 
                        value=@(value?.ToString() ?? "") 
                        @onchange="x => { if (Enum.TryParse(property.PropertyType, x.Value?.ToString(), out var v)) setValue(v); }" 
                        class="select select-sm select-bordered join-item w-full"
                    >
                        @foreach (var item in Enum.GetValues(property.PropertyType))
                        {
                            <option value="@item">@item</option>
                        }
                    </select>
                }
                else if (property.PropertyType == typeof(int) || property.PropertyType == typeof(int?))
                {
                    <input type="number" class="join-item input input-sm input-bordered w-full" placeholder="@label" 
                           value="@value" 
                           @onchange="e => { int.TryParse(e.Value?.ToString(), out var v); setValue(v); }" />
                }
            }
        </NodeFieldWraper>
    }
</div>
public partial class NodeEditor<T> where T : NodeModelBase
{
    [Parameter] public required T NodeModel { get; set; }

    private string title = "";
    private IEnumerable<(PropertyInfo Property, NodeParameterAttribute ParameterAttr)> properties = [];

    protected override void OnInitialized()
    {
        var modelType = NodeModel.GetType();
        var parameterAttr = modelType.GetCustomAttributes(typeof(NodeParameterAttribute), false).FirstOrDefault();

        title = ((NodeParameterAttribute?)parameterAttr)?.Name ?? modelType.Name;

        properties = modelType.GetProperties()
            .Select(p =>
            {
                var parameterAttr = p.GetCustomAttributes(typeof(NodeParameterAttribute), false).FirstOrDefault();
                return (p, (NodeParameterAttribute?)parameterAttr);
            })
            .Where(x => x.Item2 != null)
            .Select(x => (Property: x.p, ParameterAttr: x.Item2!))
            .ToList();
    }
}

Currently, it only supports to change the color, dash width and toggle animation when dash is enabled:

Blazor.Diagrams customized link

public class StyledLinkModel : LinkModel
{
    public const string DefaultColor = "grey";

    public StyledLinkModel(PortModel sourcePort, PortModel targetPort) : base(sourcePort, targetPort)
    {
        Color = DefaultColor;
    }

    public StyledLinkModel(Anchor source, Anchor target) : base(source, target)
    {
        Color = DefaultColor;
    }

    public int Dash { get; set; } = 0;
    public bool Animated { get; set; }

    public static string MakeDefaultStyleClass()
    {
        return $$"""
            g.diagram-link > path:not(.selection-helper) {
                stroke-dasharray: 0;
            }

            @keyframes diagram-link-dash-animation {
                100% {
                    stroke-dashoffset: -10;
                }
            }
            """;
    }

    public string MakeStyleClass()
    {
        var animatedCss = Animated ? "animation: diagram-link-dash-animation .5s linear infinite;" : "";
        return $$"""
            g.diagram-link[data-link-id="{{Id}}"] > path:not(.selection-helper) {
                stroke-dasharray: {{Dash}};
                {{animatedCss}}
            }
            """;
    }
}

The end

Overall, the Blazor.Diagrams project is quite easy to extend and customize. And it is naturally integrated with blazor ecosystem.

Do you like this post?