Syncfusion AI Assistant

How can I help you?

SignalR hub configuration in ASP.NET MVC application

18 Mar 202621 minutes to read

Overview

This guide explains how to configure SignalR Hub in an ASP.NET MVC application for real-time collaborative diagram editing.

Prerequisites

How to create ASP.NET MVC application

To create an ASP.NET MVC application, follow the steps outlined in the ASP.NET MVC Getting Started documentation.

How to add packages in the ASP.NET MVC application

Open the NuGet Package Manager and install the following packages.

  • Microsoft.AspNetCore.SignalR.Client

  • Syncfusion.EJ2.MVC5

Configure SignalR service in ASP.NET MVC application

To enable real-time collaboration, configure SignalR HubConnection in your ASP.NET MVC view/controller as follows:

  • Initialize the HubConnection and start it using start().
  • Connect to the /diagramHub endpoint with WebSocket transport skipNegotiation: true and enable automatic reconnect to handle transient network issues.
  • Subscribe to the OnConnectedAsync callback to receive the unique connection ID, confirming a successful handshake with the server.
  • Join a SignalR group by calling JoinDiagram(roomName) after connecting. This ensures updates are shared only with users in the same diagram session.
  • Refer to Create ASP.NET MVC Simple Diagram
<div class="col-lg-12 control-section">
    <div class="content-wrapper">
        @Html.EJS().Diagram("diagram").Width("100%").Height("700px").EnableCollaborativeEditing(true).SnapSettings(se => se.Constraints(Syncfusion.EJ2.Diagrams.SnapConstraints.None)).ScrollSettings(s => s.ScrollLimit(Syncfusion.EJ2.Diagrams.ScrollLimit.Infinity)).HistoryChange("onHistoryChange").Render()
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@latest/signalr.js"></script>

<script>
    let connection = null;
    let roomName = 'Syncfusion';

    async function initializeSignalRConnection() {
        if (connection === null) {
            // Create connection
            connection = new signalR.HubConnectionBuilder()
                .withUrl('<<Your ServiceURL>>', {
                    skipNegotiation: true,
                    transport: signalR.HttpTransportType.WebSockets
                })
                .withAutomaticReconnect()
                .build();

            // Triggered when the connection to the SignalR Hub is successfully established
            connection.on('OnConnectedAsync', (id) => {
                onConnectedAsync(id);
            });

            try {
                await connection.start();
                console.log('Connected to SignalR Hub');
            } catch (error) {
                console.error('Connection failed:', error);
            }
        }
    }

    function onConnectedAsync(id) {
        if (id && id.length > 0) {
            console.log('Connection ID:', id);
            // Join the room after connection is established
            connection.invoke('JoinDiagram', roomName)
                .catch((error) => {
                    console.error('JoinDiagram failed:', error);
                });
        }
    }

    // Initialize connection when page loads
    document.addEventListener('DOMContentLoaded', initializeSignalRConnection);

    // Stop connection when page unloads
    window.addEventListener('beforeunload', () => {
        if (connection && connection.state === signalR.HubConnectionState.Connected) {
            connection.stop();
        }
    });
</script>

Notes:

  • Use a unique roomName per diagram (e.g., a diagram ID) to isolate sessions.
  • If WebSockets may be unavailable, remove SkipNegotiation so SignalR can fall back to SSE or Long Polling.
  • Consider handling reconnecting/disconnected states in the UI and securing the connection with authentication, if required.
  • For ASP.NET MVC, place this script in your shared layout or specific view where the diagram is hosted.

Sending and applying real-time diagram changes

  • The ASP.NET MVC Diagram component triggers the historyChange event whenever the diagram is modified (e.g., add, delete, move, resize, or edit nodes/connectors).
  • Use getDiagramUpdates to produce a compact set of incremental updates (JSON-formatted changes) representing just the changes, not the entire diagram.
  • Send these changes to the hub method BroadcastToOtherUsers, which relays them to all users joined to the same SignalR group (room).
  • Each remote user listens for ReceiveData and applies the incoming changes with setDiagramUpdates, keeping their view synchronized without reloading the full diagram.
  • Enable the enableCollaborativeEditing property on the diagram to treat multi-step edits (like drag/resize sequences or batch changes) as a single operation.
<div class="col-lg-12 control-section">
    <div class="content-wrapper">
        @Html.EJS().Diagram("diagram").Width("100%").Height("700px").EnableCollaborativeEditing(true).SnapSettings(se => se.Constraints(Syncfusion.EJ2.Diagrams.SnapConstraints.None)).ScrollSettings(s => s.ScrollLimit(Syncfusion.EJ2.Diagrams.ScrollLimit.Infinity)).HistoryChange("onHistoryChange").Render()
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@latest/signalr.js"></script>

