Implement bidirectional event routing for SignalR mode

- Update signalr-bridge.js to handle .NET→Electron events via 'event' channel
- Add socket.io-compatible .on() and .emit() methods to SignalRBridge
- Update main.js to load all Electron API modules with SignalR bridge
- Update SignalRFacade.Emit() to send events via 'event' channel
- Add ElectronHub.ElectronEvent() to receive Electron→.NET events
- Add SignalRFacade.TriggerEvent() to invoke .NET event handlers
- Remove duplicate ElectronEvent method from hub

This enables full bidirectional communication:
- .NET can call Electron APIs via Emit (e.g., createBrowserWindow)
- Electron can send events back to .NET (e.g., BrowserWindowCreated)
- Event handlers registered via On/Once now work with SignalR
This commit is contained in:
Pierre Arnaud
2026-01-30 13:09:55 +01:00
parent be609a513e
commit da8216b292
4 changed files with 87 additions and 61 deletions

View File

@@ -116,8 +116,10 @@ namespace ElectronNET.API
try
{
// Send message to specific Electron client
await _hubContext.Clients.Client(_connectionId).SendAsync(eventName, args);
// Send message to specific Electron client via the 'event' hub method
// This will be received by signalr-bridge.js's connection.on('event', ...)
Console.WriteLine($"[SignalRFacade] Emitting event '{eventName}' to connection {_connectionId}");
await _hubContext.Clients.Client(_connectionId).SendAsync("event", eventName, args);
}
catch (Exception ex)
{
@@ -126,12 +128,20 @@ namespace ElectronNET.API
}
}
public void TriggerEvent(string eventName, object data)
public void TriggerEvent(string eventName, params object[] args)
{
Console.WriteLine($"[SignalRFacade] Triggering event '{eventName}' for .NET handlers");
if (_eventHandlers.TryGetValue(eventName, out var handler))
{
// If single arg, pass it directly; otherwise pass the array
var data = args.Length == 1 ? args[0] : args;
handler(data);
}
else
{
Console.WriteLine($"[SignalRFacade] No handler registered for event '{eventName}'");
}
}
public void DisposeSocket()

View File

@@ -60,6 +60,27 @@ namespace ElectronNET.AspNet.Hubs
await Task.CompletedTask;
}
/// <summary>
/// Receives events from Electron (e.g., "BrowserWindowCreated", "dialogResult").
/// Called by Electron to send data back to .NET.
/// </summary>
/// <param name="eventName">The event name</param>
/// <param name="args">The event arguments</param>
public async Task ElectronEvent(string eventName, params object[] args)
{
Console.WriteLine($"[ElectronHub] Received event from Electron: {eventName}");
// Get the SignalRFacade and trigger the event handlers
var runtimeController = ElectronNetRuntime.RuntimeController as RuntimeControllerAspNetDotnetFirstSignalR;
if (runtimeController?.Socket is ElectronNET.API.SignalRFacade signalRFacade)
{
// Invoke the event handlers registered via On/Once
signalRFacade.TriggerEvent(eventName, args);
}
await Task.CompletedTask;
}
/// <summary>
/// Invokes an Electron API method. Called by .NET to control Electron.
/// </summary>
@@ -91,17 +112,5 @@ namespace ElectronNET.AspNet.Hubs
await Task.CompletedTask;
}
/// <summary>
/// Handles events from Electron.
/// Called by Electron to notify .NET about events (e.g., window closed).
/// </summary>
/// <param name="eventName">The event name</param>
/// <param name="eventData">The event data as JSON</param>
public async Task ElectronEvent(string eventName, string eventData)
{
Console.WriteLine($"[ElectronHub] ElectronEvent received: {eventName}");
// This will be handled by the event system
await Task.CompletedTask;
}
}
}

View File

