Intercetta il tasto indietro dove passa davvero

Il salvataggio come bozza non e' mai scattato dal tasto indietro: la UI Blazor
non riceveva l'evento, e uscire da un form con modifiche in corso buttava via
il lavoro esattamente come prima che la bozza esistesse.

Il back non arrivava a OnBackPressed per due motivi diversi:
- da AndroidX Activity 1.6 il gesto passa dall'OnBackPressedDispatcher e il
  metodo dell'Activity non viene piu' invocato;
- con la navigazione a tre tasti il back arriva come evento di tastiera e la
  WebView lo intercetta per conto suo, tornando indietro nella propria
  cronologia, prima che Activity o dispatcher lo vedano.

Ora la chiave si ferma in DispatchKeyEvent, che passa prima della gerarchia di
view, e resta un callback sul dispatcher per la navigazione a gesti, dove
evento di tastiera non ce n'e'. Entrambi restano accesi solo finche' la UI ha
davvero qualcosa da dire: altrimenti il back si comporta come sempre.

I gestori diventano una pila, cosi' sopra il form puo' aprirsi un bottom sheet
e il back chiude prima quello senza portarsi via la registrazione sottostante.
ActionSheet si registra da solo mentre e' aperto.

Verificato sull'EDA52: back sul form con modifiche apre "Lavoro non salvato" e
salva la bozza; back sullo sheet chiude solo lo sheet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 12:45:23 +02:00
parent 811d8a86d8
commit 52b2994d3b
3 changed files with 129 additions and 15 deletions
@@ -1,4 +1,8 @@
@*
@using SteUp.Shared.Core.Messages.System
@inject BackButtonService BackButton
@implements IDisposable
@*
Bottom sheet (STILE_UI §6.14): sale dal basso, maniglia di trascinamento,
testata con titolo e chiusura, un'unica linea di struttura, corpo scorrevole.
Sostituisce i menu flottanti: stessa funzione, bersagli molto piu' grandi.
@@ -43,9 +47,35 @@
[Parameter] public RenderFragment? ChildContent { get; set; }
// Il tasto indietro del dispositivo chiude lo sheet invece di uscire dalla
// schermata sotto: e' il gesto che l'utente si aspetta, ed e' anche la
// ragione per cui i gestori sono impilati (vedi BackButtonService).
private IDisposable? _backRegistration;
protected override void OnParametersSet()
{
if (Open && _backRegistration is null)
{
_backRegistration = BackButton.Register(() => InvokeAsync(Close));
}
else if (!Open)
{
Unregister();
}
}
private async Task Close()
{
Unregister();
Open = false;
await OpenChanged.InvokeAsync(false);
}
private void Unregister()
{
_backRegistration?.Dispose();
_backRegistration = null;
}
public void Dispose() => Unregister();
}
@@ -5,15 +5,32 @@ namespace SteUp.Shared.Core.Messages.System;
/// Il form scheda si registra mentre è aperto così il back non manda l'app in
/// background buttando via il lavoro non salvato: passa invece dallo stesso
/// percorso del pulsante "Annulla", che propone il salvataggio come bozza.
///
/// I gestori sono impilati: sopra il form può aprirsi un bottom sheet, e il
/// tasto indietro deve chiudere prima quello. Chiudendolo si torna al gestore
/// sottostante, che resta valido — senza la pila il sheet, uscendo di scena,
/// porterebbe via con sé anche la registrazione del form.
/// </summary>
public class BackButtonService
{
private Func<Task>? _handler;
private readonly List<Func<Task>> _handlers = [];
/// Registra il gestore corrente. Ritorna l'azione di deregistrazione.
/// <summary>
/// Segnala all'host se in questo momento c'è qualcuno da avvisare. L'host
/// se ne serve per accendere e spegnere la propria intercettazione, così
/// quando la UI non ha nulla da dire il tasto indietro torna a comportarsi
/// esattamente come sempre.
/// </summary>
public event Action<bool>? HandlersChanged;
public bool HasHandlers => _handlers.Count > 0;
/// Registra il gestore corrente in cima alla pila. Ritorna la deregistrazione.
public IDisposable Register(Func<Task> handler)
{
_handler = handler;
_handlers.Add(handler);
HandlersChanged?.Invoke(true);
return new Registration(this, handler);
}
@@ -23,11 +40,10 @@ public class BackButtonService
/// </summary>
public bool TryHandleBackPressed()
{
var handler = _handler;
if (handler is null) return false;
if (_handlers.Count == 0) return false;
// Fire-and-forget: il gestore rientra nel dispatcher Blazor da solo.
_ = handler();
_ = _handlers[^1]();
return true;
}
@@ -35,8 +51,8 @@ public class BackButtonService
{
public void Dispose()
{
// Non azzerare se nel frattempo si è registrato qualcun altro.
if (ReferenceEquals(owner._handler, handler)) owner._handler = null;
if (!owner._handlers.Remove(handler)) return;
owner.HandlersChanged?.Invoke(owner.HasHandlers);
}
}
}