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")]
|
@attribute [Authorize(Policy = "ClubViewer")]
|
||||||
|
|
||||||
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
||||||
@implements IDisposable
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
@using Fluxor
|
@using Fluxor
|
||||||
@using Koogle.Application.DTOs
|
@using Koogle.Application.DTOs
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
@using Koogle.Web.Store.PersonState
|
@using Koogle.Web.Store.PersonState
|
||||||
@using Koogle.Web.Store.TimerState
|
@using Koogle.Web.Store.TimerState
|
||||||
@using Koogle.Web.Components.Game
|
@using Koogle.Web.Components.Game
|
||||||
|
@using Koogle.Web.Hubs
|
||||||
|
@using Koogle.Web.Services
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
|
||||||
@inject IState<DayState> DayState
|
@inject IState<DayState> DayState
|
||||||
|
|
@ -22,6 +24,7 @@
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject IDialogService DialogService
|
@inject IDialogService DialogService
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject GameHubService HubService
|
||||||
|
|
||||||
<PageTitle>Spieltag Details</PageTitle>
|
<PageTitle>Spieltag Details</PageTitle>
|
||||||
|
|
||||||
|
|
@ -875,12 +878,30 @@ else
|
||||||
_activeTabIndex = 1;
|
_activeTabIndex = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnAfterRender(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
base.OnAfterRender(firstRender);
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
TimerState.StateChanged += OnTimerStateChanged;
|
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;
|
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>
|
/// </summary>
|
||||||
public record GameEndedFromHubAction(GameSummaryDto 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
|
// Error Actions
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -448,6 +448,15 @@ public class GameEffects
|
||||||
dispatcher);
|
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 game is over, skip save (user must confirm end via UI)
|
||||||
if (isGameOver)
|
if (isGameOver)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Fluxor;
|
using Fluxor;
|
||||||
|
using Koogle.Application.DTOs;
|
||||||
using Koogle.Application.Games;
|
using Koogle.Application.Games;
|
||||||
using Koogle.Domain.Enums;
|
using Koogle.Domain.Enums;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -509,6 +510,52 @@ public static class GameReducers
|
||||||
GameModel = action.GameModel
|
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>
|
/// <summary>
|
||||||
/// Handles GameEndedFromHubAction - handles game ended from another client.
|
/// Handles GameEndedFromHubAction - handles game ended from another client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue