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.
This commit is contained in:
12
SlipItIn/Messages/SyncStateChangedMessage.cs
Normal file
12
SlipItIn/Messages/SyncStateChangedMessage.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace SlipItIn.Messages;
|
||||
|
||||
public sealed record SyncStateInfo(bool IsSyncing, string StatusText, int PendingActions);
|
||||
|
||||
public class SyncStateChangedMessage : ValueChangedMessage<SyncStateInfo>
|
||||
{
|
||||
public SyncStateChangedMessage(SyncStateInfo value) : base(value)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ public class GameStateService : IGameStateService
|
||||
private readonly ISignalRService _signalR;
|
||||
private readonly ILocalStorageService _localStorage;
|
||||
private readonly IMessenger _messenger;
|
||||
private readonly SemaphoreSlim _syncLock = new(1, 1);
|
||||
|
||||
private readonly List<QueuedGameAction> _queuedActions = [];
|
||||
|
||||
@@ -53,16 +54,16 @@ public class GameStateService : IGameStateService
|
||||
_signalR.ConnectionStateChanged += async (_, connected) =>
|
||||
{
|
||||
_messenger.Send(new ConnectionStateChangedMessage(connected));
|
||||
if (connected)
|
||||
if (!connected)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
await ResyncAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_messenger.Send(new ErrorOccurredMessage($"Resync fehlgeschlagen: {ex.Message}"));
|
||||
}
|
||||
await ResyncAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_messenger.Send(new ErrorOccurredMessage($"Resync fehlgeschlagen: {ex.Message}"));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -81,14 +82,32 @@ public class GameStateService : IGameStateService
|
||||
|
||||
if (CurrentPlayerHand is not null)
|
||||
_messenger.Send(new PlayerHandChangedMessage(CurrentPlayerHand));
|
||||
|
||||
PublishSyncState(false, "Bereit", _queuedActions.Count);
|
||||
}
|
||||
|
||||
public async Task ResyncAsync()
|
||||
{
|
||||
if (CurrentGameState?.GameId > 0 && _signalR.IsConnected)
|
||||
await _signalR.RequestGameStateAsync(CurrentGameState.GameId);
|
||||
await _syncLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
PublishSyncState(true, "Syncing...", _queuedActions.Count);
|
||||
|
||||
await FlushQueuedActionsAsync();
|
||||
if (CurrentGameState?.GameId > 0 && _signalR.IsConnected)
|
||||
await _signalR.RequestGameStateAsync(CurrentGameState.GameId);
|
||||
|
||||
await FlushQueuedActionsCoreAsync();
|
||||
PublishSyncState(false, "Synchronisiert", _queuedActions.Count);
|
||||
}
|
||||
catch
|
||||
{
|
||||
PublishSyncState(false, "Sync fehlgeschlagen", _queuedActions.Count);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnqueueActionAsync(string method, params object[] arguments)
|
||||
@@ -102,21 +121,22 @@ public class GameStateService : IGameStateService
|
||||
|
||||
_queuedActions.Add(action);
|
||||
await _localStorage.SaveQueuedActionsAsync(_queuedActions);
|
||||
PublishSyncState(false, "Offline-Aktion gespeichert", _queuedActions.Count);
|
||||
}
|
||||
|
||||
public async Task FlushQueuedActionsAsync()
|
||||
{
|
||||
if (!_signalR.IsConnected || _queuedActions.Count == 0)
|
||||
return;
|
||||
|
||||
var snapshot = _queuedActions.ToList();
|
||||
foreach (var action in snapshot)
|
||||
await _syncLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await _signalR.SendQueuedActionAsync(action);
|
||||
_queuedActions.Remove(action);
|
||||
PublishSyncState(true, "Warteschlange wird gesendet...", _queuedActions.Count);
|
||||
await FlushQueuedActionsCoreAsync();
|
||||
PublishSyncState(false, "Warteschlange synchronisiert", _queuedActions.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncLock.Release();
|
||||
}
|
||||
|
||||
await _localStorage.SaveQueuedActionsAsync(_queuedActions);
|
||||
}
|
||||
|
||||
public async Task ClearLocalGameDataAsync()
|
||||
@@ -129,5 +149,36 @@ public class GameStateService : IGameStateService
|
||||
await _localStorage.ClearGameStateAsync();
|
||||
await _localStorage.ClearPlayerHandAsync();
|
||||
await _localStorage.ClearQueuedActionsAsync();
|
||||
|
||||
PublishSyncState(false, "Bereit", 0);
|
||||
}
|
||||
|
||||
private async Task FlushQueuedActionsCoreAsync()
|
||||
{
|
||||
if (!_signalR.IsConnected || _queuedActions.Count == 0)
|
||||
return;
|
||||
|
||||
var snapshot = _queuedActions.ToList();
|
||||
foreach (var action in snapshot)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _signalR.SendQueuedActionAsync(action);
|
||||
_queuedActions.Remove(action);
|
||||
PublishSyncState(true, "Warteschlange wird gesendet...", _queuedActions.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_messenger.Send(new ErrorOccurredMessage($"Queue-Aktion fehlgeschlagen ({action.Method}): {ex.Message}"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await _localStorage.SaveQueuedActionsAsync(_queuedActions);
|
||||
}
|
||||
|
||||
private void PublishSyncState(bool isSyncing, string statusText, int pendingActions)
|
||||
{
|
||||
_messenger.Send(new SyncStateChangedMessage(new SyncStateInfo(isSyncing, statusText, pendingActions)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ public class SignalRService : ISignalRService
|
||||
if (_hubConnection is null)
|
||||
return false;
|
||||
|
||||
ConnectionStateChanged?.Invoke(this, false);
|
||||
|
||||
try
|
||||
{
|
||||
await _hubConnection.StartAsync(cancellationToken);
|
||||
@@ -46,6 +48,7 @@ public class SignalRService : ISignalRService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConnectionStateChanged?.Invoke(this, false);
|
||||
ErrorReceived?.Invoke(this, $"SignalR-Verbindung fehlgeschlagen: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
@@ -56,7 +59,9 @@ public class SignalRService : ISignalRService
|
||||
if (_hubConnection is null)
|
||||
return;
|
||||
|
||||
await _hubConnection.StopAsync();
|
||||
if (_hubConnection.State is HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting)
|
||||
await _hubConnection.StopAsync();
|
||||
|
||||
ConnectionStateChanged?.Invoke(this, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ public partial class GameBoardViewModel : BaseViewModel,
|
||||
IRecipient<GameStateChangedMessage>,
|
||||
IRecipient<PlayerHandChangedMessage>,
|
||||
IRecipient<ChallengeReceivedMessage>,
|
||||
IRecipient<ErrorOccurredMessage>
|
||||
IRecipient<ErrorOccurredMessage>,
|
||||
IRecipient<SyncStateChangedMessage>,
|
||||
IRecipient<ConnectionStateChangedMessage>
|
||||
{
|
||||
private readonly ISignalRService _signalR;
|
||||
private readonly IGameStateService _gameStateService;
|
||||
@@ -33,6 +35,18 @@ public partial class GameBoardViewModel : BaseViewModel,
|
||||
[ObservableProperty]
|
||||
private SlipChallengeDto? pendingChallenge;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isConnected;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isSyncing;
|
||||
|
||||
[ObservableProperty]
|
||||
private string syncStatusText = "Bereit";
|
||||
|
||||
[ObservableProperty]
|
||||
private int pendingSyncActions;
|
||||
|
||||
public ObservableCollection<PlayerCardDto> MyCards { get; } = [];
|
||||
public ObservableCollection<PlayerInfoDto> OtherPlayers { get; } = [];
|
||||
|
||||
@@ -42,6 +56,7 @@ public partial class GameBoardViewModel : BaseViewModel,
|
||||
_gameStateService = gameStateService;
|
||||
_authSession = authSession;
|
||||
|
||||
IsConnected = _signalR.IsConnected;
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
@@ -164,6 +179,18 @@ public partial class GameBoardViewModel : BaseViewModel,
|
||||
StatusMessage = message.Value;
|
||||
}
|
||||
|
||||
public void Receive(SyncStateChangedMessage message)
|
||||
{
|
||||
IsSyncing = message.Value.IsSyncing;
|
||||
SyncStatusText = message.Value.StatusText;
|
||||
PendingSyncActions = message.Value.PendingActions;
|
||||
}
|
||||
|
||||
public void Receive(ConnectionStateChangedMessage message)
|
||||
{
|
||||
IsConnected = message.Value;
|
||||
}
|
||||
|
||||
private int GetCurrentPlayerId()
|
||||
{
|
||||
var username = _authSession.CurrentUser?.Username;
|
||||
|
||||
@@ -13,7 +13,8 @@ public partial class LobbyViewModel : BaseViewModel,
|
||||
IRecipient<LobbyCreatedMessage>,
|
||||
IRecipient<GameStartedMessage>,
|
||||
IRecipient<ErrorOccurredMessage>,
|
||||
IRecipient<ConnectionStateChangedMessage>
|
||||
IRecipient<ConnectionStateChangedMessage>,
|
||||
IRecipient<SyncStateChangedMessage>
|
||||
{
|
||||
private readonly ISignalRService _signalR;
|
||||
private readonly IGameStateService _gameStateService;
|
||||
@@ -32,6 +33,15 @@ public partial class LobbyViewModel : BaseViewModel,
|
||||
[ObservableProperty]
|
||||
private bool isConnected;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isSyncing;
|
||||
|
||||
[ObservableProperty]
|
||||
private string syncStatusText = "Bereit";
|
||||
|
||||
[ObservableProperty]
|
||||
private int pendingSyncActions;
|
||||
|
||||
public ObservableCollection<PlayerInfoDto> Players { get; } = [];
|
||||
|
||||
public LobbyViewModel(ISignalRService signalR, IGameStateService gameStateService, IAuthSessionService authSession, IApiService apiService)
|
||||
@@ -139,6 +149,13 @@ public partial class LobbyViewModel : BaseViewModel,
|
||||
IsConnected = message.Value;
|
||||
}
|
||||
|
||||
public void Receive(SyncStateChangedMessage message)
|
||||
{
|
||||
IsSyncing = message.Value.IsSyncing;
|
||||
SyncStatusText = message.Value.StatusText;
|
||||
PendingSyncActions = message.Value.PendingActions;
|
||||
}
|
||||
|
||||
private int GetCurrentPlayerId()
|
||||
{
|
||||
var username = _authSession.CurrentUser?.Username;
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
<Label Text="Spielbrett" FontSize="24" FontAttributes="Bold" />
|
||||
<Label Text="{Binding CurrentRound, StringFormat='Runde: {0}'}" />
|
||||
<Label Text="{Binding RoundTimeRemaining, StringFormat='Verbleibende Zeit: {0}s'}" />
|
||||
<Label Text="{Binding IsConnected, StringFormat='Verbindung: {0}'}" TextColor="Gray" />
|
||||
<Label Text="{Binding SyncStatusText}" TextColor="DodgerBlue" IsVisible="{Binding IsSyncing}" />
|
||||
<Label Text="{Binding PendingSyncActions, StringFormat='Offline-Aktionen: {0}'}" TextColor="Gray" />
|
||||
|
||||
<Button Text="Status aktualisieren" Command="{Binding RefreshStateCommand}" />
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
<VerticalStackLayout Padding="24" Spacing="12">
|
||||
<Label Text="Lobby" FontSize="24" FontAttributes="Bold" />
|
||||
<Label Text="{Binding IsConnected, StringFormat='Verbindung: {0}'}" />
|
||||
<Label Text="{Binding SyncStatusText}" TextColor="DodgerBlue" IsVisible="{Binding IsSyncing}" />
|
||||
<Label Text="{Binding PendingSyncActions, StringFormat='Offline-Aktionen: {0}'}" TextColor="Gray" />
|
||||
|
||||
<Button Text="Lobby erstellen" Command="{Binding CreateLobbyCommand}" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user