Files
SlipItIn/SlipItIn/Services/SignalRService.cs
Tim Krampitz 6f3c0eb716 Synchronisierungs- und Verbindungsstatus verbessert
Neuer Nachrichtentyp SyncStateChangedMessage zur Übermittlung des Sync-Status über das MVVM-Messaging-System eingeführt. GameStateService sendet nach relevanten Aktionen den aktuellen Sync-Status. Methoden zur Synchronisierung sind jetzt thread-sicher und melden Statusänderungen laufend. Fehler beim Senden von Queue-Aktionen werden einzeln behandelt und führen zu Fehlermeldungen im Messenger. ViewModels aktualisieren neue Properties (IsSyncing, SyncStatusText, PendingSyncActions) bei Statusänderungen. XAML-Seiten zeigen Verbindungs- und Sync-Status sowie Offline-Aktionen an. SignalRService meldet Verbindungsstatus konsistenter, auch bei Fehlern und Disconnects.
2026-08-08 21:23:27 +02:00

190 lines
6.7 KiB
C#

using System.Text.Json;
using Microsoft.AspNetCore.SignalR.Client;
using SlipItIn.Models;
using SlipItIn.Services.Interfaces;
using SlipItIn.Shared.DTOs;
namespace SlipItIn.Services;
public class SignalRService : ISignalRService
{
private readonly IAppConfigurationService _configuration;
private readonly IAuthSessionService _authSession;
private HubConnection? _hubConnection;
public SignalRService(IAppConfigurationService configuration, IAuthSessionService authSession)
{
_configuration = configuration;
_authSession = authSession;
}
public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
public event EventHandler<(int GameId, string LobbyCode)>? LobbyCreated;
public event EventHandler<GameStateDto>? GameStateUpdated;
public event EventHandler<int>? GameStarted;
public event EventHandler<PlayerHandDto>? PlayerHandUpdated;
public event EventHandler<SlipChallengeDto>? ChallengeReceived;
public event EventHandler<string>? ErrorReceived;
public event EventHandler<bool>? ConnectionStateChanged;
public async Task<bool> ConnectAsync(string token, CancellationToken cancellationToken = default)
{
if (IsConnected)
return true;
EnsureConnection(token);
if (_hubConnection is null)
return false;
ConnectionStateChanged?.Invoke(this, false);
try
{
await _hubConnection.StartAsync(cancellationToken);
ConnectionStateChanged?.Invoke(this, true);
return true;
}
catch (Exception ex)
{
ConnectionStateChanged?.Invoke(this, false);
ErrorReceived?.Invoke(this, $"SignalR-Verbindung fehlgeschlagen: {ex.Message}");
return false;
}
}
public async Task DisconnectAsync()
{
if (_hubConnection is null)
return;
if (_hubConnection.State is HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting)
await _hubConnection.StopAsync();
ConnectionStateChanged?.Invoke(this, false);
}
public Task CreateLobbyAsync() => InvokeAsync("CreateLobby");
public Task JoinLobbyAsync(string lobbyCode) => InvokeAsync("JoinLobby", lobbyCode);
public Task PlayerReadyAsync(int gameId, int playerId) => InvokeAsync("PlayerReady", gameId, playerId);
public Task StartGameAsync(int gameId) => InvokeAsync("StartGame", gameId);
public Task SubmitSlipAsync(int gameId, int playerId, int cardId)
=> InvokeAsync("SubmitSlip", gameId, playerId, cardId);
public Task ChallengeSlipAsync(int gameId, int challengingPlayerId, int targetCardId)
=> InvokeAsync("ChallengeSlip", gameId, challengingPlayerId, targetCardId);
public Task ResolveChallengeAsync(int challengeId, bool approved)
=> InvokeAsync("ResolveChallenge", challengeId, approved);
public Task RequestGameStateAsync(int gameId) => InvokeAsync("RequestGameState", gameId);
public async Task SendQueuedActionAsync(QueuedGameAction action)
{
if (_hubConnection is null)
return;
object?[] args;
try
{
var doc = JsonDocument.Parse(action.ArgumentsJson);
args = doc.RootElement.ValueKind == JsonValueKind.Array
? doc.RootElement.EnumerateArray().Select(ToObject).ToArray()
: [];
}
catch
{
args = [];
}
await InvokeAsync(action.Method, args);
}
private void EnsureConnection(string token)
{
if (_hubConnection is not null)
return;
_hubConnection = new HubConnectionBuilder()
.WithUrl(_configuration.HubUrl, options =>
{
options.AccessTokenProvider = () => Task.FromResult<string?>(_authSession.AccessToken ?? token);
})
.WithAutomaticReconnect([TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)])
.Build();
_hubConnection.Reconnecting += _ =>
{
ConnectionStateChanged?.Invoke(this, false);
return Task.CompletedTask;
};
_hubConnection.Closed += _ =>
{
ConnectionStateChanged?.Invoke(this, false);
return Task.CompletedTask;
};
_hubConnection.Reconnected += _ =>
{
ConnectionStateChanged?.Invoke(this, true);
return Task.CompletedTask;
};
_hubConnection.On<JsonElement>("LobbyCreated", payload =>
{
var gameId = payload.TryGetProperty("gameId", out var gameIdElement) ? gameIdElement.GetInt32() : 0;
var lobbyCode = payload.TryGetProperty("lobbyCode", out var codeElement) ? codeElement.GetString() ?? string.Empty : string.Empty;
LobbyCreated?.Invoke(this, (gameId, lobbyCode));
});
_hubConnection.On<GameStateDto>("PlayerJoined", dto => GameStateUpdated?.Invoke(this, dto));
_hubConnection.On<GameStateDto>("GameStateUpdated", dto => GameStateUpdated?.Invoke(this, dto));
_hubConnection.On<JsonElement>("GameStarted", payload =>
{
var gameId = payload.TryGetProperty("gameId", out var gameIdElement) ? gameIdElement.GetInt32() : 0;
GameStarted?.Invoke(this, gameId);
});
_hubConnection.On<PlayerHandDto>("PlayerHandUpdated", dto => PlayerHandUpdated?.Invoke(this, dto));
_hubConnection.On<SlipChallengeDto>("ChallengeReceived", dto => ChallengeReceived?.Invoke(this, dto));
_hubConnection.On<JsonElement>("Error", payload =>
{
var message = payload.TryGetProperty("message", out var msgElement)
? msgElement.GetString() ?? "Unbekannter Fehler"
: "Unbekannter Fehler";
ErrorReceived?.Invoke(this, message);
});
}
private async Task InvokeAsync(string method, params object?[] args)
{
if (_hubConnection is null || _hubConnection.State != HubConnectionState.Connected)
throw new InvalidOperationException("Keine aktive SignalR-Verbindung.");
await _hubConnection.InvokeCoreAsync(method, args);
}
private static object? ToObject(JsonElement element)
{
return element.ValueKind switch
{
JsonValueKind.String => element.GetString(),
JsonValueKind.Number when element.TryGetInt32(out var i) => i,
JsonValueKind.Number when element.TryGetInt64(out var l) => l,
JsonValueKind.Number when element.TryGetDouble(out var d) => d,
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
_ => element.GetRawText()
};
}
}