using System.Reflection; using LANCommander.SDK.Extensions; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.SignalR.Client; namespace LANCommander.UI.Components { /// /// Base component that connects to a SignalR hub and exposes the component /// as a strongly-typed client (THubClient). /// /// Derived components must: /// - Implement THubClient /// - Provide the HubUrl /// public abstract class RpcComponentBase : ComponentBase, IAsyncDisposable where THubClient : class where THub : class { private readonly List _clientHandlerSubscriptions = new(); protected HubConnection? HubConnection { get; private set; } protected THub? Hub { get; private set; } [Inject] protected NavigationManager NavigationManager { get; set; } = default!; /// /// Relative hub URL, e.g. "/hubs/notifications". /// protected abstract string HubUrl { get; } /// /// The component cast as the hub client interface. /// protected THubClient Client => (THubClient)(object)this; protected bool IsConnected => HubConnection?.State == HubConnectionState.Connected; protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); var absoluteHubUrl = NavigationManager.ToAbsoluteUri(HubUrl); var builder = new HubConnectionBuilder() .WithUrl(absoluteHubUrl) .WithAutomaticReconnect(); HubConnection = builder.Build(); Hub = HubConnection.ServerProxy(); WireClientInterfaceHandlers(HubConnection); HubConnection.Reconnected += async _ => { await OnReconnectedAsync(); }; HubConnection.Reconnecting += async ex => { await OnReconnectingAsync(ex); }; HubConnection.Closed += async ex => { await OnClosedAsync(ex); }; await HubConnection.StartAsync(); await OnConnectedAsync(); } /// /// Called after the connection is successfully started. /// protected virtual Task OnConnectedAsync() => Task.CompletedTask; /// /// Called when the connection is in the process of reconnecting. /// protected virtual Task OnReconnectingAsync(Exception? exception) => Task.CompletedTask; /// /// Called when the connection has reconnected. /// protected virtual Task OnReconnectedAsync() => Task.CompletedTask; /// /// Called when the connection is closed and will not reconnect automatically. /// protected virtual Task OnClosedAsync(Exception? exception) => Task.CompletedTask; /// /// Registers handlers for every method on THubClient so that when the hub /// invokes these methods on the client, they are forwarded to this component. /// private void WireClientInterfaceHandlers(HubConnection hubConnection) { var clientType = typeof(THubClient); var methods = clientType .GetMethods(BindingFlags.Public | BindingFlags.Instance) .Where(m => !m.IsSpecialName); // ignore property accessors, etc. foreach (var method in methods) { var parameters = method.GetParameters(); var parameterTypes = parameters.Select(p => p.ParameterType).ToArray(); // Use the generic On(string, Type[], Func) overload var subscription = hubConnection.On( methodName: method.Name, parameterTypes: parameterTypes, handler: async args => { var result = method.Invoke(Client, args); if (result is Task task) await task; // Ensure UI updates are marshaled back onto the Blazor renderer await InvokeAsync(StateHasChanged); }); _clientHandlerSubscriptions.Add(subscription); } } public async ValueTask DisposeAsync() { foreach (var subscription in _clientHandlerSubscriptions) subscription.Dispose(); _clientHandlerSubscriptions.Clear(); if (HubConnection is not null) { await HubConnection.DisposeAsync(); } } } }