FlowState 0.0.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package FlowState --version 0.0.2
                    
NuGet\Install-Package FlowState -Version 0.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FlowState" Version="0.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FlowState" Version="0.0.2" />
                    
Directory.Packages.props
<PackageReference Include="FlowState" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FlowState --version 0.0.2
                    
#r "nuget: FlowState, 0.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FlowState@0.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FlowState&version=0.0.2
                    
Install as a Cake Addin
#tool nuget:?package=FlowState&version=0.0.2
                    
Install as a Cake Tool

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" />

FlowState Banner License

✨ 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(
    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>

📚 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

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;
}

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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