@@ -6,7 +6,7 @@ class SignalRBridge {
this.hubUrl = hubUrl;
this.connection = null;
this.isConnected = false;
this.pendingCalls = new Map(); // For tracking API calls
this.eventHandlers = new Map(); // For socket.io-style .on() handlers
this.callIdCounter = 0;
}
@@ -56,46 +56,46 @@ class SignalRBridge {
}
setupMessageHandlers() {
// Handle API calls from .NET
this.connection.on('electronApiCall', (method, data) => {
console.log(`[SignalRBridge] Received API call: ${method}`);
this.handleApiCall(method, data);
// Handle generic events from .NET - this is where .NET's Emit() calls arrive
this.connection.on('event', (eventName, ...args) => {
console.log(`[SignalRBridge] Received event: ${eventName}`);
// Check if we have handlers registered for this event
if (this.eventHandlers.has(eventName)) {
const handlers = this.eventHandlers.get(eventName);
handlers.forEach(handler => {
try {
handler(...args);
} catch (err) {
console.error(`[SignalRBridge] Error in event handler for ${eventName}:`, err);
}
});
}
});
}
async handleApiCall(method, data) {
// This will be implemented to route to the actual Electron API
// For now, just log it
console.log(`[SignalRBridge] Handling API call: ${method} with data:`, data);
// TODO: Route to actual Electron API handlers
// This will be connected to the existing API modules (browserWindows, dialog, etc.)
// Socket.io compatibility: register event handler
on(eventName, callback) {
if (!this.eventHandlers.has(eventName)) {
this.eventHandlers.set(eventName, []);
}
this.eventHandlers.get(eventName).push(callback);
console.log(`[SignalRBridge] Registered handler for event: ${eventName}`);
}
async invokeMethod(methodName, ...args) {
// Socket.io compatibility: emit event (send to .NET)
async emit(eventName, ...args) {
if (!this.isConnected) {
throw new Error('SignalR connection is not established');
}
try {
const result = await this.connection.invoke(methodName, ...args);
return result;
} catch (err) {
console.error(`[SignalRBridge] Error invoking ${methodName}:`, err);
throw err;
}
}
async sendElectronEvent(eventName, eventData) {
if (!this.isConnected) {
console.warn(`[SignalRBridge] Cannot send event - not connected`);
console.warn(`[SignalRBridge] Cannot emit ${eventName} - not connected`);
return;
}
try {
await this.connection.invoke('ElectronEvent', eventName, JSON.stringify(eventData));
console.log(`[SignalRBridge] Emitting event: ${eventName}`);
await this.connection.invoke('ElectronEvent', eventName, ...args);
} catch (err) {
console.error(`[SignalRBridge] Error sending event:`, err);
console.error(`[SignalRBridge] Error emitting ${eventName}:`, err);
throw err;
}
}
@@ -106,19 +106,6 @@ class SignalRBridge {
console.log(`[SignalRBridge] Disconnected`);
}
}
// Socket.io compatibility method - for easier transition
on(eventName, callback) {
if (this.connection) {
this.connection.on(eventName, callback);
}
}
// Socket.io compatibility method
emit(eventName, ...args) {
// Map to SignalR invoke
return this.invokeMethod(eventName, ...args);
}
}
module.exports = { SignalRBridge };

View File

@@ -427,11 +427,31 @@ async function startSignalRApiBridge(baseUrl) {
// Store the bridge globally for API access
global['electronsignalr'] = signalRBridge;
// Load API modules with SignalR bridge
// Load API modules with SignalR bridge (same as socket.io)
console.log('[SignalRBridge] Loading API components...');
// TODO: Load API modules adapted for SignalR
// For now, just log that we're connected
if (appApi === undefined) appApi = require('./api/app')(signalRBridge, app);
if (browserWindows === undefined) browserWindows = require('./api/browserWindows')(signalRBridge, app);
if (commandLine === undefined) commandLine = require('./api/commandLine')(signalRBridge, app);
if (autoUpdater === undefined) autoUpdater = require('./api/autoUpdater')(signalRBridge);
if (ipc === undefined) ipc = require('./api/ipc')(signalRBridge);
if (menu === undefined) menu = require('./api/menu')(signalRBridge);
if (dialogApi === undefined) dialogApi = require('./api/dialog')(signalRBridge);
if (notification === undefined) notification = require('./api/notification')(signalRBridge);
if (tray === undefined) tray = require('./api/tray')(signalRBridge);
if (webContents === undefined) webContents = require('./api/webContents')(signalRBridge);
if (globalShortcut === undefined) globalShortcut = require('./api/globalShortcut')(signalRBridge);
if (clipboard === undefined) clipboard = require('./api/clipboard')(signalRBridge);
if (screen === undefined) screen = require('./api/screen')(signalRBridge);
if (shell === undefined) shell = require('./api/shell')(signalRBridge);
if (nativeTheme === undefined) nativeTheme = require('./api/nativeTheme')(signalRBridge);
if (powerMonitor === undefined) powerMonitor = require('./api/powerMonitor')(signalRBridge);
if (dock === undefined) dock = require('./api/dock')(signalRBridge, app);
if (desktopCapturer === undefined) desktopCapturer = require('./api/desktopCapturer')(signalRBridge);
if (electronHostHook === undefined) electronHostHook = require('./api/hostHook')();
if (process.platform === 'darwin') {
if (touchBar === undefined) touchBar = require('./api/touchBar')(signalRBridge);
}
console.log('[SignalRBridge] Startup complete');