fix(ui): Ordner-Toggle in der Sidebar schluckt schnelle Klicks

ToggleFolderAsync war ein async [RelayCommand]; AsyncRelayCommand sperrt
sich waehrend des DB-Writes selbst, schnelle Klicks gingen verloren
(Befund F1, docs/test-session-2026-09-16.md).

Variante A: Toggle ist jetzt synchron (reiner UI-State), Persistierung
fire-and-forget mit einem CancellationTokenSource pro Ordner, damit ein
zweiter Klick vor dem ersten Schreibvorgang diesen abbricht - die DB
landet immer beim zuletzt sichtbaren Zustand, nie bei einem veralteten.
Gleiches Muster wie SaveTitleAsync/SaveDescriptionAsync in
DetailsIslandViewModel.

Gegenprueft: RemoveFromFolderAsync/CreateListAsync haben denselben
async-RelayCommand-Schnitt, sind aber Einzelklick-Aktionen (Kontextmenue/
Button), kein Rapid-Click-Ziel wie der Ordner-Toggle - bewusst nicht
umgebaut. Die Modal-oeffnenden Commands sind unkritisch, ein Modal
blockiert ohnehin.
This commit is contained in:
Mika Kuns
2026-09-16 15:43:37 +02:00
committed by mika kuns
parent 7452f6cfe4
commit 5cdfb14ff1
2 changed files with 46 additions and 3 deletions
@@ -622,16 +622,33 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
await MoveToFolderAsync(row, null);
}
private readonly Dictionary<string, CancellationTokenSource> _folderCollapseCts = new();
// Synchronous on purpose: the fold is pure UI state and must never miss a click waiting on
// the AsyncRelayCommand's own CanExecute lock while the DB write below is in flight. The
// write is fire-and-forget with a per-folder cancellation token so a second toggle before the
// first one lands cancels it — the DB always ends up with the last-clicked state, never a
// stale one racing ahead of it.
[RelayCommand]
private async Task ToggleFolderAsync(ListFolderNavItemViewModel? folder)
private void ToggleFolder(ListFolderNavItemViewModel? folder)
{
if (folder is null) return;
folder.IsCollapsed = !folder.IsCollapsed;
if (_folderCollapseCts.TryGetValue(folder.Id, out var previous)) previous.Cancel();
var cts = new CancellationTokenSource();
_folderCollapseCts[folder.Id] = cts;
_ = PersistFolderCollapsedAsync(folder.Id, folder.IsCollapsed, cts.Token);
}
private async Task PersistFolderCollapsedAsync(string folderId, bool isCollapsed, CancellationToken ct)
{
try
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
await new ListRepository(ctx).SetFolderCollapsedAsync(folder.Id, folder.IsCollapsed);
ct.ThrowIfCancellationRequested();
await new ListRepository(ctx).SetFolderCollapsedAsync(folderId, isCollapsed);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
// The fold itself already happened in the UI; only its persistence failed.