FlowState 0.0.1
See the version list below for details.
dotnet add package FlowState --version 0.0.1
NuGet\Install-Package FlowState -Version 0.0.1
<PackageReference Include="FlowState" Version="0.0.1" />
<PackageVersion Include="FlowState" Version="0.0.1" />
<PackageReference Include="FlowState" />
paket add FlowState --version 0.0.1
#r "nuget: FlowState, 0.0.1"
#:package FlowState@0.0.1
#addin nuget:?package=FlowState&version=0.0.1
#tool nuget:?package=FlowState&version=0.0.1
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 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
📦 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("Math")] // Category for organization
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("Math")]
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>
📚 Complete 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 = graph.CreateNode<NumberInputNode>(100, 100, new());
var input2 = graph.CreateNode<NumberInputNode>(100, 200, new());
var sum = graph.CreateNode<SumNode>(400, 150, new());
var display = graph.CreateNode<DisplayNode>(700, 150, new());
await Task.Delay(100); // Wait for DOM
// Connect nodes
graph.Connect(input1.Id, sum.Id, "Output", "InputA");
graph.Connect(input2.Id, sum.Id, "Output", "InputB");
graph.Connect(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;
}
</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 = graph.CreateNode<MyNodeType>(x, y, data);
// Create node - By Type
NodeInfo node = graph.CreateNode(typeof(MyNodeType), x, y, data);
// Create node - By string type name
NodeInfo node = graph.CreateNode("MyNamespace.MyNodeType", x, y, data);
// Optional: suppress event firing
NodeInfo node = graph.CreateNode<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) = graph.Connect(fromNodeId, toNodeId, "OutputSocket", "InputSocket");
// Connect by socket references
(EdgeInfo? edge, string? error) = graph.Connect(fromSocket, toSocket);
// Optional: enable type checking
(EdgeInfo? edge, string? error) = graph.Connect(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
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));
graph.Connect(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 ❤️ for the Blazor community
| 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 | 16 | 11/9/2025 |
| 0.1.11-alpha | 20 | 11/9/2025 |
| 0.1.10-alpha | 65 | 11/1/2025 |
| 0.1.9-alpha | 110 | 10/26/2025 |
| 0.1.8-alpha | 84 | 10/25/2025 |
| 0.1.7-alpha | 43 | 10/25/2025 |
| 0.1.6-alpha | 45 | 10/25/2025 |
| 0.1.5-alpha | 55 | 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 | 95 | 10/24/2025 |
| 0.0.7 | 128 | 10/23/2025 |
| 0.0.6 | 123 | 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 |