Nulla di tecnico stavolta solo un esempio di quello che si può fare, non tanto graficamente perchè sono una mezza-ciofeca, quanto in termini di UX; abbiamo parlato di Loading Adorner, con lo scopo di migliorare l’esperienza dell’utente durante l’uso della nostra applicazione.

Naturalmente non basta, uffa…, l’utente si aspetta anche altre cose (sempre in termini di operazioni asincrone):

  • La possibilità di annullare l’operazione in corso;
  • Feedback sullo stato d’esecuzione/avanzamento dell’operazione;

Entrambe le cose non è detto che siano di facile soluzione, analizziamole una alla volta:

Annullamento dell’operazione asincrona

Perchè un’operazione asincrona sia effettivamente annullabile non è di certo sufficiente piazzare un bel pulsantio “Annulla” sulla nostra UI… è invece un requisito fondamentale che tutta la “catena” dei componenti coinvolti nell’operazione asincrona abbia ben noto il concetto di annullamento.

Ad esempio il codice seguente è solo apparentemente conscio del concetto di annullamento:

var worker = Worker.UsingAsArgument( query )
    .AndExpectingAsResult<IEnumerable<TEntity>>()
    .WhenExecutedDo( arg =>
    {
        arg.Result = this.Repository.GetByQuery( arg.Argument );
    } )
    .ButBeforeDo( arg =>
    {
        this.Items.DataSource.Clear();
        this.Selection.DataSource.Clear();

        this.IsBusy = true;
    } )
    .AndAfterDo( arg =>
    {
        this.Items.DataSource
            .CastTo<IEntityCollection<TEntity>>()
            .BulkLoad( arg.Result );

        this.IsBusy = false;
    } )
    .Execute();

worker.Cancel();

ed infatti non annulla proprio un bel niente ;-) o meglio la richiesta di annullamento viene correttamente iniettata ma il “repository” non sa proprio nulla di richieste di annullamento e quindi… non c’è trippa per gatti :-)

La soluzione, dietro le quinte abbastanza complessa, forse anche troppo, è:

var repo = this.Repository.AsAsync( arg.CancellationToken );
arg.Result = repo.GetByQuery( arg.Argument );

Che comporta rendere conscia del concetto di “asincronia” l’intera pipeline, a questo punto il repository, che potete immaginare non è più lo stesso, è in grado di digerire quel CancellationToken e di conseguenza reagire alla richiesta di annullamento.

Feedback

Facciamo un classico esempio: una “form” di ricerca in cui l’utente immette dei criteri, in questo caso una lista di keyword separate dal ‘;’, il sistema per impostazione predefinita, se la singola keyword non contiene caratteri jolly, implicitamente la racchiude tra caratteri jolly (i mie utenti non è che ne capiscano molto di ‘*’ e ‘?’). Quindi ricapitolando:

mauro –> *mauro*
mau* –> mau*

Da questo ne consegue che se l’utente inserisce qualcosa di molto corto senza caratteri jolly la ricerca potrebbe produrre molti risultati e impiegare di conseguenza molto tempo.

Come per il supporto per la cancellazione, se vogliamo dare un vero feedback all’utente, dove per vero intendiamo una progress bar reale e non farlocca con IsIndeterminate=”True”, dobbiamo rendere conscia l’intera pipeline e in questo caso direi che è un lavoro pressochè inutile; possiamo però fare comunque una cosa decisamente interessante.

L’utente cerca “se”, questo comporta che la ricerca (una sorta di Full Text) viene fatta per “*se*”, quando l’utente pigia “Cerca” il nostro bel Busy Adorner prende vita:

image

dopo 2” di attesa però qualcosa cambia:

image

Nella mia infrastruttura asincrona, e dal punto di vista dell’utilizzatore, la cosa è molto semplice:

Worker.UsingAsArgument( query )
.AndExpectingAsResult<IEnumerable<TEntity>>()
...
.OnWarningThresholdReached( TimeSpan.FromMilliseconds( 2000 ), () => { this.Dispatch( () => this.WarningThresholdReached = true ); } )

...
.Execute();

L’AsyncWorker espone un metodo ad hoc per farlo: OnWarningThresholdReached() che prende come primo parametro un TimeSpan che indica il tempo massimo dopo il quale vogliamo che il sistema generi un warning, e, come secondo parametro, un delagato (Action) che il sistema invocherà nel momento in cui l’operazione asincrona eccede il tempo limite impostato.

Dietro le quinte all’atto della Execute viene istanziato un Timer che tiene traccia di quello che sta succedendo, se il Timer scatta, prima della fine dell’operazione asincrona, viene invocato il delegato che, nell’esempio, altro non fa che triggherare la “comparsa” della scritta “L’operazione sta impiegando più tempo del previsto.” Il ViewModel che gestisce la cosa potrebbe anche decidere di cambiare, solo a questo punto, l’eseguibilità del comando annulla.

Tutto ciò mi ha anche portato a scoprire che la mia allergia da manuale delle istruzioni è particolarmente grave :-D, di primo acchito la comparsa del testo aggiuntivo non ne voleva sapere di funzionare, ergo questo banalissimo xaml non sortiva il suo effetto:

<TextBlock Margin="3,0,3,0"
           Visibility="{Binding Path=WarningThresholdReached, Converter={StaticResource booleanToHiddenConverter}}"
           FontStyle="Italic"
           Text="L'Operazione sta impegando più tempo del previsto."/>

Dopo lunghe indagini, perchè neanche il manuale delle istruzioni aiutava, scopro che:

  • Il motore di binding di Wpf prende in considerazione solo ed esclusivamente i Visual che stanno nel Visual Tree;
  • Un Visual (Child) che sta dentro un Adorner non è parte del Visual Tree a meno di non aggiungerlo esplicitamente:
public BusyAdorner( UIElement adornedElement, Object userContent )
    : base( adornedElement )
{
    this.userContent = new ContentPresenter() { Content = userContent };
    this.AddVisualChild( this.userContent );
}

Dimenticavo, quel booleanToHiddenConverter è un converter in tutto e per tutto identico al BooleanToVisibilityConverter builtin solo che mi permettere di specificare come interpretare il valore false, il converter builtin parte dal presupposto che false corrisponda a Visibility.Collapsed, mentre siccome esiste anche Hidden (che ha un comportamento ben diverso) il mio converter può essere dichiarato così:

<local:BooleanToVisibilityConverter FalseValue="Hidden" x:Key="elementKey" />

.m