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