FlowState 0.0.5
See the version list below for details.
dotnet add package FlowState --version 0.0.5
NuGet\Install-Package FlowState -Version 0.0.5
<PackageReference Include="FlowState" Version="0.0.5" />
<PackageVersion Include="FlowState" Version="0.0.5" />
<PackageReference Include="FlowState" />
paket add FlowState --version 0.0.5
#r "nuget: FlowState, 0.0.5"
#:package FlowState@0.0.5
#addin nuget:?package=FlowState&version=0.0.5
#tool nuget:?package=FlowState&version=0.0.5
FlowState
A modern, high-performance node-based visual programming library for Blazor applications. Build interactive flow-based editors with custom nodes, real-time execution, and a beautiful theme UI.
<img width="1217" height="587" alt="Image" src="https://github.com/user-attachments/assets/57d6fecb-5d84-4f17-ad90-cee3cf881f48" />
⨠Features
- đ¨ Fully Customizable UI - Complete control over styles, colors, and appearance
- đ High Performance - Optimized for large graphs with hundreds of nodes
- đ Custom Nodes - Easily create your own node types including group node with full Blazor component support
- đ¯ Type-Safe Connections - Automatic type checking and conversion for socket connections
- đ Visual Execution Flow - Real-time visualization of node execution with progress indicators
- đąī¸ Intuitive Interactions - Pan, zoom, drag, select, and connect with familiar gestures
- đž Serialization - Save and load graphs with full state preservation
- đ Read-Only Mode - Lock graphs for viewing without editing
- âŠī¸ Undo/Redo - Full command pattern implementation with unlimited undo/redo history
Easy Customize with with html and css
đĻ Installation
NuGet Package
dotnet add package FlowState
From Source
git clone https://github.com/yourusername/FlowState.git
cd FlowState
dotnet build
đ Quick Start
1. Add to your Blazor page
@page "/flow-editor"
@using FlowState.Components
@using FlowState.Models
<FlowCanvas @ref="canvas"
Height="100vh"
Width="100vw"
Graph="graph">
<BackgroundContent>
<FlowBackground class="custom-grid"/>
</BackgroundContent>
</FlowCanvas>
@code {
private FlowCanvas? canvas;
private FlowGraph graph = new();
protected override void OnInitialized()
{
// Register your custom node types
graph.RegisterNode<MyCustomNode>();
}
}
2. Style your canvas
.custom-grid {
background: #111827;
background-image:
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px);
background-size: 100px 100px;
}
đ¯ Creating Custom Nodes
Basic Node Example
Create a custom node by inheriting from FlowNodeBase:
// MyCustomNode.razor.cs
using FlowState.Attributes;
using FlowState.Components;
using FlowState.Models.Execution;
using Microsoft.AspNetCore.Components;
[FlowNodeMetadata(
Category = "Math",
Title = "Double Value",
Description = "Doubles the input value",
Icon = "đĸ",
Order = 1)]
public partial class MyCustomNode : FlowNodeBase
{
[Parameter]
public int Value { get; set; } = 0;
public override async ValueTask ExecuteAsync(FlowExecutionContext context)
{
// Your execution logic here
var result = Value * 2;
context.SetOutputSocketData("Output", result);
await Task.CompletedTask;
}
}
@using FlowState.Components
@using FlowState.Models
@inherits FlowNodeBase
<FlowNode>
<div class="title">đĸ My Node</div>
<div class="body">
<input type="number" @bind="Value" />
<FlowSocket Name="Output"
Label="Result"
Type="SocketType.Output"
T="typeof(int)"
OuterColor="#4CAF50"
InnerColor="#8BC34A"/>
</div>
</FlowNode>
Advanced Node with Multiple Sockets
// SumNode.razor.cs
[FlowNodeMetadata(
Category = "Math",
Title = "Add Numbers",
Description = "Adds two numbers together",
Icon = "â",
Order = 2)]
public partial class SumNode : FlowNodeBase
{
public override async ValueTask ExecuteAsync(FlowExecutionContext context)
{
var a = context.GetInputSocketData<float>("InputA");
var b = context.GetInputSocketData<float>("InputB");
var sum = a + b;
context.SetOutputSocketData("Output", sum);
await Task.CompletedTask;
}
}
@using FlowState.Components
@using FlowState.Models
@inherits FlowNodeBase
<FlowNode>
<div class="title">â Sum</div>
<div class="body">
<FlowSocket Name="InputA" Label="A" Type="SocketType.Input" T="typeof(float)"/>
<FlowSocket Name="InputB" Label="B" Type="SocketType.Input" T="typeof(float)"/>
<FlowSocket Name="Output" Label="Sum" Type="SocketType.Output" T="typeof(float)"/>
</div>
</FlowNode>
đ Example
Here's a full working example with multiple node types:
@page "/editor"
@using FlowState.Components
@using FlowState.Models
@using FlowState.Models.Events
<div style="display: flex; gap: 10px; padding: 10px;">
<button @onclick="ExecuteGraph">âļī¸ Execute</button>
<button @onclick="SaveGraph">đž Save</button>
<button @onclick="LoadGraph">đ Load</button>
<button @onclick="ClearGraph">đī¸ Clear</button>
</div>
<FlowCanvas @ref="canvas"
Height="calc(100vh - 60px)"
Width="100vw"
Graph="graph"
OnCanvasLoaded="OnLoaded">
<BackgroundContent>
<FlowBackground class="flow-grid"/>
</BackgroundContent>
</FlowCanvas>
@code {
private FlowCanvas? canvas;
private FlowGraph graph = new();
private string savedData = "{}";
protected override void OnInitialized()
{
// Register all your custom nodes
graph.RegisterNode<NumberInputNode>();
graph.RegisterNode<SumNode>();
graph.RegisterNode<DisplayNode>();
// Register type conversions if needed
graph.TypeCompatibiltyRegistry.Register<float>(typeof(int));
}
private async Task OnLoaded()
{
// Create initial nodes programmatically
var input1 = await graph.CreateNodeAsync<NumberInputNode>(100, 100, new());
var input2 = await graph.CreateNodeAsync<NumberInputNode>(100, 200, new());
var sum = await graph.CreateNodeAsync<SumNode>(400, 150, new());
var display = await graph.CreateNodeAsync<DisplayNode>(700, 150, new());
await Task.Delay(100); // Wait for DOM
// Connect nodes
await graph.ConnectAsync(input1.Id, sum.Id, "Output", "InputA");
await graph.ConnectAsync(input2.Id, sum.Id, "Output", "InputB");
await graph.ConnectAsync(sum.Id, display.Id, "Output", "Input");
}
private async Task ExecuteGraph()
{
await graph.ExecuteAsync();
}
private async Task SaveGraph()
{
savedData = await graph.SerializeAsync();
Console.WriteLine("Graph saved!");
}
private async Task LoadGraph()
{
await graph.DeserializeAsync(savedData);
Console.WriteLine("Graph loaded!");
}
private async Task ClearGraph()
{
await graph.ClearAsync();
}
}
<style>
.flow-grid {
background: #111827;
background-image:
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px);
background-size: 100px 100px;
}
.flow-node .title {
font-weight: 600;
font-size: 14px;
margin-bottom: 8px;
color: white;
padding: 12px 16px 8px;
background: linear-gradient(90deg, rgba(124,58,237,0.1), transparent);
border-bottom: 1px solid rgba(255,255,255,0.05);
border-radius: 12px 12px 0 0;
}
# play with look of your nodes
.flow-node .body {
font-size: 13px;
color: #cbd5e1;
padding: 12px 16px;
}
.flow-node {
position: absolute;
min-width: 160px;
border-radius: 12px;
background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
border: 1px solid rgba(255,255,255,0.05);
box-shadow:
0 8px 32px rgba(2,6,23,0.6),
inset 0 1px 0 rgba(255,255,255,0.05);
transform-origin: 0 0;
user-select: none;
cursor: grab;
backdrop-filter: blur(8px);
/* PERFORMANCE OPTIMIZATIONS */
/* GPU acceleration with proper text rendering */
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
/* Text rendering optimizations - prevents blur during zoom */
-webkit-font-smoothing: subpixel-antialiased;
-moz-osx-font-smoothing: auto;
text-rendering: geometricPrecision;
/* Force subpixel precision for crisp text at any zoom level */
-webkit-transform: translate3d(0, 0, 0);
-webkit-perspective: 1000;
perspective: 1000;
/* CSS containment for better rendering performance */
contain: layout style paint;
/* Prevent layout thrashing */
isolation: isolate;
}
</style>
đ¨ Node Styling
Customize your nodes with CSS:
.flow-node {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 12px;
min-width: 200px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.flow-node .title {
font-weight: 600;
color: white;
margin-bottom: 8px;
}
.flow-node .body {
display: flex;
flex-direction: column;
gap: 8px;
}
đ Socket Types and Colors
<FlowSocket Name="Input"
Label="Value"
Type="SocketType.Input"
T="typeof(float)"
OuterColor="#2196F3"
InnerColor="#64B5F6"/>
<FlowSocket Name="Output"
Label="Result"
Type="SocketType.Output"
T="typeof(float)"
OuterColor="#4CAF50"
InnerColor="#81C784"/>
âī¸ Configuration Options
FlowCanvas Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
Graph |
FlowGraph |
Required | The graph data model |
Height |
string |
"100%" |
Canvas height (CSS value) |
Width |
string |
"100%" |
Canvas width (CSS value) |
CanZoom |
bool |
true |
Enable zoom with mouse wheel |
CanPan |
bool |
true |
Enable panning |
IsReadOnly |
bool |
false |
Lock graph for viewing only |
MinZoom |
double |
0.2 |
Minimum zoom level |
MaxZoom |
double |
2.0 |
Maximum zoom level |
PanKey |
string |
"alt" |
Key for panning (alt/shift/ctrl/meta) |
NodeSelectionClass |
string |
"selected" |
CSS class for selected nodes |
AutoUpdateSocketColors |
bool |
false |
Auto-color edges based on socket |
đ API Reference
FlowGraph Methods
Node Management
// Create node - Generic (recommended)
NodeInfo node = await graph.CreateNodeAsync<MyNodeType>(x, y, data);
// Create node - By Type
NodeInfo node = await graph.CreateNodeAsync(typeof(MyNodeType), x, y, data);
// Create node - By string type name
NodeInfo node = await graph.CreateNodeAsync("MyNamespace.MyNodeType", x, y, data);
// Optional: suppress event firing
NodeInfo node = await graph.CreateNodeAsync<MyNodeType>(x, y, data, supressEvent: true);
// Remove node
graph.RemoveNode(nodeId);
// Get node by ID
FlowNodeBase? node = graph.GetNodeById(nodeId);
Edge Management
// Connect by node IDs and socket names
(EdgeInfo? edge, string? error) = await graph.ConnectAsync(fromNodeId, toNodeId, "OutputSocket", "InputSocket");
// Connect by socket references
(EdgeInfo? edge, string? error) = await graph.ConnectAsync(fromSocket, toSocket);
// Optional: enable type checking
(EdgeInfo? edge, string? error) = await graph.ConnectAsync(fromNodeId, toNodeId, "Output", "Input", checkDataType: true);
// Remove edge
graph.RemoveEdge(edgeId);
Execution
// Execute the entire graph
await graph.ExecuteAsync();
Serialization
// Save graph to JSON (includes all node [Parameter] properties)
string json = await graph.SerializeAsync();
// Load graph from JSON (restores all node parameters)
await graph.DeserializeAsync(json);
// Clear entire graph
await graph.ClearAsync();
Note: All
[Parameter]properties in your custom nodes are automatically serialized and restored. Node positions, connections, and parameter values are preserved.
Registration
// Register node type
graph.RegisterNode<MyNodeType>();
// Register type conversion (source â target)
graph.TypeCompatibiltyRegistry.Register<float>(typeof(int)); // int can connect to float
FlowNodeBase Lifecycle
public class MyNode : FlowNodeBase
{
// Called before graph execution starts
public override ValueTask BeforeGraphExecutionAsync()
{
// Reset state, clear previous results
return ValueTask.CompletedTask;
}
// Main execution logic
public override async ValueTask ExecuteAsync(FlowExecutionContext context)
{
// Get input data
var input = context.GetInputSocketData<float>("InputName");
// Process data
var result = input * 2;
// Set output data
context.SetOutputSocketData("OutputName", result);
}
// Called after graph execution completes
public override ValueTask AfterGraphExecutionAsync()
{
// Cleanup, finalize
return ValueTask.CompletedTask;
}
}
đ¯ Events
Subscribe to graph events:
graph.NodeAdded += (sender, e) => Console.WriteLine($"Node added: {e.NodeId}");
graph.NodeRemoved += (sender, e) => Console.WriteLine($"Node removed: {e.NodeId}");
graph.EdgeAdded += (sender, e) => Console.WriteLine($"Edge added: {e.EdgeId}");
graph.EdgeRemoved += (sender, e) => Console.WriteLine($"Edge removed: {e.EdgeId}");
FlowCanvas Events
All available events with their parameters:
<FlowCanvas @ref="canvas"
Graph="graph"
OnCanvasLoaded="HandleCanvasLoaded"
OnPanned="HandlePanned"
OnZoomed="HandleZoomed"
OnNodeMoved="HandleNodeMoved"
OnNodeSelected="HandleNodeSelected"
OnNodeDeselected="HandleNodeDeselected"
OnSelectionChanged="HandleSelectionChanged"
OnNotifyNodesCleared="HandleNodesCleared"
OnEdgeConnectRequest="HandleEdgeConnectRequest"
OnSocketLongPress="HandleSocketLongPress"
OnContextMenu="HandleContextMenu"/>
Event Descriptions:
| Event | Args Type | Description |
|---|---|---|
OnCanvasLoaded |
CanvasLoadedEventArgs |
Fires when canvas finishes initial setup |
OnPanned |
PanEventArgs |
Fires when canvas is panned |
OnZoomed |
ZoomEventArgs |
Fires when zoom level changes |
OnNodeMoved |
NodeMovedArgs |
Fires when a node is moved |
OnNodeSelected |
NodeSelectedEventArgs |
Fires when a node is selected |
OnNodeDeselected |
NodeDeselectedEventArgs |
Fires when a node is deselected |
OnSelectionChanged |
SelectionChangedEventArgs |
Fires when selection changes (contains all selected nodes) |
OnNotifyNodesCleared |
NodesClearedEventArgs |
Fires when all nodes are cleared |
OnEdgeConnectRequest |
ConnectRequestArgs |
Fires when edge connection is requested |
OnSocketLongPress |
SocketLongPressEventArgs |
Fires when a socket is long-pressed (1 second) |
OnContextMenu |
CanvasContextMenuEventArgs |
Fires on canvas right-click with X, Y coordinates |
Example Event Handlers:
private void HandleCanvasLoaded(CanvasLoadedEventArgs e)
{
Console.WriteLine("Canvas is ready!");
}
private void HandleNodeMoved(NodeMovedArgs e)
{
Console.WriteLine($"Node {e.NodeId} moved to ({e.X}, {e.Y})");
}
private void HandleSelectionChanged(SelectionChangedEventArgs e)
{
Console.WriteLine($"Selected nodes: {string.Join(", ", e.SelectedNodeIds)}");
}
private void HandleSocketLongPress(SocketLongPressEventArgs e)
{
Console.WriteLine($"Socket {e.Socket.Name} long-pressed at ({e.X}, {e.Y})");
}
private void HandleContextMenu(CanvasContextMenuEventArgs e)
{
Console.WriteLine($"Right-click at canvas: ({e.X}, {e.Y}), client: ({e.ClientX}, {e.ClientY})");
}
đ§ Advanced Features
Context Menu for Adding Nodes
FlowState includes a built-in context menu component for adding nodes to the canvas:
<FlowCanvas @ref="canvas"
Graph="graph"
OnContextMenu="HandleContextMenu">
<BackgroundContent>
<FlowBackground/>
</BackgroundContent>
</FlowCanvas>
<FlowContextMenu @ref="contextMenu" Graph="graph" />
@code {
FlowCanvas? canvas;
FlowContextMenu? contextMenu;
FlowGraph graph = new();
private async Task HandleContextMenu(CanvasContextMenuEventArgs e)
{
if (contextMenu != null)
{
await contextMenu.ShowAsync(e.ClientX, e.ClientY, e.X, e.Y);
}
}
}
The context menu automatically displays all registered nodes grouped by category, with search functionality. Customize appearance using CSS variables:
:root {
--context-menu-bg: #0b1220;
--context-menu-border: #94a3b8;
--node-item-hover-bg: #7c3aed;
}
Undo/Redo
FlowState includes a built-in command manager that tracks all graph modifications and enables unlimited undo/redo operations:
Automatic Tracking
The following operations are automatically tracked:
- Node addition and removal
- Edge connection and disconnection
- Graph state changes (via StateSnapshotCommand)
Basic Usage
@code {
private FlowGraph graph = new();
// Undo the last action
private async Task Undo()
{
await graph.CommandManager.UndoAsync();
}
// Redo the last undone action
private async Task Redo()
{
await graph.CommandManager.RedoAsync();
}
}
Notes:
- Undo/Redo is automatically disabled in read-only mode
- The redo stack is cleared when new commands are executed after an undo
- Use
CommandManager.ClearStacks()to clear all undo/redo history
Type Conversion
By default, sockets can only connect if their types match exactly. Use type conversion to allow connections between different socket types:
// Allow int sockets to connect to float sockets
graph.TypeCompatibiltyRegistry.Register<float>(typeof(int));
// Allow int sockets to connect to string sockets
graph.TypeCompatibiltyRegistry.Register<string>(typeof(int));
// Now these connections work:
// OutputSocket<int> â InputSocket<float> â
// OutputSocket<int> â InputSocket<string> â
Special Case: object Type
Sockets with type object can connect to any socket type without registration:
// Create a universal socket that accepts any type
<FlowSocket Name="Input" Type="SocketType.Input" T="typeof(object)"/>
// This socket can now connect to:
// - OutputSocket<int> â
// - OutputSocket<string> â
// - OutputSocket<float> â
// - Any other type â
Example:
// Node A has: Output socket of type int
// Node B has: Input socket of type float
// Without type conversion: Connection fails â
// With type conversion: Connection succeeds â
graph.TypeCompatibiltyRegistry.Register<float>(typeof(int));
await graph.ConnectAsync(nodeA.Id, nodeB.Id, "IntOutput", "FloatInput"); // Now works!
Execution with Progress
public override async ValueTask ExecuteAsync(FlowExecutionContext context)
{
// Get input data
var input = context.GetInputSocketData<float>("Input");
// Process
var result = input * 2;
// Set output data
context.SetOutputSocketData("Output", result);
await Task.CompletedTask;
}
đ License
MIT License - See LICENSE for details
đ¤ Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Made with â¤ī¸
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.0-preview.6.25358.103)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.12-alpha | 45 | 11/9/2025 |
| 0.1.11-alpha | 40 | 11/9/2025 |
| 0.1.10-alpha | 66 | 11/1/2025 |
| 0.1.9-alpha | 110 | 10/26/2025 |
| 0.1.8-alpha | 84 | 10/25/2025 |
| 0.1.7-alpha | 44 | 10/25/2025 |
| 0.1.6-alpha | 45 | 10/25/2025 |
| 0.1.5-alpha | 56 | 10/25/2025 |
| 0.1.4-alpha | 54 | 10/25/2025 |
| 0.1.3-alpha | 62 | 10/24/2025 |
| 0.1.2-alpha | 69 | 10/24/2025 |
| 0.1.1-alpha | 70 | 10/24/2025 |
| 0.1.0-alpha | 97 | 10/24/2025 |
| 0.0.7 | 130 | 10/23/2025 |
| 0.0.6 | 125 | 10/23/2025 |
| 0.0.5 | 124 | 10/22/2025 |
| 0.0.4 | 118 | 10/21/2025 |
| 0.0.3 | 122 | 10/20/2025 |
| 0.0.2 | 117 | 10/19/2025 |
| 0.0.1 | 124 | 10/19/2025 |