
Syncfusion Blazor Diagram
- 247 installs
- 4 repo stars
- Updated July 28, 2026
- syncfusion/blazor-ui-components-skills
Use syncfusion-blazor-diagram for development tasks
About
syncfusion-blazor-diagram: A skill for development. This provides functionality for development workflows.
- syncfusion-blazor-diagram
Syncfusion Blazor Diagram by the numbers
- 247 all-time installs (skills.sh)
- +13 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,558 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/syncfusion/blazor-ui-components-skills --skill syncfusion-blazor-diagramAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 247 |
|---|---|
| repo stars | ★ 4 |
| Last updated | July 28, 2026 |
| Repository | syncfusion/blazor-ui-components-skills ↗ |
What it does
Use syncfusion-blazor-diagram for development tasks
Files
Implementing Syncfusion Blazor Diagram
A comprehensive skill for building interactive diagrams with the Syncfusion Blazor Diagram component — flowcharts, organizational charts, mind maps, BPMN process diagrams, UML sequence diagrams, network diagrams, and more.
When to Use This Skill
Use this skill when you need to:
- Create flowcharts, org charts, mind maps, or network diagrams in Blazor
- Work with
SfDiagramComponent, nodes, connectors, or shapes - Configure automatic layouts (hierarchical, radial, mind map, org chart, flowchart)
- Implement BPMN process diagrams with BPMN shapes
- Build swimlane diagrams for process modeling
- Add symbol palettes for drag-and-drop diagram building
- Bind diagram data from a collection or remote source
- Implement diagram interactions (selection, drag, resize, zoom, pan)
- Export diagrams to PNG/JPEG/SVG or print them
- Serialize and restore diagram state (save/load)
- Enable collaborative real-time editing
- Add UML sequence diagrams
- Handle diagram events, annotations, and ports
Important: API Verification Required
API Verification Required: Always verify API class names, properties, and signatures by reading reference files (references/*.md) BEFORE generating code examples. Do not assume or infer class names. ⚠️ Before writing ANY code, review the Common Mistakes section directly below to avoid known invalid APIs and properties.
Quick Start
@using Syncfusion.Blazor.Diagram
<SfDiagramComponent Width="100%" Height="600px" Nodes="@nodes" Connectors="@connectors" />
@code {
DiagramObjectCollection<Node> nodes = new DiagramObjectCollection<Node>
{
new Node
{
ID = "node1", OffsetX = 150, OffsetY = 150,
Width = 100, Height = 50,
Style = new ShapeStyle { Fill = "#6BA5D7", StrokeColor = "white" },
Annotations = new DiagramObjectCollection<ShapeAnnotation>
{
new ShapeAnnotation { Content = "Start" }
}
}
};
DiagramObjectCollection<Connector> connectors = new DiagramObjectCollection<Connector>
{
new Connector { ID = "conn1", SourceID = "node1", TargetID = "node2" }
};
}Common Patterns
| Goal | Reference |
|---|---|
| First diagram setup | references/getting-started.md |
| Add/configure nodes | references/nodes.md |
| Add/configure connectors | references/connectors.md |
| Use built-in shapes | references/shapes.md |
| Add text labels | references/annotations.md |
| Define connection points | references/ports.md |
| Org charts / auto-layout | references/layout.md |
| Swimlane diagrams | references/swimlane.md |
| BPMN process diagrams | references/bpmn.md |
| Drag-and-drop palette | references/symbol-palette.md |
| Bind data to diagram | references/data-binding.md |
| Selection, drag, zoom | references/interaction.md |
| Handle diagram events | references/events.md |
| Save and load diagrams | references/serialization.md |
| Export / print | references/export-print.md |
| CSS / theme styling | references/styling.md |
| UML sequence diagrams | references/uml-sequence.md |
| Real-time collaboration | references/collaborative-editing.md |
| Context menu, tooltips, rulers, localization | references/advanced-features.md |
| Miniature overview / bird's-eye navigation | references/overview-component.md |
---
Documentation and Navigation Guide
Getting Started
📄 Read: references/getting-started.md
- NuGet package installation (
Syncfusion.Blazor.Diagram) - Service registration and namespace imports
- Setup for Blazor Server, WebAssembly, MAUI
- CSS/theme configuration
- Minimal working diagram example
Nodes
📄 Read: references/nodes.md
- Creating and configuring nodes
- Node types: basic, flow shape, path, image, HTML, native
- Node positioning, sizing, z-order
- Node style (fill, stroke, opacity)
- Expand/collapse children
- Node events and interaction
Connectors
📄 Read: references/connectors.md
- Creating connectors between nodes or free-floating
- Segment types: straight, orthogonal, bezier
- Multiple segments per connector
- Arrows, line style, and decoration
- Connector interaction (bend, drag endpoints)
- Connector events
Shapes
📄 Read: references/shapes.md
- Built-in basic shapes (rectangle, ellipse, triangle, etc.)
- Flow shapes (process, decision, terminator, etc.)
- Path shapes (custom SVG paths)
- Image and HTML content shapes
- Native SVG shapes
- Choosing the right shape type
Annotations
📄 Read: references/annotations.md
- Adding text labels to nodes and connectors
- Annotation positioning and alignment
- Font, color, and style customization
- Inline editing of annotations
- Multiple annotations per element
- Annotation interaction events
Ports
📄 Read: references/ports.md
- Connection ports (fixed connection points on nodes)
- Dynamic ports (created at runtime)
- Port positioning (relative and absolute)
- Port appearance and visibility
- Restricting connections to specific ports
Layout
📄 Read: references/layout.md
- Automatic layout overview and when to use each type
- Hierarchical tree layout (top-down, left-right)
- Organizational chart layout
- Mind map layout
- Radial tree layout
- Flowchart layout
- Force-directed tree layout
- Complex hierarchical layout
- Layout spacing, margin, and orientation settings
- Layout events and callbacks
await DoLayoutAsync()— refresh layout at runtime after adding/removing nodes
Swimlane
📄 Read: references/swimlane.md
- Creating swimlane diagrams
- Adding lanes and configuring lane properties
- Phase configuration (vertical/horizontal phases)
- Swimlane symbol palette integration
- Swimlane interactions
<SfDiagramComponent Height="600px" Swimlanes="@swimlanes" />
@code {
DiagramObjectCollection<Swimlane> swimlanes = new();
protected override void OnInitialized()
{
swimlanes.Add(new Swimlane
{
ID = "swimlane1",
OffsetX = 400, OffsetY = 300,
Width = 600, Height = 200,
Lanes = new DiagramObjectCollection<Lane>()
{
new Lane(){
Height = 100,
Header = new SwimlaneHeader(){
Width = 30,
Annotation = new ShapeAnnotation(){ Content = "Consumer" }
},
Children = new DiagramObjectCollection<Node>()
{
new Node(){Height = 50, Width = 50, LaneOffsetX = 250, LaneOffsetY = 30},
}
},
}
});
}
}BPMN
📄 Read: references/bpmn.md
- BPMN shape types (events, activities, gateways, data)
- BPMN event types (start, end, intermediate, boundary)
- BPMN activity types (task, subprocess, call activity)
- BPMN gateway types (exclusive, parallel, inclusive, etc.)
- BPMN connectors (sequence flow, message flow, association)
- Data objects and data stores
- Expanded sub-process
- BPMN text annotation
// Exclusive gateway (XOR)
nodes.Add(new Node
{
ID = "gateway1", OffsetX = 300, OffsetY = 200, Width = 50, Height = 50,
Shape = new BpmnGateway
{
GatewayType = BpmnGatewayType.Exclusive
}
});Symbol Palette
📄 Read: references/symbol-palette.md
- Setting up
SfSymbolPaletteComponent - Defining palette groups and symbols
- Custom symbols and stencils
- Drag-and-drop from palette to diagram
- Palette search and customization
Data Binding
📄 Read: references/data-binding.md
- Binding diagram from a flat list or IEnumerable
- Hierarchical data binding (parent-child relationships)
- Remote data source integration
- Runtime CRUD:
await ReadDataAsync(query?),await InsertDataAsync(data),await UpdateDataAsync(keyField, data),await DeleteDataAsync(keyField, value) await RefreshDataSourceAsync()— reload all data and rebuild layout- Mapping data fields to node/connector properties
Interaction & Commands
📄 Read: references/interaction.md
- Selection:
Select(collection, multipleSelection?),SelectAll(),UnSelect(obj),ClearSelection() - Drag, resize, and rotate elements (user interaction + programmatic)
- Programmatic transforms:
Drag(obj, tx, ty),Rotate(obj, angle, pivot?),Scale(obj, sx, sy, pivot) - Zoom and pan: mouse wheel, toolbar,
Zoom(factor, focusPoint),ResetZoom(),Pan(hOffset, vOffset, focusPoint?) BringIntoView(DiagramRect)— scroll viewport to show a regionBringIntoCenter(DiagramRect)— scroll viewport to center a regionFitToPage(FitOptions?)— fit content to viewport (sync;FitMode.Width/Height/Both,DiagramRegion.Content/PageSettings)Nudge(Direction, int?)— move selected elements by pixels; default 1px;Direction.Top/Bottom/Left/Right- Z-Order:
BringToFront(),BringForward(),SendBackward(),SendToBack()— mustSelect()first - Clipboard:
Copy(),Cut(),Paste(collection?),Delete(collection?) - Group/Ungroup:
Group(),Ungroup(),AddChildAsync(group, child),RemoveChild(group, child) - Inline text editing:
StartTextEdit(obj, annotationId?) - Keyboard shortcuts (built-in table) and
CommandManager(custom/override shortcuts via child component) CommandManagerusesKeyboardCommand+KeyGesture(DiagramKeys+ModifierKeys) +CommandKeyArgs- Snapping to grid or objects
- Alignment, spacing, and sizing commands (
SetAlign,SetDistribute,SetSameSize— all sync) - User handles (custom action buttons on selection)
- Undo/redo:
Undo(),Redo()(sync);StartGroupAction()/EndGroupAction()for batched undo steps - History:
AddHistoryEntry(entry),ClearHistory() - Utility:
GetObject(id),GetPageBounds(x?, y?),Clear()(removes all elements) - Batch updates:
BeginUpdate()+await EndUpdateAsync()— group multiple changes into one render pass - Add multiple elements:
await AddDiagramElementsAsync(DiagramObjectCollection<NodeBase>)
Events
📄 Read: references/events.md
- Diagram-level events (Created, Click, Drop)
- Node events (NodeCreating, PositionChanged, SizeChanged)
- Connector events (ConnectionChanged, SegmentChanged)
- Selection events (SelectionChanged)
- History change events (HistoryChanged for undo/redo)
- Event argument types and usage patterns
Serialization
📄 Read: references/serialization.md
- Saving diagram state as JSON string
- Loading a diagram from saved JSON
- Custom serialization properties
- Partial diagram save and restore patterns
Export & Print
📄 Read: references/export-print.md
- Exporting to PNG, JPEG, SVG formats
- Export region options (diagram, page, content)
- Scale and margin settings
- Print configuration
- Custom page size and orientation
- Fit diagram to single page on print
Styling
📄 Read: references/styling.md
- CSS class customization (
CssClassproperty) - Built-in themes (Material, Bootstrap, Fluent, Tailwind)
- Node and connector style properties
- Selection and hover styles
- Theme Studio customization
- CSS variable overrides
UML Sequence Diagrams
📄 Read: references/uml-sequence.md
- UML sequence diagram setup
- Lifelines and activation boxes
- Message types (synchronous, asynchronous, return, create, destroy)
- UML interaction shapes and connectors
await UpdateFromModelAsync()— refresh diagram after programmatic model changes
Collaborative Editing
📄 Read: references/collaborative-editing.md
- Setting up real-time collaborative diagram editing
- SignalR hub configuration
- Blazor Server and WASM app integration
- Handling real-time sync and conflict resolution
- Delta sync:
GetDiagramUpdates(HistoryChangedEventArgs)+await SetDiagramUpdatesAsync(updates)— efficient change propagation
Overview Component
📄 Read: references/overview-component.md
- Adding
SfDiagramOverviewComponentas a miniature thumbnail panel - Linking the overview to the main diagram via
SourceID/ID - Controlling panel size with
WidthandHeight - Zoom and pan interactions (drag, resize, click, draw-region)
- Enabling or disabling interactions with
DiagramOverviewConstraints - Required
@using Syncfusion.Blazor.Diagram.Overviewnamespace.
@using Syncfusion.Blazor.Diagram
@using Syncfusion.Blazor.Diagram.Overview
@using System.Collections.ObjectModel
<SfDiagramComponent ID="element"
Width="100%"
Height="500px">
</SfDiagramComponent>
<!-- Overview panel linked to the diagram above -->
<SfDiagramOverviewComponent Height="150px" SourceID="element" />Advanced Features
📄 Read: references/advanced-features.md
- Context menu (built-in and custom items)
- Tooltips for nodes, connectors, ports, user handles
- Programmatic tooltips:
await ShowTooltipAsync(obj)/await HideTooltipAsync(obj)— requiresOpensOn = "Custom" - Gridlines and rulers
- Scroll settings and page settings
- Container and group nodes
- Flip (horizontal/vertical)
- Constraints (restricting behavior per element)
- Localization (static text translation)
- Accessibility (WCAG 2.1, keyboard navigation)
- Migration from classic to native diagram
Common Mistakes
Annotation Editing
⚠️ `AllowEditing` does NOT exist onShapeAnnotationorPathAnnotation.
Inline editing is on by default — no property is needed to enable it.
To disable editing, set Constraints = AnnotationConstraints.ReadOnly:```csharp
// ❌ Wrong — CS0117: AllowEditing does not exist
new ShapeAnnotation { Content = "Label", AllowEditing = false }
>
// ✅ Correct — use AnnotationConstraints.ReadOnly to disable editing
new ShapeAnnotation { Content = "Label", Constraints = AnnotationConstraints.ReadOnly }
```
EndUpdateAsync Method
⚠️ Always use `EndUpdateAsync()` (async) — EndUpdate() (sync, non-async) does NOT exist and will cause a compile error.UseBeginUpdate()/EndUpdateAsync()when changing multiple properties at once —awaitis required:
```csharp
// ❌ Wrong — EndUpdate() does not exist
diagram.BeginUpdate();
// ... changes ...
diagram.EndUpdate();
>
// ✅ Correct — EndUpdateAsync is async
diagram.BeginUpdate();
// ... changes ...
await diagram.EndUpdateAsync();
```
Click Event
⚠️ `ClickEventArgs` name collision: If your page also uses @using Syncfusion.Blazor.Navigations (or Buttons),ClickEventArgs becomes ambiguous. Always qualify it:```csharp
// ✅ Use the fully qualified type in the handler signature
private void OnClick(Syncfusion.Blazor.Diagram.ClickEventArgs args) { }
```
⚠️ `args.Count` is NOT an `int` field — it is a method that returns an int.Do NOT compare it directly with == inline without storing the result first:```csharp
// ❌ Wrong — CS0019: Operator '==' cannot be applied to operands of type 'method group' and 'int'
if (args.Count == 2)
>
// ✅ Correct — store result then compare
int clickCount = args.Count;
if (clickCount == 2) { / double-click / }
```
SizeChanged Event
⚠️ `SizeChangedEventArgs.Element` is typed as `DiagramSelectionSettings`, not Node.Pattern-matchingargs.Element is Node nalways fails withCS8121.
Cast toDiagramSelectionSettingsand read.Nodes[0]to get the resized node:
```csharp
// ❌ Wrong — CS8121: DiagramSelectionSettings cannot match Node
if (args.Element is Node n) { }
>
// ✅ Correct — Element is DiagramSelectionSettings
if (args.Element is DiagramSelectionSettings sel && sel.Nodes.Count > 0)
{
var node = sel.Nodes[0];
double w = args.NewValue.Width;
double h = args.NewValue.Height;
}
```
⚠️ `args.NewValue.Width` and `args.NewValue.Height` are plain `double`, not double?.Using??on them causesCS0019. Assign them directly:
```csharp
// ❌ Wrong — CS0019
double w = args.NewValue.Width ?? 0;
>
// ✅ Correct
double w = args.NewValue.Width;
```
Selection Changed Event
⚠️ `SelectionChangedEventArgs` name collision: If your page also uses @using Syncfusion.Blazor.Buttons(or other Syncfusion packages), SelectionChangedEventArgs becomes ambiguous. Always qualify it:```csharp
// ✅ Fully qualified
private void OnSelectionChanged(Syncfusion.Blazor.Diagram.SelectionChangedEventArgs args) { }
```
⚠️ `args.NewValue` is a `DiagramSelectionSettings` object — NOT a `Node`, NOT a collection:
- Pattern-matchingargs.NewValue is Nodealways fails withCS8121
- Iterating args.NewValue as a collection fails — it is a single settings object- The only correct approach is to read_diagram.SelectionSettings.Nodes/.Connectors:
```csharp
// ❌ Wrong — CS8121: DiagramSelectionSettings cannot match Node
if (args.NewValue is Node n) { }
>
// ❌ Wrong — DiagramSelectionSettings is not IEnumerable
foreach (var item in args.NewValue) { }
>
// ✅ Correct — use SelectionSettings on the diagram reference
foreach (var node in _diagram.SelectionSettings.Nodes)
Console.WriteLine(node.ID);
foreach (var conn in _diagram.SelectionSettings.Connectors)
Console.WriteLine(conn.ID);
```
Text Changed Event
⚠️ `TextChangedEventArgs` does NOT exist — using it causes CS0246.The correct event args type is `TextChangeEventArgs` (no d):```csharp
// ❌ Wrong — CS0246: TextChangedEventArgs not found
private void OnTextChanged(TextChangedEventArgs args) { }
>
// ✅ Correct
private void OnTextChanged(TextChangeEventArgs args) { }
```
Drag Start Event
⚠️ `DragStartEventArgs` is ambiguous whenSyncfusion.Blazor.Popups(or other packages that exposeDragStartEventArgs) is also referenced.
Always qualify it as Syncfusion.Blazor.Diagram.DragStartEventArgs:```csharp
// ❌ Wrong — CS0104: ambiguous reference between Diagram and Popups
private void OnDragStart(DragStartEventArgs args) { }
>
// ✅ Correct — fully qualified
private void OnDragStart(Syncfusion.Blazor.Diagram.DragStartEventArgs args) { }
```
⚠️ `DragEnterEventArgs` does NOT exist in Syncfusion.Blazor.Diagram.There is no `DragEnter` event onSfDiagramComponentthat receives aDragEnterEventArgs.
The available drag events onSfDiagramComponentare:DragStart,Dragging,DragLeave,DragDrop— all for SymbolPalette drag-and-drop only.
For tracking when a node is being moved (internal drag), use PositionChanged:```csharp
// ❌ Wrong — DragEnterEventArgs does not exist
private void OnDragEnter(DragEnterEventArgs args) { }
>
// ❌ Wrong — OnPositionChange does not exist on SfDiagramComponent
<SfDiagramComponent OnPositionChange="OnPositionChange" />
>
// ✅ Correct — use PositionChanged
<SfDiagramComponent PositionChanged="OnPositionChanged" />
>
private void OnPositionChanged(PositionChangedEventArgs args)
{
if (args.Element is Node n)
Console.WriteLine($"Node {n.ID} moved to ({n.OffsetX}, {n.OffsetY})");
}
```
Snap Distance
⚠️ `SnapObjectDistance` does NOT exist onSnapSettings— using it causesInvalidOperationException: does not have a property matching the name 'SnapObjectDistance'.
The correct property name is `SnapDistance`:
```razor
@ ❌ Wrong — SnapObjectDistance does not exist @
<SnapSettings SnapObjectDistance="5" />
>
@ ✅ Correct @
<SnapSettings Constraints="SnapConstraints.SnapToObject" SnapDistance="5" />
```
Styling
⚠️ `CssClass` does NOT exist on SfDiagramComponent — using it causesInvalidOperationException: Object of type 'SfDiagramComponent' does not have a property matching the name 'CssClass'.Wrap the component in a <div> with a scoping class instead:```razor
@ ❌ Wrong — CssClass does not exist on SfDiagramComponent @
<SfDiagramComponent CssClass="my-diagram" />
>
@ ✅ Correct — use a wrapper div @
<div class="my-diagram">
<SfDiagramComponent ... />
</div>
```
Phase Offset Property
⚠️ `Phase.Offset` does NOT exist — using it causes a compile error.
Use `Phase.Width` to set the size of a phase in a swimlane:
```csharp
// ❌ Wrong — Offset does not exist on Phase
new Phase { ID = "ph1", Offset = 220 }
>
// ✅ Correct — use Width
new Phase { ID = "ph1", Width = 220 }
```
Lane Constraints Property
⚠️ `Lane.Constraints` does NOT exist and `LaneConstraints` enum does NOT exist.
Individual lanes cannot have constraints set via a Constraints property.To restrict swimlane-level interactions, use `SwimlaneConstraints` on the `Swimlane` object itself:
```csharp
// ❌ Wrong — Lane.Constraints and LaneConstraints do not exist
lane.Constraints = LaneConstraints.Default & ~LaneConstraints.ResizeEntries;
>
// ✅ Correct — set constraints on the Swimlane object
swimlane.Constraints = SwimlaneConstraints.Default & ~SwimlaneConstraints.Interaction;
```
FitMode.Page Value
⚠️ `FitMode.Page` does NOT exist — using it causes CS0117.The correct values for FitMode are `FitMode.Width` and `FitMode.Height`:```csharp
// ❌ Wrong — FitMode.Page does not exist
new FitOptions { Mode = FitMode.Page }
>
// ✅ Correct — use FitMode.Width or FitMode.Height
new FitOptions { Mode = FitMode.Width, Region = DiagramRegion.Content }
```
LoadDiagram Method
⚠️ `SfDiagramComponent.LoadDiagram()` does NOT exist — using it causes a compile error.
Use the async version `LoadDiagramAsync()` instead:
```csharp
// ❌ Wrong — LoadDiagram() does not exist
diagram.LoadDiagram(savedJson);
>
// ✅ Correct — use LoadDiagramAsync
await diagram.LoadDiagramAsync(savedJson);
```
FitToPageAsync Method
⚠️ `SfDiagramComponent.FitToPageAsync()` does NOT exist — using it causes a compile error.
Use the non-async overload `FitToPage()` instead:
```csharp
// ❌ Wrong — FitToPageAsync does not exist
await diagram.FitToPageAsync(new FitOptions { Mode = FitMode.Width });
>
// ✅ Correct — use FitToPage (synchronous)
diagram.FitToPage(new FitOptions { Mode = FitMode.Width, Region = DiagramRegion.Content });
```
BasicShapes Enum
⚠️ `BasicShapes` does NOT exist — use NodeBasicShapes instead:```csharp
// ❌ Wrong
new BasicShape { Shape = BasicShapes.Rectangle }
>
// ✅ Correct
new BasicShape { Shape = NodeBasicShapes.Rectangle }
```
DiagramThickness Constructor
⚠️ `DiagramThickness` does NOT have a 4-argument constructor — using it causes CS1729: does not contain a constructor that takes 4 arguments.Use the object initializer syntax with named properties instead:
```csharp
// ❌ Wrong — CS1729: no 4-argument constructor
new DiagramThickness(20, 50, 20, 20)
>
// ✅ Correct — use object initializer with named properties
new DiagramThickness { Left = 20, Top = 50, Right = 20, Bottom = 20 }
>
// ✅ Correct — set only the sides you need
new DiagramThickness { Top = 50 }
```
ScrollSettings EnableAutoScroll Property
⚠️ `CanAutoScroll` does NOT exist onScrollSettings— using it causesInvalidOperationException: does not have a property matching the name 'CanAutoScroll'.
The correct property name is `EnableAutoScroll`:
```razor
@ ❌ Wrong — CanAutoScroll does not exist @
<ScrollSettings CanAutoScroll="true" />
>
@ ✅ Correct @
<ScrollSettings EnableAutoScroll="true" />
```
Zoom, Undo, and Redo Methods
⚠️ `ZoomAsync()`, `UndoAsync()`, and `RedoAsync()` do NOT exist — using them causes a compile error.
Use the non-async overloads `Zoom()`, `Undo()`, and `Redo()` instead:
```csharp
// ❌ Wrong — ZoomAsync, UndoAsync, RedoAsync do not exist
await _diagram.ZoomAsync(1.2, new DiagramPoint { X = 300, Y = 300 });
await _diagram.UndoAsync();
await _diagram.RedoAsync();
>
// ✅ Correct — use non-async overloads
_diagram.Zoom(1.2, new DiagramPoint { X = 300, Y = 300 });
_diagram.Undo();
_diagram.Redo();
```
Overview Component Namespace
⚠️ `SfDiagramOverviewComponent` requires an additional `@using` — it lives inSyncfusion.Blazor.Diagram.Overview, NOT inSyncfusion.Blazor.Diagram. Forgetting it causesCS0246:
```razor
@ ❌ Wrong — SfDiagramOverviewComponent not found without the Overview namespace @
@using Syncfusion.Blazor.Diagram
>
@ ✅ Correct — both namespaces required @
@using Syncfusion.Blazor.Diagram
@using Syncfusion.Blazor.Diagram.Overview
```
⚠️ `SourceID` must exactly match the `ID` set on `SfDiagramComponent` — the ID is NOT auto-generated; you must set it explicitly. A mismatch (including case) renders the overview empty:```razor
@ ❌ Wrong — ID not set on the diagram; SourceID has nothing to link to @
<SfDiagramComponent Width="100%" Height="500px" Nodes="@_nodes" />
<SfDiagramOverviewComponent SourceID="myDiagram" Height="150px" />
>
@ ✅ Correct — ID set on diagram, SourceID matches exactly @
<SfDiagramComponent ID="myDiagram" Width="100%" Height="500px" Nodes="@_nodes" />
<SfDiagramOverviewComponent SourceID="myDiagram" Height="150px" />
```
⚠️ Do NOT nest `SfDiagramOverviewComponent` inside `SfDiagramComponent` — the overview is a sibling component rendered outside the diagram markup.
Advanced Features in Blazor Diagram
Table of Contents
- Groups
- Context Menu
- Tooltips
- Undo and Redo
- Constraints
- Grid Lines and Rulers
- Scroll Settings
- Page Settings
- Container (Swimlane Equivalent)
- Flip
- Localization
- Accessibility
- Common Gotchas
---
Groups
Group multiple nodes into a single unit using NodeGroup:
@code {
protected override void OnInitialized()
{
// Child nodes must be declared before the group
_nodes.Add(new Node { ID = "node1", OffsetX = 100, OffsetY = 100, Width = 100, Height = 100 });
_nodes.Add(new Node { ID = "node2", OffsetX = 250, OffsetY = 100, Width = 100, Height = 100 });
var group = new NodeGroup
{
Children = new string[] { "node1", "node2" }
};
_nodes.Add(group);
}
}Programmatic group/ungroup:
// Group selected nodes
_diagram.SelectAll();
_diagram.Group();
// Ungroup selected group
_diagram.Ungroup();Group child node constraints — to allow independent dragging of a child:
new Node
{
ID = "child1",
Constraints = NodeConstraints.Default // full constraints apply within the group
}---
Context Menu
Enable default context menu (copy, cut, paste, undo, redo, group):
<SfDiagramComponent>
<ContextMenuSettings Show="true" />
</SfDiagramComponent>Add custom items alongside defaults:
<SfDiagramComponent>
<ContextMenuSettings Show="true" ShowCustomMenuOnly="false" Items="@_items">
</ContextMenuSettings>
</SfDiagramComponent>
@code {
private List<ContextMenuItem> _items = new()
{
new ContextMenuItem
{
ID = "clone", Text = "Clone Node",
IconCss = "e-icons e-copy"
}
};
}Handle context menu clicks:
<SfDiagramComponent ContextMenuItemClicked="OnMenuClick" />
@code {
private void OnMenuClick(DiagramMenuClickEventArgs args)
{
if (args.Item.ID == "clone") { /* custom logic */ }
}
}---
Tooltips
Default interaction tooltips (drag/resize/rotate position data):
Shown automatically — no configuration needed.
Custom tooltip on hover for a node:
new Node
{
Tooltip = new DiagramTooltip { Content = "My Node Info" },
Constraints = NodeConstraints.Default | NodeConstraints.Tooltip
}Enable tooltip on the diagram globally:
<SfDiagramComponent Constraints="DiagramConstraints.Default | DiagramConstraints.Tooltip" />Programmatic tooltip control (Custom open mode)
Use ShowTooltipAsync and HideTooltipAsync to show/hide tooltips programmatically. The element's tooltip must have OpensOn = "Custom" — tooltips configured with automatic open modes are not affected.
// Node configured with custom tooltip open mode
new Node
{
ID = "node2",
OffsetX = 240, OffsetY = 100,
Tooltip = new DiagramTooltip { Content = "Custom tooltip", OpensOn = "Custom" },
Constraints = NodeConstraints.Default | NodeConstraints.Tooltip
}
// Show tooltip programmatically
await _diagram.ShowTooltipAsync(_diagram.Nodes[1] as NodeBase);
// Hide tooltip programmatically
await _diagram.HideTooltipAsync(_diagram.Nodes[1] as NodeBase);Note:ShowTooltipAsyncandHideTooltipAsynconly work for elements withOpensOn = "Custom". Elements withOpensOn = "Auto"orOpensOn = "Hover"are controlled by the browser and are not affected.
---
Undo and Redo
// Keyboard: Ctrl+Z / Ctrl+Y (built-in)
// Programmatic:
_diagram.Undo();
_diagram.Redo();
// Check availability:
bool canUndo = _diagram.HistoryManager.CanUndo;
bool canRedo = _diagram.HistoryManager.CanRedo;Group multiple changes into one undo step:
_diagram.StartGroupAction();
// ... make multiple changes ...
_diagram.EndGroupAction();
// Now Ctrl+Z undoes all changes at once---
Constraints
Constraints use bitwise flags (| to add, & ~ to remove).
Diagram-level constraints:
<!-- Disable page editing but keep everything else -->
<SfDiagramComponent Constraints="DiagramConstraints.Default & ~DiagramConstraints.PageEditable" />
<!-- Enable auto-routing -->
<SfDiagramComponent Constraints="DiagramConstraints.Default | DiagramConstraints.Routing" />Key `DiagramConstraints` flags:
| Flag | Effect |
|---|---|
PageEditable | Enable/disable editing the page |
Zoom | Enable/disable zoom |
Pan | Enable/disable pan |
UndoRedo | Enable/disable undo/redo |
UserInteraction | Enable/disable all user interaction |
Tooltip | Enable/disable element tooltips |
Bridging | Enable/disable connector bridging |
Routing | Enable automatic connector routing |
Node-level constraints:
new Node
{
// Remove drag and resize but keep selection
Constraints = NodeConstraints.Default & ~NodeConstraints.Drag & ~NodeConstraints.Resize
}Connector-level constraints:
new Connector
{
// Prevent editing the connector path
Constraints = ConnectorConstraints.Default & ~ConnectorConstraints.DragSegmentThumb
}---
Grid Lines and Rulers
Show grid and snap to it:
<SfDiagramComponent>
<SnapSettings Constraints="SnapConstraints.ShowLines | SnapConstraints.SnapToLines">
<HorizontalGridLines LineColor="#e0e0e0" LineDashArray="2,2"
LineIntervals="@_intervals" />
<VerticalGridLines LineColor="#e0e0e0" LineDashArray="2,2"
LineIntervals="@_intervals" />
</SnapSettings>
</SfDiagramComponent>
@code {
double[] _intervals = { 1, 9, 0.25, 9.75, 0.25, 9.75, 0.25, 9.75, 0.25, 9.75 };
}Show rulers:
<SfDiagramComponent>
<RulerSettings ShowRulers="true" />
</SfDiagramComponent>---
Scroll Settings
<SfDiagramComponent>
<ScrollSettings EnableAutoScroll="true"
AutoScrollPadding="@_padding"
@bind-ScrollLimit="@_scrollLimit">
</ScrollSettings>
</SfDiagramComponent>
@code {
ScrollLimitMode _scrollLimit = ScrollLimitMode.Diagram;
DiagramMargin _padding = new DiagramMargin { Left = 30, Right = 30, Top = 30, Bottom = 30 };
}`ScrollLimitMode` options:
| Value | Description |
|---|---|
Infinity | Unlimited scroll |
Diagram | Limited to diagram bounds |
Limited | Limited to ScrollableArea bounds |
---
Page Settings
Configure the virtual page area inside the diagram canvas:
<SfDiagramComponent>
<PageSettings Width="816" Height="1054"
MultiplePage="true"
Orientation="PageOrientation.Portrait"
ShowPageBreaks="true">
<PageMargin Left="10" Right="10" Top="10" Bottom="10" />
</PageSettings>
</SfDiagramComponent>---
Container
A Container is a boundary node that visually groups child nodes without the group behavior — children move freely within it:
<SfDiagramComponent @ref="@_diagram" Height="600px" Nodes="@_nodes">
</SfDiagramComponent>
@code
{
private SfDiagramComponent _diagram;
//Initialize the node collection
private DiagramObjectCollection<Node> _nodes = new DiagramObjectCollection<Node>();
protected override void OnInitialized()
{
Node node1 = new Node()
{
ID = "node1",
Height = 60,
Width = 100,
OffsetX = 400,
OffsetY = 300,
Annotations = new DiagramObjectCollection<ShapeAnnotation>()
{
new ShapeAnnotation(){ Content = "Process"}
}
};
Node node2 = new Node()
{
ID = "node2",
Height = 60,
Width = 100,
OffsetX = 600,
OffsetY = 300,
Annotations = new DiagramObjectCollection<ShapeAnnotation>()
{
new ShapeAnnotation(){ Content = "Process"}
}
};
Container container = new Container()
{
ID = "container",
Height = 300, Width = 500, OffsetX = 500, OffsetY = 300,
Children = new string[] { "node1", "node2" }
};
_nodes.Add(node1);
_nodes.Add(node2);
_nodes.Add(container);
}
}---
Flip
Flip a node horizontally or vertically:
new Node
{
Flip = FlipDirection.Horizontal // or Vertical, Both, None
}
// Programmatic flip:
node.Flip = FlipDirection.Vertical;---
Localization
Override default UI text (e.g., context menu labels):
// In Program.cs:
builder.Services.AddSyncfusionBlazor();
// Provide custom locale JSON for the diagram component
// See: Syncfusion globalization documentation---
Accessibility
The diagram supports ARIA attributes and keyboard navigation:
- Tab — move focus between diagram elements
- Enter — select focused element
- Arrow keys — move selected element
- Escape — deselect / cancel editing
- ARIA roles are applied to nodes and connectors automatically
---
Common Gotchas
- Group children must be added to `Nodes` before the `NodeGroup` — the group references children by ID; adding the group first causes a null-reference during render
- `_diagram.Group()` requires selected nodes — call
SelectAll()or programmatically select nodes first - `DiagramConstraints.Routing` enables auto-routing for all connectors; individual connectors can still override with
ConnectorConstraints - `ScrollLimitMode.Infinity` can cause performance issues with very large diagrams — use
DiagramorLimitedfor bounded scroll - `StartGroupAction` / `EndGroupAction` must always be paired — an unclosed group action causes all subsequent changes to be grouped indefinitely
- Tooltips require `NodeConstraints.Tooltip` on the individual node in addition to
DiagramConstraints.Tooltipat the diagram level
Annotations in Blazor Diagram
Table of Contents
- Overview
- Node Annotations
- Connector Annotations
- Annotation Position and Alignment
- Annotation Style
- Multiple Annotations
- Add / Remove / Update at Runtime
- Inline Editing
- Common Gotchas
---
Overview
Annotations are text labels attached to nodes or connectors. They support positioning, alignment, font styling, and runtime editing. Each node/connector can have multiple annotations.
- Node annotations: Use
ShapeAnnotation - Connector annotations: Use
PathAnnotation
---
Node Annotations
@using Syncfusion.Blazor.Diagram
<SfDiagramComponent Height="600px" Nodes="@nodes" />
@code {
DiagramObjectCollection<Node> nodes = new();
protected override void OnInitialized()
{
nodes.Add(new Node
{
ID = "node1",
OffsetX = 200, OffsetY = 200,
Width = 120, Height = 60,
Style = new ShapeStyle { Fill = "#6495ED", StrokeColor = "white" },
Annotations = new DiagramObjectCollection<ShapeAnnotation>
{
new ShapeAnnotation { Content = "Process Step" }
}
});
}
}---
Connector Annotations
connectors.Add(new Connector
{
ID = "conn1",
SourceID = "node1", TargetID = "node2",
Type = ConnectorSegmentType.Orthogonal,
Annotations = new DiagramObjectCollection<PathAnnotation>
{
new PathAnnotation
{
Content = "Yes",
Offset = 0.5 // position along the path (0=source, 1=target, 0.5=middle)
}
}
});---
Annotation Position and Alignment
Node annotation positioning via `Offset` (fraction of node width/height):
| Offset | Position |
|---|---|
(0.5, 0.5) | Center (default) |
(0.5, 0) | Top center |
(0.5, 1) | Bottom center |
(0, 0.5) | Left center |
(1, 0.5) | Right center |
new ShapeAnnotation
{
Content = "Label",
Offset = new DiagramPoint { X = 0.5, Y = 0 }, // top center
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Bottom,
Margin = new DiagramThickness { Top = 5 }
}Connector annotation positioning via `Offset` (0 to 1 along the path):
new PathAnnotation
{
Content = "Flow Label",
Offset = 0.5, // middle of connector
Alignment = AnnotationAlignment.Center
}---
Annotation Style
new ShapeAnnotation
{
Content = "Styled Label",
Style = new TextStyle
{
FontSize = 14,
Bold = true,
Italic = false,
Color = "#333333",
TextDecoration = TextDecoration.Underline,
TextAlign = TextAlign.Center
}
}---
Multiple Annotations
A node can have many annotations at different positions:
Annotations = new DiagramObjectCollection<ShapeAnnotation>
{
new ShapeAnnotation
{
Content = "Title",
Offset = new DiagramPoint { X = 0.5, Y = 0 },
VerticalAlignment = VerticalAlignment.Bottom
},
new ShapeAnnotation
{
Content = "Subtitle",
Offset = new DiagramPoint { X = 0.5, Y = 1 },
VerticalAlignment = VerticalAlignment.Top,
Style = new TextStyle { FontSize = 10, Color = "#888" }
}
}---
Add / Remove / Update at Runtime
Add annotation at runtime:
_diagram.Nodes[0].Annotations.Add(new ShapeAnnotation
{
Content = "New Label",
Offset = new DiagramPoint { X = 0.5, Y = 0.5 }
});Remove annotation:
_diagram.Nodes[0].Annotations.RemoveAt(0);Update annotation text:
_diagram.BeginUpdate();
_diagram.Nodes[0].Annotations[0].Content = "Updated Text";
await _diagram.EndUpdateAsync();---
Inline Editing
Users can double-click an annotation to edit it inline (enabled by default).
Disable editing for a specific annotation:
new ShapeAnnotation
{
Content = "Read Only",
Constraints = AnnotationConstraints.ReadOnly
}⚠️ `AllowEditing` does NOT exist onShapeAnnotationorPathAnnotation.
Inline editing is on by default — no property is needed to enable it.
To disable editing, set Constraints = AnnotationConstraints.ReadOnly.Handle annotation edit events:
<SfDiagramComponent TextChanged="OnTextChanged" />
@code {
private void OnTextChanged(TextChangeEventArgs args)
{
// args.OldValue — text before editing
// args.NewValue — text after editing
Console.WriteLine($"Changed from '{args.OldValue}' to '{args.NewValue}'");
}
}---
Common Gotchas
- Node uses `ShapeAnnotation`, connector uses `PathAnnotation` — using the wrong type causes no label to show
- Annotation ID must be unique and must not start with a number or contain underscores/spaces
- Default position is center (
Offset = (0.5, 0.5)for nodes,Offset = 0.5for connectors) - `HorizontalAlignment` and `VerticalAlignment` control which side of the offset point the text is anchored to
- `PathAnnotation.Offset` is a
double(0–1), not aDiagramPointlikeShapeAnnotation.Offset - Inline editing is enabled by default — use
AnnotationConstraints.ReadOnlyto prevent it - `AllowEditing` does NOT exist on
ShapeAnnotationorPathAnnotation— this property will cause a compile error. Editing is on by default; useAnnotationConstraints.ReadOnlyto opt out
BPMN Shapes in Blazor Diagram
Table of Contents
- Overview
- BPMN Events
- BPMN Activities (Tasks and Subprocesses)
- BPMN Gateways
- BPMN Data Objects and Data Stores
- BPMN Connectors
- BPMN Text Annotation
- Expanded Sub-Process
- Common Gotchas
---
Overview
BPMN (Business Process Model and Notation) shapes model business processes visually. Use BpmnActivity, BpmnEvent, BpmnGateway, etc. as a node's Shape property.
---
BPMN Events
Events represent things that happen during a process (start, end, intermediate):
@using Syncfusion.Blazor.Diagram
<SfDiagramComponent Height="600px" Nodes="@nodes" />
@code {
DiagramObjectCollection<Node> nodes = new();
protected override void OnInitialized()
{
// Start event
nodes.Add(new Node
{
ID = "start", OffsetX = 100, OffsetY = 200, Width = 50, Height = 50,
Shape = new BpmnEvent
{
EventType = BpmnEventType.Start,
Trigger = BpmnEventTrigger.None
}
});
// End event
nodes.Add(new Node
{
ID = "end", OffsetX = 500, OffsetY = 200, Width = 50, Height = 50,
Shape = new BpmnEvent
{
EventType = BpmnEventType.End,
Trigger = BpmnEventTrigger.None
}
});
// Intermediate event with message trigger
nodes.Add(new Node
{
ID = "intermediate", OffsetX = 300, OffsetY = 200, Width = 50, Height = 50,
Shape = new BpmnEvent
{
EventType = BpmnEventType.Intermediate,
Trigger = BpmnEventTrigger.Message
}
});
}
}`BpmnEventType` values: Start, End, Intermediate, NonInterruptingStart, NonInterruptingIntermediate, ThrowingIntermediate
`BpmnEventTrigger` values: None, Message, Timer, Escalation, Conditional, Error, Cancel, Compensation, Signal, Multiple, Terminate, Parallel
---
BPMN Activities (Tasks and Subprocesses)
Activities describe work being done in a process:
// Task
nodes.Add(new Node
{
ID = "task1", OffsetX = 200, OffsetY = 200, Width = 120, Height = 60,
Shape = new BpmnActivity
{
ActivityType = BpmnActivityType.Task,
TaskType = BpmnTaskType.Service // or User, Send, Receive, Script, Manual, etc.
},
Annotations = new() { new ShapeAnnotation { Content = "Process Order" } }
});
// Subprocess (collapsed)
nodes.Add(new Node
{
ID = "sub1", OffsetX = 400, OffsetY = 200, Width = 120, Height = 60,
Shape = new BpmnActivity
{
ActivityType = BpmnActivityType.SubProcess,
Loop = BpmnLoopCharacteristic.None,
IsCompensation = false,
IsAdhoc = false,
IsCall = false
}
});`BpmnActivityType`: Task, SubProcess
`BpmnTaskType`: None, User, Send, Receive, Service, Script, Manual, BusinessRule
---
BPMN Gateways
Gateways control the flow of a process (branching/merging):
// Exclusive gateway (XOR)
nodes.Add(new Node
{
ID = "gateway1", OffsetX = 300, OffsetY = 200, Width = 50, Height = 50,
Shape = new BpmnGateway
{
GatewayType = BpmnGatewayType.Exclusive
}
});
// Parallel gateway (AND)
nodes.Add(new Node
{
ID = "gateway2", OffsetX = 450, OffsetY = 200, Width = 50, Height = 50,
Shape = new BpmnGateway { GatewayType = BpmnGatewayType.Parallel }
});`BpmnGatewayType` values: None, Exclusive, Inclusive, Parallel, Complex, EventBased, ParallelEventBased, ExclusiveEventBased
---
BPMN Data Objects and Data Stores
// Data Object
nodes.Add(new Node
{
ID = "data1", OffsetX = 200, OffsetY = 350, Width = 50, Height = 60,
Shape = new BpmnDataObject
{
DataObjectType = BpmnDataObjectType.Input // Input, Output, None
}
});
// Data Store
nodes.Add(new Node
{
ID = "store1", OffsetX = 350, OffsetY = 350, Width = 60, Height = 50,
Shape = new BpmnDataStore()
});---
BPMN Connectors
Use BpmnConnectorType on the connector's shape for BPMN-specific flow lines:
connectors.Add(new Connector
{
ID = "seq1",
SourceID = "start", TargetID = "task1",
Shape = new BpmnFlow
{
Flow = BpmnFlowType.SequenceFlow
}
});
// Message flow
connectors.Add(new Connector
{
ID = "msg1",
SourceID = "task1", TargetID = "task2",
Shape = new BpmnFlow
{
Flow = BpmnFlowType.MessageFlow
}
});`BpmnFlowType` values: SequenceFlow, DefaultSequenceFlow, ConditionalSequenceFlow, AssociationFlow, DirectionalAssociationFlow, BiDirectionalAssociationFlow, MessageFlow, InitiatingMessageFlow, NonInitiatingMessageFlow
---
BPMN Text Annotation
Text annotations attach explanatory notes to BPMN shapes:
nodes.Add(new Node
{
ID = "annotation1", OffsetX = 300, OffsetY = 100, Width = 100, Height = 50,
Shape = new BpmnTextAnnotation
{
TextAnnotationDirection = TextAnnotationDirection.Auto,
TextAnnotationTarget = "task1" // ID of the target shape
},
Annotations = new() { new ShapeAnnotation { Content = "Must complete in 2hrs" } }
});---
Expanded Sub-Process
An expanded sub-process is a container for child nodes and connectors:
// Add child nodes inside
nodes.Add(new Node
{
ID = "child1", OffsetX = 230, OffsetY = 280, Width = 80, Height = 40,
ParentID = "subprocess1",
Shape = new BpmnActivity { ActivityType = BpmnActivityType.Task },
Annotations = new() { new ShapeAnnotation { Content = "Step 1" } }
});
nodes.Add(new Node
{
ID = "subprocess1", OffsetX = 300, OffsetY = 300, Width = 300, Height = 200,
Shape = new BpmnExpandedSubProcess(){
Children = new DiagramObjectCollection<string>() { "child1" }
}
});
---
Common Gotchas
- Use `BpmnFlow` (not standard
Connector) for BPMN-typed connectors — otherwise the line style won't match the BPMN notation - Event + Trigger combination controls appearance — e.g.,
Start+Message= envelope icon inside circle - `BpmnTextAnnotation.TextAnnotationTarget` must be the ID of an existing node
- Expanded sub-process children use
ParentIDto nest inside the container - BPMN shapes are nodes — they go in the
Nodescollection like any other node - Gateway size is typically 50x50 — gateways look best as squares (equal width/height)
Collaborative Editing in Blazor Diagram
Collaborative editing enables multiple users to edit the same diagram simultaneously in real time.
---
Architecture
- SignalR handles real-time communication between the browser and server
- Redis (optional) acts as a shared temporary store for multi-server deployments
- Changes that trigger
HistoryChangedare propagated to all connected clients - `GetDiagramUpdates(HistoryChangedEventArgs)` — serializes the diagram change from
HistoryChangedEventArgsinto aList<string>for transmission - `SetDiagramUpdatesAsync(List<string>)` — applies received serialized changes from other clients to the local diagram
---
Setup Overview
Collaborative editing requires two parts:
1. ASP.NET Core SignalR Hub — receives and broadcasts diagram changes 2. Blazor App — connects to the hub and syncs the diagram component
---
Step 1: Create the SignalR Hub (Server Project)
// DiagramHub.cs
using Microsoft.AspNetCore.SignalR;
public class DiagramHub : Hub
{
private readonly IDiagramStateService _stateService;
public DiagramHub(IDiagramStateService stateService)
{
_stateService = stateService;
}
public async Task BroadcastDiagramAction(string action, string data)
{
// Broadcast to all OTHER connected clients (not the sender)
await Clients.Others.SendAsync("ReceiveDiagramAction", action, data);
// Store state (e.g., in Redis or in-memory)
await _stateService.SaveAsync(action, data);
}
public async Task<string> GetCurrentState()
{
return await _stateService.GetAsync();
}
}Register in `Program.cs`:
builder.Services.AddSignalR();
// ...
app.MapHub<DiagramHub>("/diagramhub");---
Step 2: Connect the Blazor App to the Hub
@using Microsoft.AspNetCore.SignalR.Client
@using Syncfusion.Blazor.Diagram
@implements IAsyncDisposable
<SfDiagramComponent @ref="_diagram"
@bind-Nodes="_nodes"
@bind-Connectors="_connectors"
HistoryChanged="OnHistoryChanged" />
@code {
private SfDiagramComponent _diagram;
private DiagramObjectCollection<Node> _nodes = new();
private DiagramObjectCollection<Connector> _connectors = new();
private HubConnection _hubConnection;
protected override async Task OnInitializedAsync()
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/diagramhub"))
.Build();
// Receive changes from other clients
_hubConnection.On<string, string>("ReceiveDiagramAction", async (action, data) =>
{
await _diagram.LoadDiagramAsync(data);
await InvokeAsync(StateHasChanged);
});
await _hubConnection.StartAsync();
// Load current diagram state on join
var state = await _hubConnection.InvokeAsync<string>("GetCurrentState");
if (!string.IsNullOrEmpty(state))
await _diagram.LoadDiagramAsync(state);
}
private async void OnHistoryChanged(HistoryChangedEventArgs args)
{
// Broadcast current state after each change
string currentState = _diagram.SaveDiagram();
await _hubConnection.SendAsync("BroadcastDiagramAction", args.Action.ToString(), currentState);
}
public async ValueTask DisposeAsync()
{
await _hubConnection.DisposeAsync();
}
}---
Optimized Sync with GetDiagramUpdates / SetDiagramUpdatesAsync
Instead of sending the full SaveDiagram() JSON on every change, use the optimized delta-based API:
- `GetDiagramUpdates(HistoryChangedEventArgs)` — serializes only the change described by the history event into a compact
List<string> - `SetDiagramUpdatesAsync(List<string>)` — applies a received delta update to the local diagram without a full reload
This approach is more efficient for real-time collaboration because it transmits only what changed, not the entire diagram state.
<SfDiagramComponent @ref="_diagram"
@bind-Nodes="_nodes"
@bind-Connectors="_connectors"
HistoryChanged="OnHistoryChanged" />
@code {
private SfDiagramComponent _diagram;
private HubConnection _hubConnection;
protected override async Task OnInitializedAsync()
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/diagramhub"))
.Build();
// Receive delta updates from other clients and apply them
_hubConnection.On<List<string>>("ReceiveDiagramUpdates", async (updates) =>
{
await _diagram.SetDiagramUpdatesAsync(updates);
await InvokeAsync(StateHasChanged);
});
await _hubConnection.StartAsync();
}
private async void OnHistoryChanged(HistoryChangedEventArgs args)
{
// Serialize only the delta change (not the whole diagram)
List<string> updates = _diagram.GetDiagramUpdates(args);
// Broadcast the compact delta to other clients
await _hubConnection.SendAsync("BroadcastDiagramUpdates", updates);
}
}Hub method for delta broadcast:
public async Task BroadcastDiagramUpdates(List<string> updates)
{
// Forward delta to all other connected clients
await Clients.Others.SendAsync("ReceiveDiagramUpdates", updates);
}Tip: Use the delta-basedGetDiagramUpdates/SetDiagramUpdatesAsyncpattern for production collaboration. The fullSaveDiagram/LoadDiagramAsyncapproach is simpler but sends much larger payloads, causing higher latency for complex diagrams.
---
Multi-Server (Scale-Out) Setup
For deployments with multiple server instances, configure a SignalR backplane so all nodes share messages:
// In Program.cs — add Redis backplane
builder.Services.AddSignalR()
.AddStackExchangeRedis("localhost:6379");Store shared diagram state in Redis instead of in-memory:
// Use IConnectionMultiplexer to store/retrieve diagram JSON in Redis---
Limitations
The following settings are NOT synchronized across clients — they are local only:
| Unsynchronized Setting |
|---|
PageSettings |
ContextMenu |
DiagramHistoryManager |
SnapSettings |
Rulers |
UmlSequenceDiagram model |
Layout |
ScrollSettings (zoom/pan) |
Zoom and pan are per-user and not broadcast.
---
Common Gotchas
- Only `HistoryChanged`-tracked actions are propagated — programmatic changes that bypass undo/redo history are not broadcast
- Two-way binding required — use
@bind-Nodesand@bind-ConnectorssoLoadDiagramAsynccorrectly updates the UI - Race conditions — if two users edit simultaneously, the last broadcast wins; implement optimistic locking or versioning in the hub for conflict resolution
- Single server works without Redis — add Redis only for load-balanced multi-instance deployments
- Complete working sample available at: https://github.com/syncfusion/blazor-showcase-diagram-collaborative-editing
Connectors in Blazor Diagram
Table of Contents
- Overview
- Creating Connectors
- Connector Properties
- Segment Types
- Decorators (Arrows)
- Connector Style
- Add / Remove / Update at Runtime
- Common Gotchas
---
Overview
Connectors create links between nodes (or free-floating points) to show relationships and flows. They support multiple segment types, custom arrows, labels, and interactions.
---
Creating Connectors
Connect two nodes by ID:
@using Syncfusion.Blazor.Diagram
<SfDiagramComponent Height="600px" Nodes="@nodes" Connectors="@connectors" />
@code {
DiagramObjectCollection<Node> nodes = new();
DiagramObjectCollection<Connector> connectors = new();
protected override void OnInitialized()
{
nodes.Add(new Node { ID = "node1", OffsetX = 100, OffsetY = 150, Width = 100, Height = 50 });
nodes.Add(new Node { ID = "node2", OffsetX = 300, OffsetY = 150, Width = 100, Height = 50 });
connectors.Add(new Connector
{
ID = "conn1",
SourceID = "node1",
TargetID = "node2",
Type = ConnectorSegmentType.Orthogonal
});
}
}Free-floating connector (point to point):
connectors.Add(new Connector
{
ID = "conn1",
SourcePoint = new DiagramPoint { X = 100, Y = 100 },
TargetPoint = new DiagramPoint { X = 300, Y = 200 },
Type = ConnectorSegmentType.Straight
});⚠️ Connector ID rules: Must not start with a number or contain underscores/spaces.
---
Connector Properties
| Property | Type | Description |
|---|---|---|
ID | string | Unique identifier (required) |
SourceID | string | ID of source node |
TargetID | string | ID of target node |
SourcePoint | DiagramPoint | Source coordinate (when not connecting to a node) |
TargetPoint | DiagramPoint | Target coordinate |
SourcePortID | string | Connect to a specific port on the source node |
TargetPortID | string | Connect to a specific port on the target node |
Type | ConnectorSegmentType | Straight, Orthogonal, Bezier |
Segments | DiagramObjectCollection | Custom segments |
Style | ShapeStyle | Line color, width, dash |
SourceDecorator | DecoratorSettings | Arrow/shape at source end |
TargetDecorator | DecoratorSettings | Arrow/shape at target end |
Annotations | DiagramObjectCollection\<PathAnnotation\> | Text labels on the connector |
Constraints | ConnectorConstraints | Enable/disable behaviors |
---
Segment Types
| Type | When to Use |
|---|---|
ConnectorSegmentType.Straight | Direct line between points |
ConnectorSegmentType.Orthogonal | Right-angle paths (default for flowcharts) |
ConnectorSegmentType.Bezier | Smooth curves |
Orthogonal connector (auto-routes around nodes):
new Connector
{
ID = "conn1",
SourceID = "node1", TargetID = "node2",
Type = ConnectorSegmentType.Orthogonal
}Bezier connector:
new Connector
{
ID = "conn1",
SourceID = "node1", TargetID = "node2",
Type = ConnectorSegmentType.Bezier
}Multiple segments (bend path):
new Connector
{
ID = "conn1",
SourceID = "node1", TargetID = "node2",
Type = ConnectorSegmentType.Orthogonal,
Segments = new DiagramObjectCollection<ConnectorSegment>
{
new OrthogonalSegment { Direction = Direction.Right, Length = 70 },
new OrthogonalSegment { Direction = Direction.Bottom, Length = 50 }
}
}---
Decorators (Arrows)
Control the arrowhead shapes at both ends:
new Connector
{
ID = "conn1",
SourceID = "node1", TargetID = "node2",
SourceDecorator = new DecoratorSettings
{
Shape = DecoratorShape.Circle,
Style = new ShapeStyle { Fill = "#37909A", StrokeColor = "#37909A" }
},
TargetDecorator = new DecoratorSettings
{
Shape = DecoratorShape.Arrow,
Style = new ShapeStyle { Fill = "#6f409f", StrokeColor = "#6f409f" }
}
}Remove arrowhead:
TargetDecorator = new DecoratorSettings { Shape = DecoratorShape.None }Custom path decorator:
TargetDecorator = new DecoratorSettings
{
Shape = DecoratorShape.Custom,
PathData = "M 0,0 L 10,5 L 0,10 Z" // SVG path
}Available DecoratorShape values: Arrow, OpenArrow, Circle, Square, Diamond, Custom, None
---
Connector Style
new Connector
{
Style = new ShapeStyle
{
StrokeColor = "#6f409f",
StrokeWidth = 2,
StrokeDashArray = "5,3", // dashed line
Opacity = 0.8
}
}---
Add / Remove / Update at Runtime
Add at runtime:
connectors.Add(new Connector
{
ID = "newConn",
SourceID = "node1",
TargetID = "node3",
Type = ConnectorSegmentType.Orthogonal
});Add with annotation at runtime:
var newConn = new Connector
{
ID = "conn2",
SourcePoint = new DiagramPoint { X = 100, Y = 100 },
TargetPoint = new DiagramPoint { X = 300, Y = 200 },
Annotations = new DiagramObjectCollection<PathAnnotation>
{
new PathAnnotation { Content = "Flow" }
}
};
await _diagram.AddDiagramElementsAsync(new DiagramObjectCollection<NodeBase> { newConn });Remove at runtime:
connectors.Remove(connectors[0]);Update style at runtime:
_diagram.BeginUpdate();
_diagram.Connectors[0].Style.StrokeColor = "#FF0000";
await _diagram.EndUpdateAsync();⚠️ Always use `EndUpdateAsync()` (async) — EndUpdate() (sync, non-async) does NOT exist and will cause a compile error.---
Common Gotchas
- `SourceID`/`TargetID` must match existing node IDs exactly — otherwise the connector will be free-floating
- Use `SourcePortID`/`TargetPortID` to connect to specific ports instead of the nearest edge
- Orthogonal segments automatically route around nodes; straight segments do not
- `PathAnnotation` (not
ShapeAnnotation) is used for connector labels - `ConnectorSegmentType.Bezier` ignores manual segment definitions — control points are auto-calculated
- Use `BeginUpdate()`/`EndUpdateAsync()` when changing multiple connector properties at once — note
EndUpdateAsync()is async (awaitrequired);EndUpdate()does not exist
Data Binding in Blazor Diagram
Table of Contents
- Overview
- Local Data Binding
- Hierarchical Data Binding
- Accessing Custom Data in NodeCreating
- Remote Data Binding
- Runtime Data CRUD Operations
- Common Gotchas
---
Overview
Instead of manually defining every node and connector, the diagram can auto-generate them from a data source. Define a model class with ID and ParentID fields, pass it to DataSourceSettings, and let the layout engine handle placement.
When to use data binding:
- Loading org chart data from a database
- Rendering hierarchical trees from API data
- Building diagrams from any collection of objects with parent-child relationships
---
Local Data Binding
Bind a flat list to automatically generate a hierarchical diagram:
@using Syncfusion.Blazor.Diagram
<SfDiagramComponent Height="600px"
NodeCreating="OnNodeCreating"
ConnectorCreating="OnConnectorCreating">
<DataSourceSettings ID="Id" ParentID="ParentId" DataSource="@data" />
<Layout Type="LayoutType.HierarchicalTree"
HorizontalSpacing="40" VerticalSpacing="40" />
</SfDiagramComponent>
@code {
public class OrgData
{
public string Id { get; set; }
public string Name { get; set; }
public string Role { get; set; }
public string ParentId { get; set; } // empty = root
}
List<OrgData> data = new()
{
new OrgData { Id = "1", Name = "CEO", Role = "Director", ParentId = "" },
new OrgData { Id = "2", Name = "CTO", Role = "Manager", ParentId = "1" },
new OrgData { Id = "3", Name = "CFO", Role = "Manager", ParentId = "1" },
new OrgData { Id = "4", Name = "Dev Lead", Role = "Lead", ParentId = "2" },
new OrgData { Id = "5", Name = "Designer", Role = "Creative", ParentId = "3" },
};
private void OnNodeCreating(IDiagramObject obj)
{
var node = obj as Node;
node.Width = 120; node.Height = 50;
node.Style = new ShapeStyle { Fill = "#6BA5D7", StrokeColor = "white" };
}
private void OnConnectorCreating(IDiagramObject obj)
{
(obj as Connector).Type = ConnectorSegmentType.Orthogonal;
}
}Key `DataSourceSettings` properties:
| Property | Description |
|---|---|
ID | Name of the unique identifier field in your data class |
ParentID | Name of the parent reference field |
DataSource | The collection of data objects |
Root | Optional: specify the ID of the root node (when multiple root candidates exist) |
---
Hierarchical Data Binding
For multi-root or complex hierarchies:
<DataSourceSettings ID="Id" ParentID="ParentId" DataSource="@data" Root="root1" />Set Root to the ID of the node you want as the top-level root when the layout has multiple nodes with empty ParentId.
---
Accessing Custom Data in NodeCreating
The NodeCreating callback receives the raw data object — cast it to access your model's fields:
private void OnNodeCreating(IDiagramObject obj)
{
var node = obj as Node;
node.Width = 120;
node.Height = 50;
// Access your data model
if (node.Data is OrgData data)
{
// Apply styling based on data
node.Style = new ShapeStyle
{
Fill = data.Role == "Director" ? "#E74C3C" :
data.Role == "Manager" ? "#3498DB" : "#2ECC71",
StrokeColor = "white"
};
// The annotation content is usually auto-set from data,
// but you can customize it:
if (node.Annotations.Count > 0)
node.Annotations[0].Content = $"{data.Name}\n({data.Role})";
}
}---
Remote Data Binding
Fetch data from an API and bind after loading:
<SfDiagramComponent @ref="_diagram" Height="600px"
NodeCreating="OnNodeCreating"
ConnectorCreating="OnConnectorCreating">
<DataSourceSettings ID="Id" ParentID="ParentId" DataSource="@data" />
<Layout Type="LayoutType.OrganizationalChart"
HorizontalSpacing="40" VerticalSpacing="40" />
</SfDiagramComponent>
@code {
List<OrgData> data = new();
protected override async Task OnInitializedAsync()
{
// Fetch from API
data = await Http.GetFromJsonAsync<List<OrgData>>("api/org-chart");
}
private void OnNodeCreating(IDiagramObject obj)
{
var node = obj as Node;
node.Width = 120; node.Height = 50;
node.Style = new ShapeStyle { Fill = "#6BA5D7", StrokeColor = "white" };
}
private void OnConnectorCreating(IDiagramObject obj)
{
(obj as Connector).Type = ConnectorSegmentType.Orthogonal;
}
}---
Runtime Data CRUD Operations
When using a remote data source (SfDataManager), use these async methods to perform CRUD operations on the bound data source. The diagram automatically refreshes its layout after each operation.
Setup — Remote Data with WebAPI
<SfDiagramComponent ID="diagram" @ref="@_diagram" Width="100%" Height="690px"
ConnectorCreating="@ConnectorCreating"
NodeCreating="@NodeCreating">
<DataSourceSettings ID="EmployeeID" ParentID="ReportsTo">
<SfDataManager Url="api/Data" Adaptor="Syncfusion.Blazor.Adaptors.WebApiAdaptor" />
</DataSourceSettings>
<Layout Type="LayoutType.HierarchicalTree" VerticalSpacing="75" HorizontalSpacing="75" />
</SfDiagramComponent>Read Data
Fetch records from the data source based on an optional query:
// Read all data
List<object> records = (List<object>)await _diagram.ReadDataAsync();
// Read with a query filter
Query query = new Query().Where("ReportsTo", "equal", "1");
List<object> filtered = (List<object>)await _diagram.ReadDataAsync(query);Insert Data
Add a new record to the data source. The diagram layout updates automatically:
var newEmployee = new EmployeeDetails
{
EmployeeID = 10,
Name = "Alice",
Designation = "Developer",
ReportsTo = "3",
Colour = "Blue"
};
await _diagram.InsertDataAsync(newEmployee);
// Insert with explicit table name and position
await _diagram.InsertDataAsync(newEmployee, tableName: "Employees", position: 0);Update Data
Modify an existing record identified by a key field:
var updatedEmployee = new EmployeeDetails
{
EmployeeID = 6,
Name = "Michael",
Designation = "Product Manager",
ReportsTo = "1",
Colour = "Green"
};
await _diagram.UpdateDataAsync("EmployeeID", updatedEmployee);
// Update with table name and original data (for delta updates)
await _diagram.UpdateDataAsync("EmployeeID", updatedEmployee, tableName: "Employees", original: originalEmployee);Delete Data
Remove a record by key field and value:
// Delete the record where EmployeeID = 6
await _diagram.DeleteDataAsync("EmployeeID", 6);
// Delete with explicit table name
await _diagram.DeleteDataAsync("EmployeeID", 6, tableName: "Employees");Refresh Data Source
RefreshDataSourceAsync() dynamically updates the diagram layout to reflect changes made to the underlying DataSource object. It rebuilds the entire diagram from the new data — use it when you replace or mutate the data collection bound to DataSourceSettings.DataSource.
Example — MindMap with runtime data refresh:
@using Syncfusion.Blazor.Diagram
@using Syncfusion.Blazor.Buttons
<SfDiagramComponent @ref="_diagram" Height="600px"
NodeCreating="@OnNodeCreating"
ConnectorCreating="@OnConnectorCreating">
<DataSourceSettings ID="Id" ParentID="ParentId" DataSource="@_dataSource" />
<Layout Type="LayoutType.MindMap">
<LayoutMargin Top="20" Left="20" />
</Layout>
</SfDiagramComponent>
<SfButton Content="Refresh Data Source" OnClick="@RefreshData" />
@code {
private SfDiagramComponent _diagram;
private void OnNodeCreating(IDiagramObject obj)
{
Node node = obj as Node;
node.Height = 25;
node.Width = 25;
node.Style = new ShapeStyle { Fill = "#6495ED", StrokeWidth = 1, StrokeColor = "white" };
node.Shape = new BasicShape { Type = NodeShapes.Basic };
}
private void OnConnectorCreating(IDiagramObject obj)
{
Connector connector = obj as Connector;
connector.Type = ConnectorSegmentType.Bezier;
connector.Style = new ShapeStyle { StrokeColor = "#6495ED", StrokeWidth = 2 };
connector.TargetDecorator = new DecoratorSettings { Shape = DecoratorShape.None };
}
public class MindMapDetails
{
public string Id { get; set; }
public string Label { get; set; }
public string ParentId { get; set; }
public string Branch { get; set; }
}
// Initial data source — full tree
public object _dataSource = new List<object>()
{
new MindMapDetails { Id = "1", Label = "Creativity", ParentId = "", Branch = "Root" },
new MindMapDetails { Id = "2", Label = "Brainstorming", ParentId = "1", Branch = "Right" },
new MindMapDetails { Id = "3", Label = "Complementing", ParentId = "1", Branch = "Left" },
new MindMapDetails { Id = "4", Label = "Sessions", ParentId = "2", Branch = "subRight" },
new MindMapDetails { Id = "5", Label = "Complementing", ParentId = "2", Branch = "subRight" },
new MindMapDetails { Id = "6", Label = "Local", ParentId = "3", Branch = "subRight" },
new MindMapDetails { Id = "7", Label = "Remote", ParentId = "3", Branch = "subRight" },
new MindMapDetails { Id = "8", Label = "Individual", ParentId = "3", Branch = "subRight" },
new MindMapDetails { Id = "9", Label = "Teams", ParentId = "3", Branch = "subRight" },
new MindMapDetails { Id = "10", Label = "Ideas", ParentId = "5", Branch = "subRight" },
new MindMapDetails { Id = "11", Label = "Engagement", ParentId = "5", Branch = "subRight" },
};
// Replace the data source with a trimmed set, then call RefreshDataSourceAsync
private async Task RefreshData()
{
_dataSource = new List<object>()
{
new MindMapDetails { Id = "1", Label = "Creativity", ParentId = "", Branch = "Root" },
new MindMapDetails { Id = "2", Label = "Brainstorming", ParentId = "1", Branch = "Right" },
new MindMapDetails { Id = "3", Label = "Complementing", ParentId = "1", Branch = "Left" },
new MindMapDetails { Id = "4", Label = "Sessions", ParentId = "2", Branch = "subRight" },
new MindMapDetails { Id = "5", Label = "Complementing", ParentId = "2", Branch = "subRight" },
};
// Rebuild the diagram layout from the new data
await _diagram.RefreshDataSourceAsync();
}
}Note:RefreshDataSourceAsync()rebuilds the entire diagram from the currentDataSourcevalue. Always assign the new data to the field before calling this method so the diagram reads the updated collection.
UseDoLayoutAsync()instead when working with manual node/connector collections (withoutDataSourceSettings).
Method Signatures Reference
| Method | Signature | Description |
|---|---|---|
ReadDataAsync | Task<IEnumerable<object>> ReadDataAsync(Query? query = null) | Read records from data source |
InsertDataAsync | Task InsertDataAsync(object data, string? tableName = null, Query? query = null, int position = 0) | Add a new record |
UpdateDataAsync | Task UpdateDataAsync(string keyField, object data, string? tableName = null, Query? query = null, object? original = null, IDictionary<string, object>? updateProperties = null) | Modify an existing record |
DeleteDataAsync | Task DeleteDataAsync(string keyField, object value, string? tableName = null, Query? query = null) | Remove a record |
RefreshDataSourceAsync | Task RefreshDataSourceAsync() | Reload all data and rebuild layout |
---
Common Gotchas
- Root node must have an empty `ParentId` (empty string
"", notnull) — otherwise it won't be recognized as root - `ID` and `ParentID` in `DataSourceSettings` are field name strings — they must exactly match your model's property names (case-sensitive)
- `NodeCreating` is required — without it, the auto-generated nodes have no size and won't be visible
- `DataSource` + `Layout` go together — data binding without a layout type results in all nodes stacked at (0, 0)
- `node.Data` in
NodeCreatingholds your original data object — cast it to your model type to access custom fields - Do NOT set `Nodes`/`Connectors` collections when using data binding — the diagram generates them automatically from the data source
Diagram Events in Blazor Diagram
Table of Contents
- Overview
- Lifecycle Events
- NodeCreating / ConnectorCreating
- Click and Keyboard Events
- Mouse Interaction Events
- MouseEnter / MouseLeave / MouseHover
- Element Change Events
- CollectionChanging / CollectionChanged
- SourcePointChanging / SourcePointChanged
- TargetPointChanging / TargetPointChanged
- SegmentCollectionChange
- PropertyChanged
- Drag-and-Drop Events
- User Handle Events
- FixedUserHandleClick
- History (Undo/Redo) Events
- Auto-Scroll Events
- Events Quick-Reference Table
- Common Gotchas
---
Overview
Subscribe to diagram events by binding event handlers in the SfDiagramComponent markup. All events follow standard Blazor event callback patterns.
---
Lifecycle Events
Created
Fires once after the diagram is fully rendered. Use it to perform post-render setup (e.g., programmatic selection):
<SfDiagramComponent @ref="_diagram" Created="OnCreated" />
@code {
private void OnCreated(object args)
{
_diagram.Select(new ObservableCollection<IDiagramObject> { _diagram.Nodes[0] });
}
}NodeCreating / ConnectorCreating
Fires for each node or connector as it is initialised. Use these events to apply default properties uniformly across all diagram elements without repeating setup in OnInitialized:
<SfDiagramComponent Nodes="@_nodes"
Connectors="@_connectors"
NodeCreating="OnNodeCreating"
ConnectorCreating="OnConnectorCreating" />
@code {
private void OnNodeCreating(IDiagramObject obj)
{
// obj is always the raw IDiagramObject — cast to Node to access properties
if (obj is Node node)
{
node.Style.Fill = "#357BD2";
node.Style.StrokeColor = "white";
node.Style.Opacity = 1;
}
}
private void OnConnectorCreating(IDiagramObject obj)
{
if (obj is Connector connector)
{
connector.Style.StrokeColor = "black";
connector.Style.StrokeWidth = 1;
connector.TargetDecorator.Style.Fill = "black";
connector.TargetDecorator.Style.StrokeColor = "black";
}
}
}⚠️ The parameter type is `IDiagramObject`, not `Node` or `Connector` directly.
Always cast before accessing element-specific properties:
```csharp
// ❌ Wrong — IDiagramObject has no Style property
private void OnNodeCreating(IDiagramObject obj) { obj.Style.Fill = "red"; }
>
// ✅ Correct — cast first
private void OnNodeCreating(IDiagramObject obj)
{
if (obj is Node node) { node.Style.Fill = "red"; }
}
```
---
Mouse Interaction Events
MouseEnter / MouseLeave / MouseHover
Fire when the mouse pointer enters, leaves, or hovers over a node or connector. All three share the same DiagramElementMouseEventArgs argument type with IDiagramObject? elements:
<SfDiagramComponent MouseEnter="OnMouseEnter"
MouseLeave="OnMouseLeave"
MouseHover="OnMouseHover" />
@code {
private void OnMouseEnter(DiagramElementMouseEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — you must cast to Node or Connector
// args.Element — IDiagramObject? — the node or connector the pointer entered
// args.ActualObject — IDiagramObject? — the actual object being hovered
if (args.Element is Node node)
{
// ✅ Now you can access Node-specific properties
Console.WriteLine($"Mouse entered node: {node.ID}");
Console.WriteLine($" Label: {node.Annotations?[0]?.Content}");
Console.WriteLine($" Position: ({node.OffsetX}, {node.OffsetY})");
}
else if (args.Element is Connector conn)
{
// ✅ Now you can access Connector-specific properties
Console.WriteLine($"Mouse entered connector: {conn.ID}");
Console.WriteLine($" From {conn.SourceID} to {conn.TargetID}");
}
}
private void OnMouseLeave(DiagramElementMouseEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — cast to access type-specific properties
if (args.Element is Node node)
{
Console.WriteLine($"Mouse left node: {node.ID}");
}
else if (args.Element is Connector conn)
{
Console.WriteLine($"Mouse left connector: {conn.ID}");
}
}
private void OnMouseHover(DiagramElementMouseEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — cast to access type-specific properties
if (args.Element is Node node)
{
Console.WriteLine($"Hovering over node: {node.ID}");
// Change appearance on hover
node.Style.Opacity = 0.8;
}
}
}| Event | Fires When |
|---|---|
MouseEnter | Mouse pointer enters the boundary of a node or connector |
MouseLeave | Mouse pointer exits the boundary of a node or connector |
MouseHover | Mouse pointer is hovering over a node or connector |
⚠️ Critical: All mouse event arguments contain IDiagramObject? elements.You must cast toNodeorConnectorto access properties likeID,Annotations, or connector-specific properties.
Attempting to access type-specific properties without casting causes CS1061 (member not found).---
Click and Keyboard Events
Click
Fires when a user clicks a node, connector, or the canvas:
<SfDiagramComponent Click="OnClick" />
@code {
private void OnClick(Syncfusion.Blazor.Diagram.ClickEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — you must cast to Node or Connector
// args.Element — IDiagramObject? — the clicked element (null if canvas clicked)
// args.ActualObject — IDiagramObject? — the actual object under the cursor
// args.Position — DiagramPoint with click coordinates
// args.Count — method returning int: 1 for single click, 2 for double click
int count = args.Count; // ✅ call Count as a property/method, assign to int first
if (count == 1)
{
// Single click
if (args.Element is Node node)
{
// ✅ Now you can access Node-specific properties
Console.WriteLine($"Clicked node: {node.ID}");
Console.WriteLine($" Label: {node.Annotations?[0]?.Content}");
}
else if (args.Element is Connector conn)
{
// ✅ Now you can access Connector-specific properties
Console.WriteLine($"Clicked connector: {conn.ID}");
Console.WriteLine($" Connects {conn.SourceID} to {conn.TargetID}");
}
else
{
Console.WriteLine($"Clicked canvas at ({args.Position?.X}, {args.Position?.Y})");
}
}
}
}⚠️ Critical:args.ElementisIDiagramObject?, notNodeorConnectordirectly.
You must cast to access type-specific properties. Attempting to access properties without casting causes CS1061 (member not found).⚠️ `ClickEventArgs` name collision: If your page also uses @using Syncfusion.Blazor.Navigations (or Buttons),ClickEventArgs becomes ambiguous. Always qualify it:```csharp
// ✅ Use the fully qualified type in the handler signature
private void OnClick(Syncfusion.Blazor.Diagram.ClickEventArgs args) { }
```
⚠️ `args.Count` is NOT an `int` field — it is a method/property that returns an int.Do NOT compare it directly with == inline without storing the result first:```csharp
// ❌ Wrong — CS0019: Operator '==' cannot be applied to operands of type 'method group' and 'int'
if (args.Count == 2)
>
// ✅ Correct — store result then compare
int clickCount = args.Count;
if (clickCount == 2) { / double-click / }
```
Double-Click
⚠️ There is NO `OnDoubleClick` event and NO `DoubleClickEventArgs` type in SfDiagramComponent.Detect double-clicks via theClickevent by readingargs.Countas anint:
<SfDiagramComponent Click="OnClick" />
@code {
private void OnClick(Syncfusion.Blazor.Diagram.ClickEventArgs args)
{
int clickCount = args.Count; // store as int first
if (clickCount == 2)
{
// Double-click
var target = args.Element is Connector c ? $"Connector [{c.ID}]"
: args.Element is Node n ? $"Node [{n.ID}]"
: "Canvas";
Console.WriteLine($"Double-clicked: {target}");
}
}
}KeyDown / KeyUp
Fires when a key is pressed/released while the diagram has focus:
<SfDiagramComponent KeyDown="OnKeyDown" KeyUp="OnKeyUp" />
@code {
private void OnKeyDown(KeyEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — you must cast to Node or Connector
// args.Key — string — key name (e.g., "Delete", "Enter")
// args.KeyCode — int — the actual key code pressed
// args.KeyModifiers — ModifierKeys — modifier flags (Control, Shift, Alt)
// args.Element — IDiagramObject? — the currently selected element (if any)
if (args.Key == "Delete")
{
// Custom delete handling
if (args.Element is Node node)
{
// ✅ Now you can access Node-specific properties
Console.WriteLine($"Delete key pressed on node: {node.ID}");
}
else if (args.Element is Connector conn)
{
// ✅ Now you can access Connector-specific properties
Console.WriteLine($"Delete key pressed on connector: {conn.ID}");
}
}
else if (args.Key == "c" && args.KeyModifiers == ModifierKeys.Control)
{
// Custom copy handling
Console.WriteLine("Ctrl+C pressed");
}
}
private void OnKeyUp(KeyEventArgs args)
{
// Similar structure — args.Element must be cast to access type-specific properties
}
}⚠️ Critical:args.ElementisIDiagramObject?, notNodeorConnectordirectly.
You must cast to access type-specific properties likeID,Annotations, orSourceID.
Attempting to access properties without casting causes CS1061 (member not found).---
Element Change Events
PositionChanging / PositionChanged
Fire when a node or connector is moved:
<SfDiagramComponent PositionChanging="OnPositionChanging"
PositionChanged="OnPositionChanged" />
@code {
private void OnPositionChanging(Syncfusion.Blazor.Diagram.PositionChangingEventArgs args)
{
args.Cancel = true; // block the move
}
private void OnPositionChanged(Syncfusion.Blazor.Diagram.PositionChangedEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — you must cast to Node or Connector
// args.Element — IDiagramObject? — the node or connector being dragged
// args.NewValue — DiagramSelectionSettings? — selector state after the move
// args.OldValue — DiagramSelectionSettings? — selector state before the move
if (args.Element is Node node)
{
// ✅ Now you can access Node-specific properties
Console.WriteLine($"Node [{node.ID}] moved");
Console.WriteLine($" New position: ({node.OffsetX}, {node.OffsetY})");
Console.WriteLine($" Size: {node.Width} x {node.Height}");
Console.WriteLine($" Label: {node.Annotations?[0]?.Content}");
}
else if (args.Element is Connector connector)
{
// ✅ Now you can access Connector-specific properties
Console.WriteLine($"Connector [{connector.ID}] moved");
Console.WriteLine($" Source: {connector.SourceID}, Target: {connector.TargetID}");
}
// Alternative: read position from the selector's bounding box
if (args.NewValue != null)
{
Console.WriteLine($"Selection box after move:");
Console.WriteLine($" OffsetX: {args.NewValue.OffsetX} OffsetY: {args.NewValue.OffsetY}");
Console.WriteLine($" Width: {args.NewValue.Width} Height: {args.NewValue.Height}");
}
if (args.OldValue != null)
{
Console.WriteLine($"Selection box before move:");
Console.WriteLine($" OffsetX: {args.OldValue.OffsetX} OffsetY: {args.OldValue.OffsetY}");
}
}
}PositionChangedEventArgs properties
| Property | Type | Description |
|---|---|---|
Element | IDiagramObject? | The node or connector currently being dragged |
NewValue | DiagramSelectionSettings? | The selector's state (position, size) after the drag operation |
OldValue | DiagramSelectionSettings? | The selector's state (position, size) before the drag operation |
// ✅ Pattern-match Element to get the specific node or connector
if (args.Element is Node n)
Console.WriteLine($"Moved node: {n.ID} new pos: ({n.OffsetX}, {n.OffsetY})");
else if (args.Element is Connector c)
Console.WriteLine($"Moved connector: {c.ID}");
// ✅ Use NewValue / OldValue for the bounding-box of the selection
double deltaX = (args.NewValue?.OffsetX ?? 0) - (args.OldValue?.OffsetX ?? 0);
double deltaY = (args.NewValue?.OffsetY ?? 0) - (args.OldValue?.OffsetY ?? 0);
Console.WriteLine($"Moved by: ({deltaX}, {deltaY})");⚠️ `args.NewValue` and `args.OldValue` are `DiagramSelectionSettings?`, not aNodewithOffsetX/OffsetYfields.
They represent the selector bounding box, not the individual node position.
To read the moved node's position, either castargs.ElementasNode, or read from theDiagramSelectionSettings:
```csharp
// ❌ Wrong — NewValue is DiagramSelectionSettings?, not a Node
double x = args.NewValue.OffsetX; // compiles but is selector bbox, not node center
>
// ✅ Correct — read node position from Element
if (args.Element is Node n)
Console.WriteLine($"({n.OffsetX}, {n.OffsetY})");
>
// ✅ Also correct — selector bbox from NewValue (null-safe)
Console.WriteLine($"({args.NewValue?.OffsetX}, {args.NewValue?.OffsetY})");
```
SizeChanging / SizeChanged
Fire when a node is resized:
<SfDiagramComponent SizeChanging="OnSizeChanging" SizeChanged="OnSizeChanged" />
@code {
private void OnSizeChanging(Syncfusion.Blazor.Diagram.SizeChangingEventArgs args)
{
args.Cancel = true; // block resize
}
private void OnSizeChanged(Syncfusion.Blazor.Diagram.SizeChangedEventArgs args)
{
// ✅ args.Element is DiagramSelectionSettings — NOT Node.
// Cast it and read the first resized node from its .Nodes collection.
if (args.Element is DiagramSelectionSettings sel && sel.Nodes.Count > 0)
{
var node = sel.Nodes[0];
double w = args.NewValue.Width; // plain double — no ?? needed
double h = args.NewValue.Height; // plain double — no ?? needed
Console.WriteLine($"Node [{node.ID}] resized to {Math.Round(w)} x {Math.Round(h)}");
}
}
}⚠️ `SizeChangedEventArgs.Element` is typed as `DiagramSelectionSettings`, not Node.Pattern-matchingargs.Element is Node nalways fails withCS8121.
Cast toDiagramSelectionSettingsand read.Nodes[0]to get the resized node:
```csharp
// ❌ Wrong — CS8121: DiagramSelectionSettings cannot match Node
if (args.Element is Node n) { }
>
// ✅ Correct — Element is DiagramSelectionSettings
if (args.Element is DiagramSelectionSettings sel && sel.Nodes.Count > 0)
{
var node = sel.Nodes[0];
double w = args.NewValue.Width;
double h = args.NewValue.Height;
}
```
⚠️ `args.NewValue.Width` and `args.NewValue.Height` are plain `double`, not double?.Using??on them causesCS0019. Assign them directly:
```csharp
// ❌ Wrong — CS0019
double w = args.NewValue.Width ?? 0;
>
// ✅ Correct
double w = args.NewValue.Width;
```
RotationChanging / RotationChanged
Fire when a node is rotated:
<SfDiagramComponent RotationChanging="OnRotationChanging"
RotationChanged="OnRotationChanged" />
@code {
private void OnRotationChanging(Syncfusion.Blazor.Diagram.RotationChangingEventArgs args)
{
args.Cancel = true; // block rotation
}
private void OnRotationChanged(Syncfusion.Blazor.Diagram.RotationChangedEventArgs args)
{
}
}SelectionChanging / SelectionChanged
Fire when the selection set changes:
<SfDiagramComponent SelectionChanging="OnSelectionChanging"
SelectionChanged="OnSelectionChanged" />
@code {
private void OnSelectionChanging(Syncfusion.Blazor.Diagram.SelectionChangingEventArgs args)
{
args.Cancel = true; // prevent selection change
}
private void OnSelectionChanged(Syncfusion.Blazor.Diagram.SelectionChangedEventArgs args)
{
// args.NewValue — ObservableCollection<IDiagramObject>? — elements selected after the event
// args.OldValue — ObservableCollection<IDiagramObject>? — elements that were selected before
// args.Type — CollectionChangedAction (ObjectAdded / ObjectRemoved)
// args.ActionTrigger — DiagramAction — what caused the selection change
if (args.NewValue != null)
{
foreach (var obj in args.NewValue)
{
if (obj is Node node)
Console.WriteLine($"Newly selected node: {node.ID}");
else if (obj is Connector conn)
Console.WriteLine($"Newly selected connector: {conn.ID}");
}
}
if (args.OldValue != null)
{
foreach (var obj in args.OldValue)
{
if (obj is Node node)
Console.WriteLine($"Deselected node: {node.ID}");
}
}
Console.WriteLine($"Change type: {args.Type}"); // ObjectAdded / ObjectRemoved
Console.WriteLine($"Triggered by: {args.ActionTrigger}"); // DiagramAction enum value
}
}⚠️ `SelectionChangedEventArgs` name collision: If your page also uses @using Syncfusion.Blazor.Buttons(or other Syncfusion packages), SelectionChangedEventArgs becomes ambiguous. Always qualify it:```csharp
// ✅ Fully qualified
private void OnSelectionChanged(Syncfusion.Blazor.Diagram.SelectionChangedEventArgs args) { }
```
SelectionChangedEventArgs properties
| Property | Type | Description |
|---|---|---|
NewValue | ObservableCollection<IDiagramObject>? | Elements that are selected after the event fires |
OldValue | ObservableCollection<IDiagramObject>? | Elements that were selected before the event fired; empty if nothing was previously selected |
Type | CollectionChangedAction | Whether items were added (ObjectAdded) or removed (ObjectRemoved) from the selection |
ActionTrigger | DiagramAction | The actual cause of the selection change (e.g., user interaction, programmatic call) |
Understanding IDiagramObject and Casting
⚠️ Critical:args.NewValuecontains `IDiagramObject` items, notNodeorConnectordirectly.
IDiagramObjectis an interface that bothNodeandConnectorimplement. To access element-specific properties likeID,Annotations, or connector-specific properties, you must cast toNodeorConnector.
Why casting is required:
IDiagramObjectis a common interface for all diagram elements- It provides only basic properties shared across all element types
NodeandConnectorhave type-specific properties not available onIDiagramObject- Attempting to access these properties without casting causes
CS1061(member not found) errors
Complete working example with proper casting:
<SfDiagramComponent SelectionChanged="OnSelectionChanged" />
@code {
private void OnSelectionChanged(Syncfusion.Blazor.Diagram.SelectionChangedEventArgs args)
{
if (args?.NewValue?.Count > 0)
{
foreach (var item in args.NewValue)
{
if (item is Node selectedNode)
{
// ✅ Now you can access Node-specific properties
var nodeId = selectedNode.ID;
var label = selectedNode.Annotations?[0]?.Content;
var offsetX = selectedNode.OffsetX;
var offsetY = selectedNode.OffsetY;
Console.WriteLine($"Selected Node: ID={nodeId}, Label={label}, Position=({offsetX}, {offsetY})");
}
else if (item is Connector selectedConnector)
{
// ✅ Now you can access Connector-specific properties
var connectorId = selectedConnector.ID;
var sourceNode = selectedConnector.SourceID;
var targetNode = selectedConnector.TargetID;
Console.WriteLine($"Selected Connector: ID={connectorId}, From={sourceNode} To={targetNode}");
}
}
}
}
}Common pitfalls:
// ❌ Wrong — IDiagramObject has no ID property (well, it does via interface, but shows confusion)
// More importantly, you can't access Node/Connector-specific props like Annotations
foreach (var item in args.NewValue)
{
Console.WriteLine(item.ID); // May compile but incorrect intent
var label = item.Annotations[0].Content; // ❌ CS1061 — Annotations not on IDiagramObject
}
// ✅ Correct — always cast first
foreach (var item in args.NewValue)
{
if (item is Node node)
Console.WriteLine($"Node {node.ID}: {node.Annotations?[0]?.Content}");
else if (item is Connector conn)
Console.WriteLine($"Connector {conn.ID}");
}// ✅ Iterate args.NewValue directly — it is ObservableCollection<IDiagramObject>
if (args.NewValue != null)
{
foreach (var obj in args.NewValue)
{
if (obj is Node node) Console.WriteLine($"Added to selection: {node.ID}");
else if (obj is Connector c) Console.WriteLine($"Added to selection: {c.ID}");
}
}
// ✅ Check what triggered the change
Console.WriteLine($"Type: {args.Type}"); // ObjectAdded / ObjectRemoved
Console.WriteLine($"Trigger: {args.ActionTrigger}"); // DiagramAction enum valueCollectionChanging / CollectionChanged
CollectionChanging fires before a node or connector is added or removed (cancellable). CollectionChanged fires after the collection has been updated:
<SfDiagramComponent CollectionChanging="OnCollectionChanging"
CollectionChanged="OnCollectionChanged" />
@code {
private void OnCollectionChanging(Syncfusion.Blazor.Diagram.CollectionChangingEventArgs args)
{
// args.Cancel = true — prevent the add/remove
args.Cancel = true;
}
}---
CollectionChanged
Fires when a node or connector is added to or removed from the diagram at runtime:
<SfDiagramComponent CollectionChanged="OnCollectionChanged" />
@code {
private void OnCollectionChanged(Syncfusion.Blazor.Diagram.CollectionChangedEventArgs args)
{
// args.Action — CollectionChangedAction — whether the element was added or removed
// args.ActionTrigger — DiagramAction — what caused the change (interaction, tool, etc.)
// args.Element — NodeBase? — the node or connector that was added/removed/modified
if (args.Action == CollectionChangedAction.Add)
{
if (args.Element is Node node)
Console.WriteLine($"Node added: {node.ID}");
else if (args.Element is Connector conn)
Console.WriteLine($"Connector added: {conn.ID}");
}
else if (args.Action == CollectionChangedAction.Remove)
{
Console.WriteLine($"Element removed: {args.Element?.ID}");
}
Console.WriteLine($"Triggered by: {args.ActionTrigger}"); // DiagramAction enum value
}
}CollectionChangedEventArgs properties
| Property | Type | Description |
|---|---|---|
Action | CollectionChangedAction | The type of change — Add when an element is added, Remove when an element is removed |
ActionTrigger | DiagramAction | The current action being performed (e.g., user interaction, drawing tool, programmatic call) |
Element | NodeBase? | The actual node or connector that was added, removed, or modified — pattern-match as Node or Connector |
// ✅ Pattern-match Element as Node or Connector — it is typed as NodeBase?
if (args.Element is Node n)
Console.WriteLine($"Node [{n.ID}] — Action: {args.Action} Trigger: {args.ActionTrigger}");
else if (args.Element is Connector c)
Console.WriteLine($"Connector [{c.ID}] — Action: {args.Action} Trigger: {args.ActionTrigger}");⚠️ `args.Element` is typed as `NodeBase?` — notNodeorConnectordirectly.
Always null-check or pattern-match before accessing it:
```csharp
// ❌ Wrong — Element is NodeBase?, not Node; direct cast throws InvalidCastException
var node = (Node)args.Element;
>
// ✅ Correct — safe pattern match
if (args.Element is Node node) { Console.WriteLine(node.ID); }
if (args.Element is Connector conn) { Console.WriteLine(conn.ID); }
```
---
SourcePointChanging / SourcePointChanged
Fire when a connector's source endpoint is dragged:
<SfDiagramComponent SourcePointChanging="OnSourcePointChanging"
SourcePointChanged="OnSourcePointChanged" />
@code {
private void OnSourcePointChanging(EndPointChangingEventArgs args)
{
// args.Cancel = true — block the source point change
args.Cancel = true;
}
private void OnSourcePointChanged(EndPointChangedEventArgs args)
{
// args.Connector — the connector whose source point changed
// args.NewValue — DiagramPoint — new source point position
// args.OldValue — DiagramPoint — previous source point position
Console.WriteLine($"Source moved to ({args.NewValue?.X}, {args.NewValue?.Y})");
}
}TargetPointChanging / TargetPointChanged
Fire when a connector's target endpoint is dragged:
<SfDiagramComponent TargetPointChanging="OnTargetPointChanging"
TargetPointChanged="OnTargetPointChanged" />
@code {
private void OnTargetPointChanging(EndPointChangingEventArgs args)
{
// args.Cancel = true — block the target point change
}
private void OnTargetPointChanged(EndPointChangedEventArgs args)
{
// args.Connector — the connector whose target point changed
// args.NewValue — DiagramPoint — new target point position
// args.OldValue — DiagramPoint — previous target point position
Console.WriteLine($"Target moved to ({args.NewValue?.X}, {args.NewValue?.Y})");
}
}| Event pair | Args type (Changing) | Args type (Changed) | Has Cancel? |
|---|---|---|---|
SourcePointChanging / SourcePointChanged | EndPointChangingEventArgs | EndPointChangedEventArgs | Yes |
TargetPointChanging / TargetPointChanged | EndPointChangingEventArgs | EndPointChangedEventArgs | Yes |
⚠️ `SourcePointChanging`/`TargetPointChanging` use `EndPointChangingEventArgs` — not PositionChangingEventArgs. These events fire only when the connector endpoint itself is dragged, not when the whole connector moves with a node.---
SegmentCollectionChange
Fires when a connector's segment collection is modified (e.g. segments added or removed during interaction):
<SfDiagramComponent SegmentCollectionChange="OnSegmentCollectionChange" />
@code {
private void OnSegmentCollectionChange(SegmentCollectionChangeEventArgs args)
{
// args.Element — the connector whose segments changed
// args.NewValue — updated segment collection
// args.Cancel = true — prevent the segment change
Console.WriteLine($"Segment changed on connector: {(args.Element as Connector)?.ID}");
}
}---
PropertyChanged
Fires when a node or connector property is modified at runtime (e.g. style, size, position set programmatically):
<SfDiagramComponent PropertyChanged="OnPropertyChanged" />
@code {
private void OnPropertyChanged(Syncfusion.Blazor.Diagram.PropertyChangedEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — you must cast to Node or Connector
// args.Element — IDiagramObject? — the node or connector whose property changed
// args.PropertyName — string — the name of the property that changed
// args.NewValue — object? — the new value of the property
// args.OldValue — object? — the old value of the property
if (args.Element is Node node)
{
Console.WriteLine($"Node [{node.ID}] property changed: {args.PropertyName}");
Console.WriteLine($" Old value: {args.OldValue}");
Console.WriteLine($" New value: {args.NewValue}");
// Now you can access Node-specific properties
if (args.PropertyName == "Style")
{
var fill = node.Style?.Fill;
var stroke = node.Style?.StrokeColor;
Console.WriteLine($" Fill: {fill}, Stroke: {stroke}");
}
}
else if (args.Element is Connector conn)
{
Console.WriteLine($"Connector [{conn.ID}] property changed: {args.PropertyName}");
// Now you can access Connector-specific properties
if (args.PropertyName == "TargetID")
{
Console.WriteLine($" Connector now targets: {conn.TargetID}");
}
}
}
}⚠️ Critical:args.ElementisIDiagramObject?, notNodeorConnectordirectly.
You must cast to access type-specific properties. Attempting to access properties likeAnnotationsorStyleonIDiagramObjectcausesCS1061(member not found).
---
ConnectionChanging / ConnectionChanged
Fire when a connector's source or target endpoint is dragged to a new node or port:
<SfDiagramComponent ConnectionChanging="OnConnectionChanging"
ConnectionChanged="OnConnectionChanged" />
@code {
private void OnConnectionChanging(Syncfusion.Blazor.Diagram.ConnectionChangingEventArgs args)
{
args.Cancel = true; // prevent the connection change
}
private void OnConnectionChanged(Syncfusion.Blazor.Diagram.ConnectionChangedEventArgs args)
{
// args.Connector — Connector? — the connector whose endpoint changed
// args.ConnectorAction — DiagramElementAction — whether the source or target end moved
// args.NewValue — ConnectionObject? — new source/target node or port after the change
// args.OldValue — ConnectionObject? — previous source/target node or port before the change
if (args.Connector != null)
Console.WriteLine($"Connector: {args.Connector.ID}");
// Distinguish source-end change from target-end change
Console.WriteLine($"Endpoint changed: {args.ConnectorAction}"); // DiagramElementAction enum
// NewValue — what the endpoint is NOW connected to
if (args.NewValue != null)
{
Console.WriteLine($"New NodeID: {args.NewValue.NodeID}"); // node the endpoint landed on
Console.WriteLine($"New PortID: {args.NewValue.PortID}"); // port, if any
}
// OldValue — what the endpoint WAS connected to
if (args.OldValue != null)
{
Console.WriteLine($"Old NodeID: {args.OldValue.NodeID}");
Console.WriteLine($"Old PortID: {args.OldValue.PortID}");
}
}
}ConnectionChangedEventArgs properties
| Property | Type | Description |
|---|---|---|
Connector | Connector? | The connector whose source or target endpoint was changed |
ConnectorAction | DiagramElementAction | Indicates which endpoint moved — source end or target end |
NewValue | ConnectionObject? | The current (after) source or target — holds NodeID and PortID of the new connection |
OldValue | ConnectionObject? | The previous (before) source or target — holds NodeID and PortID of the old connection |
// ✅ Check which endpoint changed
if (args.ConnectorAction == DiagramElementAction.ConnectorSourceEnd)
Console.WriteLine("Source endpoint was moved.");
else if (args.ConnectorAction == DiagramElementAction.ConnectorTargetEnd)
Console.WriteLine("Target endpoint was moved.");
// ✅ Read NodeID / PortID from ConnectionObject (null-safe)
string newNode = args.NewValue?.NodeID ?? "(none)";
string newPort = args.NewValue?.PortID ?? "(none)";
string oldNode = args.OldValue?.NodeID ?? "(none)";
Console.WriteLine($"Reconnected: {oldNode} → {newNode} (port: {newPort})");⚠️ `args.NewValue` and `args.OldValue` are `ConnectionObject?`, not Node.They do not haveOffsetX/OffsetY. Access node and port identity via.NodeIDand.PortID:
```csharp
// ❌ Wrong — ConnectionObject has no OffsetX
double x = args.NewValue.OffsetX;
>
// ✅ Correct — read NodeID and PortID
string nodeId = args.NewValue?.NodeID ?? string.Empty;
string portId = args.NewValue?.PortID ?? string.Empty;
```
---
TextChanged
Fires after inline text editing completes:
<SfDiagramComponent TextChanged="OnTextChanged" />
@code {
private void OnTextChanged(TextChangeEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — you must cast to Node or Connector
// args.Element — IDiagramObject? — the node or connector being edited
// args.Annotation — Annotation? — the specific annotation being edited
// args.NewValue — string? — updated text
// args.OldValue — string? — previous text
if (args.Element is Node node)
{
// ✅ Now you can access Node-specific properties
Console.WriteLine($"Node [{node.ID}] text changed");
Console.WriteLine($" Old text: '{args.OldValue}'");
Console.WriteLine($" New text: '{args.NewValue}'");
Console.WriteLine($" Annotation ID: {args.Annotation?.ID}");
}
else if (args.Element is Connector connector)
{
// ✅ Now you can access Connector-specific properties
Console.WriteLine($"Connector [{connector.ID}] text changed");
Console.WriteLine($" From '{args.OldValue}' to '{args.NewValue}'");
Console.WriteLine($" Source: {connector.SourceID}, Target: {connector.TargetID}");
}
}
}⚠️ `TextChangedEventArgs` does NOT exist — using it causes CS0246.The correct event args type is `TextChangeEventArgs` (no d):```csharp
// ❌ Wrong — CS0246: TextChangedEventArgs not found
private void OnTextChanged(TextChangedEventArgs args) { }
>
// ✅ Correct
private void OnTextChanged(TextChangeEventArgs args) { }
```
⚠️ Critical:args.ElementisIDiagramObject?, notNodeorConnectordirectly.
You must cast to access type-specific properties. Attempting to access properties without casting causes CS1061 (member not found).---
Drag-and-Drop Events
These fire when symbols are dragged from the SymbolPalette into the diagram. All use IDiagramObject elements that must be cast:
<SfDiagramComponent DragStart="OnDragStart"
Dragging="OnDragging"
DragLeave="OnDragLeave"
DragDrop="OnDragDrop" />
@code {
private void OnDragStart(Syncfusion.Blazor.Diagram.DragStartEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — cast to Node or Connector
if (args.Element is Node node)
{
// Modify the node before it's added
node.Width = 300;
node.Height = 300;
node.Style.Fill = "#FF5722";
}
else if (args.Element is Connector conn)
{
conn.Style.StrokeWidth = 3;
}
}
private void OnDragging(DraggingEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — the element being dragged
if (args.Element is DiagramSelectionSettings selector)
{
Console.WriteLine($"Dragging at position: ({args.Position?.X}, {args.Position?.Y})");
}
}
private void OnDragLeave(DragLeaveEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — element leaving the diagram
if (args.Element is Node node)
Console.WriteLine($"Node [{node.ID}] left the diagram");
}
private void OnDragDrop(DropEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — the dropped node
// args.Target is IDiagramObject? — the target node/connector if dropped on one
// args.Position — drop location
// args.Cancel = true — reject the drop
if (args.Element is Node droppedNode)
{
Console.WriteLine($"Dropped node: {droppedNode.ID}");
if (args.Target is Node targetNode)
Console.WriteLine($" Dropped on node: {targetNode.ID}");
}
}
}⚠️ Critical: All drag-drop event arguments contain IDiagramObject? elements.You must cast toNodeorConnectorto access properties likeID,Width,Height,Style, or connector-specific properties.
Attempting to access type-specific properties without casting causes CS1061 (member not found).⚠️ `DragStartEventArgs` is ambiguous whenSyncfusion.Blazor.Popups(or other packages that exposeDragStartEventArgs) is also referenced.
Always qualify it as Syncfusion.Blazor.Diagram.DragStartEventArgs:```csharp
// ❌ Wrong — CS0104: ambiguous reference between Diagram and Popups
private void OnDragStart(DragStartEventArgs args) { }
>
// ✅ Correct — fully qualified
private void OnDragStart(Syncfusion.Blazor.Diagram.DragStartEventArgs args) { }
```
⚠️ `DragEnterEventArgs` does NOT exist in Syncfusion.Blazor.Diagram.There is no `DragEnter` event onSfDiagramComponentthat receives aDragEnterEventArgs.
The available drag events onSfDiagramComponentare:DragStart,Dragging,DragLeave,DragDrop— all for SymbolPalette drag-and-drop only.
For tracking when a node is being moved (internal drag), use PositionChanged.// ❌ Wrong — DragEnterEventArgs does not exist
private void OnDragEnter(DragEnterEventArgs args) { }
// ❌ Wrong — OnPositionChange does not exist on SfDiagramComponent
<SfDiagramComponent OnPositionChange="OnPositionChange" />
// ✅ Correct — use PositionChanged
<SfDiagramComponent PositionChanged="OnPositionChanged" />
private void OnPositionChanged(PositionChangedEventArgs args)
{
if (args.Element is Node n)
Console.WriteLine($"Node {n.ID} moved to ({n.OffsetX}, {n.OffsetY})");
}---
User Handle Events
FixedUserHandleClick
Fires when a fixed user handle (custom action button) on a selected element is clicked. The event argument contains the clicked handle and the element it belongs to:
<SfDiagramComponent FixedUserHandleClick="OnFixedUserHandleClick">
<DiagramSelectionSettings>
<!-- User handles configured here -->
</DiagramSelectionSettings>
</SfDiagramComponent>
@code {
private void OnFixedUserHandleClick(FixedUserHandleClickEventArgs args)
{
// ⚠️ args.Element is IDiagramObject? — you must cast to Node or Connector
// args.Element — IDiagramObject? — the node or connector with the clicked handle
// args.FixedUserHandle — FixedUserHandle? — the handle that was clicked
if (args.Element is Node node)
{
// ✅ Now you can access Node-specific properties
Console.WriteLine($"User handle clicked on node: {node.ID}");
Console.WriteLine($" Handle name: {args.FixedUserHandle?.Name}");
Console.WriteLine($" Node label: {node.Annotations?[0]?.Content}");
// Perform custom action based on handle name
if (args.FixedUserHandle?.Name == "clone")
{
// Clone the node
Console.WriteLine(" Cloning node...");
}
else if (args.FixedUserHandle?.Name == "delete")
{
// Delete the node
Console.WriteLine(" Deleting node...");
}
}
else if (args.Element is Connector connector)
{
// ✅ Now you can access Connector-specific properties
Console.WriteLine($"User handle clicked on connector: {connector.ID}");
Console.WriteLine($" Handle name: {args.FixedUserHandle?.Name}");
Console.WriteLine($" Connects {connector.SourceID} to {connector.TargetID}");
}
}
}⚠️ Critical:args.ElementisIDiagramObject?, notNodeorConnectordirectly.
You must cast to access type-specific properties likeID,Annotations, orSourceID.
Attempting to access type-specific properties without casting causes CS1061 (member not found).---
History (Undo/Redo) Events
HistoryChanged
Fires after any undo/redo action:
<SfDiagramComponent HistoryChanged="OnHistoryChanged" />
@code {
private void OnHistoryChanged(HistoryChangedEventArgs args)
{
// args.Action — HistoryChangedAction (Undo, Redo, etc.)
// args.RedoStack / UndoStack — stacks after the change
}
}---
Auto-Scroll Events
OnAutoScrollChange
Fires when auto-scroll activates as an element is dragged near the canvas edge:
<SfDiagramComponent OnAutoScrollChange="OnAutoScrollChange">
<ScrollSettings EnableAutoScroll="true" />
</SfDiagramComponent>
@code {
private void OnAutoScrollChange(AutoScrollChangeEventArgs args)
{
args.Cancel = true; // stop auto-scroll
args.Delay = new TimeSpan(0, 0, 0, 1, 0); // delay before scrolling
}
}---
Events Quick-Reference Table
| Event | Fires When | Args Type | Has Cancel? |
|---|---|---|---|
Created | Diagram fully rendered | object | No |
NodeCreating | Each node is initialised | IDiagramObject | No |
ConnectorCreating | Each connector is initialised | IDiagramObject | No |
Click | Mouse click on element or canvas | ClickEventArgs | No |
KeyDown / KeyUp | Key pressed/released | KeyEventArgs | No |
MouseEnter | Pointer enters a node or connector | DiagramElementMouseEventArgs | No |
MouseLeave | Pointer exits a node or connector | DiagramElementMouseEventArgs | No |
MouseHover | Pointer hovers over a node or connector | DiagramElementMouseEventArgs | No |
PositionChanging / PositionChanged | Node/connector moved | PositionChangingEventArgs / PositionChangedEventArgs | Yes (Changing) |
SizeChanging / SizeChanged | Node resized | SizeChangingEventArgs / SizeChangedEventArgs | Yes (Changing) |
RotationChanging / RotationChanged | Node rotated | RotationChangingEventArgs / RotationChangedEventArgs | Yes (Changing) |
SelectionChanging / SelectionChanged | Selection changes | SelectionChangingEventArgs / SelectionChangedEventArgs | Yes (Changing) |
CollectionChanging / CollectionChanged | Node or connector added/removed | CollectionChangingEventArgs / CollectionChangedEventArgs | Yes (Changing) |
SourcePointChanging / SourcePointChanged | Connector source endpoint dragged | EndPointChangingEventArgs / EndPointChangedEventArgs | Yes (Changing) |
TargetPointChanging / TargetPointChanged | Connector target endpoint dragged | EndPointChangingEventArgs / EndPointChangedEventArgs | Yes (Changing) |
ConnectionChanging / ConnectionChanged | Connector reconnected to new node/port | ConnectionChangingEventArgs / ConnectionChangedEventArgs | Yes (Changing) |
SegmentCollectionChange | Connector segment collection modified | SegmentCollectionChangeEventArgs | Yes |
PropertyChanged | Node or connector property modified at runtime | PropertyChangedEventArgs | No |
TextChanged | Inline annotation editing completed | TextChangeEventArgs | No |
DragStart / Dragging / DragLeave / DragDrop | Symbol palette drag operations | Various | Yes (DragDrop) |
FixedUserHandleClick | Fixed user handle clicked | FixedUserHandleClickEventArgs | No |
HistoryChanged | Undo/Redo action | HistoryChangedEventArgs | No |
OnAutoScrollChange | Auto-scroll during drag | AutoScrollChangeEventArgs | Yes |
---
Common Gotchas
- ⚠️ `IDiagramObject` casting is required in MOST events — Many event arguments (PropertyChanged, DragStart, Dragging, DragLeave, DragDrop, PositionChanged, TextChanged, MouseEnter, MouseLeave, MouseHover, Click, KeyDown, KeyUp, FixedUserHandleClick) expose
Element,Target, orTargetsasIDiagramObject?, which is an interface, not a concrete type. To access type-specific properties likeNode.Annotations,Node.OffsetX, orConnector.SourceID, you MUST cast using pattern matching:if (args.Element is Node node) { var ann = node.Annotations; }. Attempting to access type-specific properties directly onIDiagramObjectcausesCS1061("member not found on interface type"). See each event section for complete casting examples. - `Created` fires once — do not register one-time initialization logic in
OnAfterRenderAsyncif it depends on fully rendered nodes; useCreatedinstead - `Changing` events with `Cancel = true` block the action without triggering the corresponding
Changedevent - There is NO `OnDoubleClick` event — double-clicks are detected via the
Clickevent by readingargs.Countas anintand comparing it:int c = args.Count; if (c == 2) { ... } - `DoubleClickEventArgs` does NOT exist — attempting to use it will cause a compile error; use
Syncfusion.Blazor.Diagram.ClickEventArgsfrom theClickevent instead - The correct event attribute name is `Click` (not
Clicked) when binding in markup — usingClickedcausesInvalidOperationException: does not have a property matching the name 'Clicked' - `args.Count` is a method/property returning `int` — comparing it directly with
== intinline without storing it first causesCS0019("method group" error); always store:int count = args.Count; - `ClickEventArgs` is ambiguous when multiple Syncfusion packages are used — qualify it as
Syncfusion.Blazor.Diagram.ClickEventArgs - `SelectionChangedEventArgs` is ambiguous when
Syncfusion.Blazor.Buttonsis referenced — qualify it asSyncfusion.Blazor.Diagram.SelectionChangedEventArgs - `SelectionChangedEventArgs.NewValue` is `ObservableCollection<IDiagramObject>?` — it is a nullable collection of
IDiagramObject, NOT aDiagramSelectionSettings. Iterate it withforeachand pattern-match each element asNodeorConnector. Also available:OldValue(previously selected items),Type(CollectionChangedAction—ObjectAdded/ObjectRemoved), andActionTrigger(DiagramAction— the cause of the change) - `IDiagramObject` items in `SelectionChangedEventArgs.NewValue` MUST be cast to access type-specific properties —
IDiagramObjectis an interface; to accessNodeproperties likeAnnotations,OffsetX, orConnectorproperties likeSourceID, pattern-match first:if (item is Node node) { var annotations = node.Annotations; }. Attempting to access type-specific properties without casting causesCS1061(member not found) - `SizeChangedEventArgs.Element` is also `DiagramSelectionSettings`, not
Node—args.Element is Node ncausesCS8121. Cast it correctly:if (args.Element is DiagramSelectionSettings sel && sel.Nodes.Count > 0) { var node = sel.Nodes[0]; } - `CollectionChangedEventArgs.Element` is typed as `NodeBase?` — never cast it directly to
Node; useis Node n/is Connector cpattern matching.Action(CollectionChangedAction) tells you whether the element was added or removed;ActionTrigger(DiagramAction) tells you what caused the change - `DragEnterEventArgs` does NOT exist — there is no
DragEnterevent onSfDiagramComponent; usePositionChangedto track node movement - `OnPositionChange` does NOT exist as an event attribute on
SfDiagramComponent— using it causesInvalidOperationException: does not have a property matching the name 'OnPositionChange'. The correct attribute name isPositionChanged - `PositionChangedEventArgs.NewValue` and `OldValue` are `DiagramSelectionSettings?`, not
Node— they expose the selector bounding box (OffsetX,OffsetY,Width,Height), not the individual node center. To get the moved node's position, pattern-matchargs.Element is Node nand readn.OffsetX/n.OffsetYdirectly. Always null-check:args.NewValue?.OffsetX - `PositionChangedEventArgs.Element` is `IDiagramObject?` — pattern-match as
NodeorConnectorbefore accessing type-specific members; accessing it without a null check or cast causesNullReferenceException - `ConnectionChangedEventArgs.NewValue` and `OldValue` are `ConnectionObject?`, not
Node— they carry onlyNodeIDandPortIDstrings identifying the connected node/port. Do not attempt to readOffsetX/OffsetYfrom them. UseConnectorAction(DiagramElementAction) to distinguish whether the source or target endpoint was moved, and null-check before reading:args.NewValue?.NodeID - `ConnectionChangedEventArgs.Connector` is `Connector?` — null-check before accessing it; it holds the connector whose endpoint changed, not the node
- `DragDrop` / `DragLeave` / `DragStart` / `Dragging` events only fire for SymbolPalette symbols — internal node moves use
PositionChanged - `args.NewValue.Width` and `args.NewValue.Height` in `SizeChangedEventArgs` are plain `double` (not
double?) — assign them directly:double w = args.NewValue.Width;. Using??on them causesCS0019("operator??cannot be applied todoubleandint") - `HistoryChanged` fires for both user actions and programmatic changes — check
args.Actionto differentiate - `TextChangedEventArgs` does NOT exist — causes
CS0246. The correct type is `TextChangeEventArgs` (nod):private void OnTextChanged(TextChangeEventArgs args) { } - `DragStartEventArgs` is ambiguous when
Syncfusion.Blazor.Popupsis also referenced — causesCS0104. Always qualify:Syncfusion.Blazor.Diagram.DragStartEventArgs - `NodeCreating` and `ConnectorCreating` parameter type is `IDiagramObject` — not
NodeorConnector. Always cast:if (obj is Node node) { ... }. Accessing.Styledirectly onIDiagramObjectcausesCS1061 - `MouseEnter`, `MouseLeave`, `MouseHover` all use `DiagramElementMouseEventArgs` — access the hovered element via
args.Elementand pattern-match asNodeorConnector - `SourcePointChanging`/`TargetPointChanging` use `EndPointChangingEventArgs`, not
PositionChangingEventArgs— these events fire only when the connector endpoint itself is dragged, not when the whole connector moves - `CollectionChanging` is the cancellable counterpart to `CollectionChanged` — set
args.Cancel = trueinCollectionChangingto block the add/remove.CollectionChangedhas no cancel - `SegmentCollectionChange` is cancellable — set
args.Cancel = trueto block segment modifications; castargs.ElementasConnectorto identify which connector changed - `PropertyChanged` fires for programmatic changes as well as user interactions — check
args.Element is Nodeorargs.Element is Connectorto identify what changed