feat: sichtbare Seite nach eingehendem Sync automatisch neu laden

EventApplier schreibt eingehende Sync-Ereignisse absichtlich direkt auf die rohe LiteDB-Collection
(Ping-Pong-Vermeidung), benachrichtigt dabei aber kein ViewModel - eine per Sync empfangene neue
Stunde erschien bisher erst nach manuellem Neuladen (z.B. Tab-Wechsel).

Neues SyncEngine.DataChanged-Ereignis, gefeuert nach jedem Pull mit Ereignissen sowie bei einer
RemoteWon-Konfliktauflösung; SyncStatusViewModel reicht es durch. MainWindowViewModel abonniert es
und lädt ausschließlich die gerade sichtbare Seite über ihren eigenen, längst vorhandenen
Lade-Einstieg neu - keine Navigation weg von offenen Detailansichten, keine neue Lade-Logik, und
eine laufende Inline-Bearbeitung (Schülerdetail) wird nicht überschrieben.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 00:18:56 +02:00
co-authored by Claude Sonnet 5
parent 979511ab6a
commit f868b3a259
6 changed files with 211 additions and 2 deletions
@@ -1,3 +1,4 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Services;
@@ -43,6 +44,39 @@ public partial class MainWindowViewModel : ObservableObject
CurrentSchoolYear = sy.CurrentSchoolYear();
CurrentPage = dashboard;
AppLock.ApplyConfig();
SyncStatus.DataChanged += OnSyncDataChanged;
}
// EventApplier schreibt bei eingehenden Sync-Ereignissen absichtlich direkt auf die rohe
// LiteDB-Collection, an jedem ViewModel vorbei (Ping-Pong-Vermeidung, siehe EventApplier-
// Klassenkommentar) - ohne diesen Hook blieb die gerade sichtbare Seite bis zum nächsten
// manuellen Neuladen (z.B. Tab-Wechsel) auf dem alten Stand (TODO 10.1.11). Lädt bewusst nur
// die aktuell sichtbare Seite über ihren eigenen, längst vorhandenen Lade-Einstieg neu - keine
// neue Lade-Logik, keine Navigation weg von einer offenen Detailansicht. SyncEngine.DataChanged
// feuert aus dem Sync-Timer-Hintergrundthread - daher der Dispatcher-Sprung zurück auf den
// UI-Thread (gleiches Muster wie NotificationService.Show).
private void OnSyncDataChanged()
{
if (Dispatcher.UIThread.CheckAccess()) RefreshCurrentPage();
else Dispatcher.UIThread.Post(RefreshCurrentPage);
}
private void RefreshCurrentPage()
{
switch (CurrentPage)
{
case DashboardViewModel vm: vm.RefreshCommand.Execute(null); break;
case GroupListViewModel vm: vm.RefreshCommand.Execute(null); break;
case StudentListViewModel vm: vm.LoadStudents(); break;
case TimetableViewModel vm: vm.Load(); break;
case WorkloadViewModel vm:
vm.Tasks.Load(); vm.TimeTracking.Load(); vm.Evaluation.Load(); break;
case GroupDetailViewModel { Group: { } group } vm: vm.LoadGroup(group.Id); break;
// Inline-Bearbeitung (IsEditing) nicht überschreiben - anders als die Gruppenansicht
// laufen Namens-/Geschlechtsänderungen hier nicht über einen Dialog.
case StudentDetailViewModel { Student: { } student, IsEditing: false } vm:
vm.LoadStudent(student.Id); break;
}
}
partial void OnActiveNavItemChanged(NavItem value)
@@ -15,6 +15,12 @@ public partial class SyncStatusViewModel : ObservableObject
[ObservableProperty] private bool _isServerConfigured;
[ObservableProperty] private int _pendingCount;
/// <summary>Feuert, wenn ein Sync tatsächlich Daten angewendet hat (siehe SyncEngine.
/// DataChanged, TODO 10.1.11) - MainWindowViewModel nutzt das, um die gerade sichtbare Seite
/// neu zu laden, da EventApplier absichtlich an jedem ViewModel vorbei direkt auf die LiteDB
/// schreibt.</summary>
public event Action? DataChanged;
public SyncStatusViewModel(SyncEngine? engine)
{
_engine = engine;
@@ -22,6 +28,7 @@ public partial class SyncStatusViewModel : ObservableObject
if (_engine is not null)
{
_engine.StatusChanged += OnStatus;
_engine.DataChanged += () => DataChanged?.Invoke();
// SyncEngine feuert sein erstes StatusChanged bereits im eigenen Konstruktor
// (UpdateStatus) - der läuft aber schon, während wir hier noch in GetService<SyncEngine>()
// stecken, also VOR dem obigen Abonnieren. Ohne diesen Nachhol-Aufruf bliebe StatusText