<script>
    let diagramInstance;
    let connection = null;
    let roomName = 'Syncfusion';

    // Create diagram when page loads
    document.addEventListener('DOMContentLoaded', function() {
        diagramInstance = document.getElementById("diagram").ej2_instances[0];
        initializeSignalRConnection();
    });

    async function initializeSignalRConnection() {
        if (connection === null) {
            connection = new signalR.HubConnectionBuilder()
                .withUrl('<<Your ServiceURL>>', {
                    skipNegotiation: true,
                    transport: signalR.HttpTransportType.WebSockets
                })
                .withAutomaticReconnect()
                .build();

            // Listen for remote changes from other users
            connection.on('ReceiveData', (diagramChanges) => {
                if (diagramChanges && diagramChanges.length > 0) {
                    if (diagramInstance && diagramInstance.setDiagramUpdates) {
                        diagramInstance.setDiagramUpdates(diagramChanges);
                    }
                }
            });

            try {
                await connection.start();
                console.log('Connected to SignalR Hub');
                // Join the room after connection is established
                connection.invoke('JoinDiagram', roomName)
                    .catch((error) => {
                        console.error('JoinDiagram failed:', error);
                    });
            } catch (error) {
                console.error('Connection failed:', error);
                setTimeout(initializeSignalRConnection, 5000);
            }
        }
    }

    function onHistoryChange(args) {
        if (args && diagramInstance && diagramInstance.getDiagramUpdates) {
            // Get diagram updates (incremental changes) and send to hub
            const diagramChanges = diagramInstance.getDiagramUpdates(args);
            
            // When enableCollaborativeEditing is enabled, retrieve diagramChanges only after the group action completes.
            if (diagramChanges && diagramChanges.length > 0) {
                // Send changes to the SignalR Hub for broadcasting
                if (connection && connection.state === signalR.HubConnectionState.Connected) {
                    connection.invoke('BroadcastToOtherUsers', diagramChanges, roomName)
                        .catch((err) => {
                            console.error('Send failed:', err);
                        });
                }
            }
        }
    }

    // Stop connection when page unloads
    window.addEventListener('beforeunload', () => {
        if (connection && connection.state === signalR.HubConnectionState.Connected) {
            connection.stop();
        }
    });
</script>

Conflict policy (optimistic concurrency) in ASP.NET MVC application

To maintain consistency during collaborative editing, each user applies incoming changes using setDiagramUpdates. After applying changes, the ASP.NET MVC sample synchronizes its userVersion with the serverVersion through the UpdateVersion event. This version-based approach ensures conflicts are resolved without locking, allowing real-time responsiveness while preserving data integrity.

Add the following code in the ASP.NET MVC application:

<div class="col-lg-12 control-section">
    <div class="content-wrapper">
        @Html.EJS().Diagram("diagram").Width("100%").Height("700px").EnableCollaborativeEditing(true).SnapSettings(se => se.Constraints(Syncfusion.EJ2.Diagrams.SnapConstraints.None)).ScrollSettings(s => s.ScrollLimit(Syncfusion.EJ2.Diagrams.ScrollLimit.Infinity)).HistoryChange("onHistoryChange").Render()
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@latest/signalr.js"></script>

<script>
    let diagramInstance;
    let connection = null;
    let roomName = 'Syncfusion';
    let userVersion = 0;

    // Create diagram when page loads
    document.addEventListener('DOMContentLoaded', function() {
        diagramInstance = document.getElementById("diagram").ej2_instances[0];
        initializeSignalRConnection();
    });

    async function initializeSignalRConnection() {
        if (connection === null) {
            connection = new signalR.HubConnectionBuilder()
                .withUrl('<<Your ServiceURL>>', {
                    skipNegotiation: true,
                    transport: signalR.HttpTransportType.WebSockets
                })
                .withAutomaticReconnect()
                .build();

            // Listen for remote changes with version tracking
            connection.on('ReceiveData', (diagramChanges, serverVersion) => {
                applyRemoteDiagramChanges(diagramChanges, serverVersion);
            });

            // Listen for conflict notifications
            connection.on('ShowConflict', () => {
                showConflict();
            });

            // Listen for explicit version updates
            connection.on('UpdateVersion', (serverVersion) => {
                updateVersion(serverVersion);
            });

            try {
                await connection.start();
                console.log('Connected to SignalR Hub');
                // Join the room after connection is established
                connection.invoke('JoinDiagram', roomName)
                    .catch((error) => {
                        console.error('JoinDiagram failed:', error);
                    });
            } catch (error) {
                console.error('Connection failed:', error);
                setTimeout(initializeSignalRConnection, 5000);
            }
        }
    }

    function applyRemoteDiagramChanges(diagramChanges, serverVersion) {
        // Sets diagram updates to current diagram
        if (diagramInstance && diagramInstance.setDiagramUpdates) {
            diagramInstance.setDiagramUpdates(diagramChanges);
        }
        // Update user version to server version after applying changes
        userVersion = serverVersion;
    }

    // Capture local changes and send with version and edited IDs
    function onHistoryChange(args) {
        if (!diagramInstance) {
            return;
        }

        const diagramChanges = diagramInstance.getDiagramUpdates(args);
        if (diagramChanges && diagramChanges.length > 0) {
            const editedElements = getEditedElements(args);
            // Send changes with version and edited element IDs
            if (connection && connection.state === signalR.HubConnectionState.Connected) {
                connection.invoke('BroadcastToOtherUsers', diagramChanges, userVersion, editedElements, roomName)
                    .catch((err) => {
                        console.error('Send failed:', err);
                    });
            }
        }
    }

    function getEditedElements(args) {
        const editedElements = [];
        // Extract and return IDs of affected nodes/connectors from args
        // TODO: implement extraction logic based on historyChange event args
        return editedElements;
    }

    function updateVersion(serverVersion) {
        userVersion = serverVersion;
    }

    function showConflict() {
        // Show notification to inform user their update was rejected due to conflict
        const message = "Your changes conflicted with another user's updates and were not applied. Please refresh to see the latest version.";
        alert(message);
    }

    // Stop connection when page unloads
    window.addEventListener('beforeunload', () => {
        if (connection && connection.state === signalR.HubConnectionState.Connected) {
            connection.stop();
        }
    });
</script>