From da8216b292c33b8f5a2e7eec301965d6421080cb Mon Sep 17 00:00:00 2001 From: Pierre Arnaud Date: Fri, 30 Jan 2026 13:09:55 +0100 Subject: [PATCH] Implement bidirectional event routing for SignalR mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../Bridge/SignalRFacade.cs | 16 +++- src/ElectronNET.AspNet/Hubs/ElectronHub.cs | 33 ++++++--- src/ElectronNET.Host/api/signalr-bridge.js | 73 ++++++++----------- src/ElectronNET.Host/main.js | 26 ++++++- 4 files changed, 87 insertions(+), 61 deletions(-) diff --git a/src/ElectronNET.AspNet/Bridge/SignalRFacade.cs b/src/ElectronNET.AspNet/Bridge/SignalRFacade.cs index 54216d9..d270d98 100644 --- a/src/ElectronNET.AspNet/Bridge/SignalRFacade.cs +++ b/src/ElectronNET.AspNet/Bridge/SignalRFacade.cs @@ -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() diff --git a/src/ElectronNET.AspNet/Hubs/ElectronHub.cs b/src/ElectronNET.AspNet/Hubs/ElectronHub.cs index c7e6e73..18c0962 100644 --- a/src/ElectronNET.AspNet/Hubs/ElectronHub.cs +++ b/src/ElectronNET.AspNet/Hubs/ElectronHub.cs @@ -60,6 +60,27 @@ namespace ElectronNET.AspNet.Hubs await Task.CompletedTask; } + /// + /// Receives events from Electron (e.g., "BrowserWindowCreated", "dialogResult"). + /// Called by Electron to send data back to .NET. + /// + /// The event name + /// The event arguments + 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; + } + /// /// Invokes an Electron API method. Called by .NET to control Electron. /// @@ -91,17 +112,5 @@ namespace ElectronNET.AspNet.Hubs await Task.CompletedTask; } - /// - /// Handles events from Electron. - /// Called by Electron to notify .NET about events (e.g., window closed). - /// - /// The event name - /// The event data as JSON - 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; - } } } diff --git a/src/ElectronNET.Host/api/signalr-bridge.js b/src/ElectronNET.Host/api/signalr-bridge.js index d3f631a..98b2e3e 100644 --- a/src/ElectronNET.Host/api/signalr-bridge.js +++ b/src/ElectronNET.Host/api/signalr-bridge.js @@ -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 }; diff --git a/src/ElectronNET.Host/main.js b/src/ElectronNET.Host/main.js index d7e5fc7..fa2e896 100644 --- a/src/ElectronNET.Host/main.js +++ b/src/ElectronNET.Host/main.js @@ -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');