SinalR Sync part 2:
DayDetails.razor
1. GameHubService injiziert (Zeile 26)
2. IAsyncDisposable implementiert statt IDisposable
3. SignalR-Events abonniert:
- OnGameStateUpdated → aktualisiert lokalen State via RemoteGameStateUpdatedAction
- OnThrowRecorded → zeigt Snackbar-Benachrichtigung
- OnGameStarted → lädt aktives Spiel neu
- OnGameEnded → lädt abgeschlossene Spiele neu
4. Hub-Verbindung initialisiert in OnAfterRenderAsync:
- StartAsync() - verbindet zum Hub
- JoinDayAsync(DayId) - tritt der Day-Gruppe bei
- JoinGameAsync() - tritt der Game-Gruppe bei (wenn aktiv)
5. Automatisches Join/Leave bei Spielwechsel via OnGameStateChanged
6. Cleanup in DisposeAsync:
- Unsubscribe von Events
- LeaveGameAsync/LeaveDayAsync aufrufen
- Hub-Verbindung disposen
GameEffects.cs
BroadcastThrowAsync hinzugefügt (Zeile 451-458) - broadcast Wurf sofort an andere Clients
GameActions.cs / GameReducers.cs
RemoteGameStateUpdatedAction und Reducer hinzugefügt für Remote-State-Updates
This commit is contained in:
parent
7cbdfe28e6
commit
6f419b3373
|
|
@ -2,7 +2,7 @@
|
|||
@attribute [Authorize(Policy = "ClubViewer")]
|
||||
|
||||
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
||||
@implements IDisposable
|
||||
@implements IAsyncDisposable
|
||||
|
||||
@using Fluxor
|
||||
@using Koogle.Application.DTOs
|
||||
|
|
@ -12,6 +12,8 @@
|
|||
@using Koogle.Web.Store.PersonState
|
||||
@using Koogle.Web.Store.TimerState
|
||||
@using Koogle.Web.Components.Game
|
||||
@using Koogle.Web.Hubs
|
||||
@using Koogle.Web.Services
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
|
||||
@inject IState<DayState> DayState
|
||||
|
|
@ -22,6 +24,7 @@
|
|||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject GameHubService HubService
|
||||
|
||||
<PageTitle>Spieltag Details</PageTitle>
|
||||
|
||||
|
|
@ -875,12 +878,30 @@ else
|
|||
_activeTabIndex = 1;
|
||||
}
|
||||
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
base.OnAfterRender(firstRender);
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
if (firstRender)
|
||||
{
|
||||
TimerState.StateChanged += OnTimerStateChanged;
|
||||
GameState.StateChanged += OnGameStateChanged;
|
||||
|
||||
// Subscribe to SignalR events
|
||||
HubService.OnGameStateUpdated += HandleHubGameStateUpdated;
|
||||
HubService.OnThrowRecorded += HandleHubThrowRecorded;
|
||||
HubService.OnGameStarted += HandleHubGameStarted;
|
||||
HubService.OnGameEnded += HandleHubGameEnded;
|
||||
|
||||
// Start SignalR connection and join day group
|
||||
await HubService.StartAsync();
|
||||
await HubService.JoinDayAsync(DayId);
|
||||
|
||||
// If there's an active game, join its group too
|
||||
if (GameState.Value.ActiveGameId.HasValue)
|
||||
{
|
||||
await HubService.JoinGameAsync(GameState.Value.ActiveGameId.Value);
|
||||
_previousGameId = GameState.Value.ActiveGameId.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -894,8 +915,82 @@ else
|
|||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
private Guid? _previousGameId;
|
||||
|
||||
private async void OnGameStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var currentGameId = GameState.Value.ActiveGameId;
|
||||
|
||||
// Game started - join game group
|
||||
if (currentGameId.HasValue && _previousGameId != currentGameId)
|
||||
{
|
||||
if (_previousGameId.HasValue)
|
||||
{
|
||||
await HubService.LeaveGameAsync(_previousGameId.Value);
|
||||
}
|
||||
await HubService.JoinGameAsync(currentGameId.Value);
|
||||
_previousGameId = currentGameId;
|
||||
}
|
||||
// Game ended - leave game group
|
||||
else if (!currentGameId.HasValue && _previousGameId.HasValue)
|
||||
{
|
||||
await HubService.LeaveGameAsync(_previousGameId.Value);
|
||||
_previousGameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// SignalR event handlers
|
||||
private void HandleHubGameStateUpdated(GameStateSerializationDto state)
|
||||
{
|
||||
// Dispatch action to update local state from remote
|
||||
Dispatcher.Dispatch(new RemoteGameStateUpdatedAction(state));
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void HandleHubThrowRecorded(Guid gameId, Guid playerId, int pinsKnocked)
|
||||
{
|
||||
// Throw was recorded by another client - refresh state
|
||||
Snackbar.Add($"Wurf empfangen: {pinsKnocked} Pins", Severity.Info);
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void HandleHubGameStarted(Guid gameId, string gameTypeName)
|
||||
{
|
||||
// New game started - reload active game
|
||||
Dispatcher.Dispatch(new LoadActiveGameAction(DayId));
|
||||
Snackbar.Add($"Neues Spiel gestartet: {gameTypeName}", Severity.Success);
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void HandleHubGameEnded(GameResultDto result)
|
||||
{
|
||||
// Game ended - reload completed games
|
||||
Dispatcher.Dispatch(new LoadCompletedGamesAction(DayId));
|
||||
var message = result.WinnerName != null
|
||||
? $"Spiel beendet! Gewinner: {result.WinnerName}"
|
||||
: "Spiel beendet!";
|
||||
Snackbar.Add(message, Severity.Success);
|
||||
_activeTabIndex = 0;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
TimerState.StateChanged -= OnTimerStateChanged;
|
||||
GameState.StateChanged -= OnGameStateChanged;
|
||||
|
||||
// Unsubscribe from SignalR events
|
||||
HubService.OnGameStateUpdated -= HandleHubGameStateUpdated;
|
||||
HubService.OnThrowRecorded -= HandleHubThrowRecorded;
|
||||
HubService.OnGameStarted -= HandleHubGameStarted;
|
||||
HubService.OnGameEnded -= HandleHubGameEnded;
|
||||
|
||||
// Leave groups and stop hub connection
|
||||
if (_previousGameId.HasValue)
|
||||
{
|
||||
await HubService.LeaveGameAsync(_previousGameId.Value);
|
||||
}
|
||||
await HubService.LeaveDayAsync(DayId);
|
||||
await HubService.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,6 +258,11 @@ public record GameStateUpdatedFromHubAction(
|
|||
/// </summary>
|
||||
public record GameEndedFromHubAction(GameSummaryDto Summary);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when game state DTO is received from remote SignalR hub.
|
||||
/// </summary>
|
||||
public record RemoteGameStateUpdatedAction(Koogle.Application.DTOs.GameStateSerializationDto State);
|
||||
|
||||
// Error Actions
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -448,6 +448,15 @@ public class GameEffects
|
|||
dispatcher);
|
||||
}
|
||||
|
||||
// Broadcast throw immediately to other clients (before debounced save)
|
||||
if (state.ActiveGameId.HasValue)
|
||||
{
|
||||
await _hubService.BroadcastThrowAsync(
|
||||
state.ActiveGameId.Value,
|
||||
currentPlayerId.Value,
|
||||
afterThrowState.PinsKnocked);
|
||||
}
|
||||
|
||||
// If game is over, skip save (user must confirm end via UI)
|
||||
if (isGameOver)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using Fluxor;
|
||||
using Koogle.Application.DTOs;
|
||||
using Koogle.Application.Games;
|
||||
using Koogle.Domain.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -509,6 +510,52 @@ public static class GameReducers
|
|||
GameModel = action.GameModel
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles RemoteGameStateUpdatedAction - updates state from remote SignalR DTO.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static GameState OnRemoteGameStateUpdated(GameState state, RemoteGameStateUpdatedAction action)
|
||||
{
|
||||
var dto = action.State;
|
||||
|
||||
var throwPanelAfter = new ThrowPanelState
|
||||
{
|
||||
IsStarted = dto.ThrowPanelAfter.IsStarted,
|
||||
Pin1 = (PinStatus)dto.ThrowPanelAfter.Pins[0],
|
||||
Pin2 = (PinStatus)dto.ThrowPanelAfter.Pins[1],
|
||||
Pin3 = (PinStatus)dto.ThrowPanelAfter.Pins[2],
|
||||
Pin4 = (PinStatus)dto.ThrowPanelAfter.Pins[3],
|
||||
Pin5 = (PinStatus)dto.ThrowPanelAfter.Pins[4],
|
||||
Pin6 = (PinStatus)dto.ThrowPanelAfter.Pins[5],
|
||||
Pin7 = (PinStatus)dto.ThrowPanelAfter.Pins[6],
|
||||
Pin8 = (PinStatus)dto.ThrowPanelAfter.Pins[7],
|
||||
Pin9 = (PinStatus)dto.ThrowPanelAfter.Pins[8],
|
||||
ThrowsPerRound = dto.ThrowPanelAfter.ThrowsPerRound,
|
||||
ThrowCounterPerRound = dto.ThrowPanelAfter.ThrowCounterPerRound,
|
||||
ThrowMode = (ThrowMode)dto.ThrowPanelAfter.ThrowMode,
|
||||
TotalThrowCounter = dto.ThrowPanelAfter.TotalThrowCounter,
|
||||
BellValue = dto.ThrowPanelAfter.BellValue
|
||||
};
|
||||
|
||||
var participants = new ParticipantsState
|
||||
{
|
||||
PlayerIds = dto.Participants.PlayerIds,
|
||||
CurrentPlayerIndex = dto.Participants.CurrentPlayerIndex,
|
||||
Mode = (ParticipantsMode)dto.Participants.Mode
|
||||
};
|
||||
|
||||
object? gameModel = dto.GameModel.HasValue
|
||||
? (object?)dto.GameModel.Value
|
||||
: null;
|
||||
|
||||
return state with
|
||||
{
|
||||
ThrowPanelAfter = throwPanelAfter,
|
||||
Participants = participants,
|
||||
GameModel = gameModel
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles GameEndedFromHubAction - handles game ended from another client.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Reference in New Issue