il blog di Marco Amendola

Le Coroutine sono morte. Lunga vita alle Coroutine. Oct 17

Come alcuni di voi possono aver già sentito, Caliburn.Micro è stato recentemente convertito a WinRT grazie a Nigel Sampson e Keith Patton.

Sia che facciate già sviluppo per Windows 8, sia che stiate lavorando con WPF ed il framework .NET 4.5, è probabile che siate tutti presi a giocare lavorando molto proficuamente con il potente supporto per la programmazione asincrona presente in Visual Studio 2012. (Non ancora? Correte a studiarlo ORA).

Per chi ha utilizzato Caliburn e Caliburn.Micro, una forma di programmazione asincrona molto pulita e ordinata era già disponibile sin dal 2009: date un’occhiata a questo post di Rob che presenta IResult e le Coroutine, un anno prima di Async CTP!

Adesso comunque il paradigma async/await è destinato a diventare di comune utilizzo; significa che IResult e le Coroutine sono destinate a diventare superflue?
La mia opinione inziale era che potessero essere mantenute solo per compatibilità, ma in realtà si sono dimostrate ancora piuttosto importanti, per una serie di ragioni.

Accesso al contesto di esecuzione

In numerosi scenari l’azione definita nel ViewModel richiede di ottenere un accesso diretto alla Vies o all’elemento che ha scatenato l’azione stessa.
L’istanza della View può essere ottenuta usando il metodo GetView della classe Screen (o implementando direttamente IViewAware), ma l’elemento “scatenante” non è altrettanto facile da ottenere.

Inoltre, mentre in alcuni casi isolati l’utilizzo di un approccio “stile Model-View-Presenter” è l’unica soluzione praticabile, in generale i ViewModel non dovrebbero comandare la UI in maniera diretta, anzi non dovrebbero occuparsi affatto di affari riguardanti la mera presentazione.

Implementazioni personalizzate di IResult, invece, consentono un facile accesso ad un oggetto di tipo ActionExecutionContext, che raggruppa tutti gli oggetti coinvolti nell’esecuzione di una azione: la View corrente, l’elemento di UI “scatenante”, l’istanza del ViewModel corrente, il metodo invocato, ecc.
Così, mentre effettivamente creare IResult ad-hoc per ciascuna semplice operazione asincrona è decisamente noioso, utilizzare IResult riusabili per isolare particolari aspetti di UI risulta molto efficace, e aiuta a mantenere pulito il codice del ViewModel.

Testabilità

E’ assolutamente possibile testare del codice asincrono scritto con async/await, generalmente rimpiazzando i servizi e le dipendenze con dei mock.
In altrernativa, utilizzare una coroutine che restituisca IEnumerable<IResult> consente di testare solamente la sequenza restituita, senza eseguire realmente i passi rappresentati dagli IResult. 

“Contenimento” dell’esecuzione di una azione

Caliburn.Micro può mappare (attraverso convenzioni oppure utilizzando una sintassi esplicita) un evento che si verifica all’interno della UI verso la chiamata ad un metodo dalla parte del ViewModel.
Di conseguenza è molto semplice convertire un semplice metodo void in un metodo asincrono (async void oppure async Task) e lasciare che Caliburn.Micro si occupi del suo avvio.

Tuttavia non consiglio quiesto approccio: in tal modo, infatti, l’esecuzione dell’azione (dal punto di vista di CM) si esaurisce non appena un oggetto Task viene instanziato (ed eventualmente restituito dal metodo), mentre l’effettiva esecuzione richede generalmente molto più tempo. Inoltre, eventuali eccezioni scatenate durante l’esecuzione possono rimanere non osservate (vedi  http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx), poiché l’oggetto task non è più accessibile.

Per questi motivi ho scritto una semplice estensione che si occupa di queste problematiche; la strategia scelta è quella di personalizzare il codice di invocazione delle action di Caliburn.Micro, in modo da intercettari i metodi asincroni che restituiscano Task ed ospitarli all’interno di un IResult personalizzato. 
In questo modo è possibile sfuttare semplicemente l’infrastruttura esistente; in particolare, è possibile utilizzare il già presente meccanismo di notifica del completamento delle Coroutine (evento statico Coroutine.Complete), che fornisce anche informazioni circa eventuali eccezioni verificatesi nel metodo.

Di seguito il codice dell’estensione:

public static class AsyncAwaitSupport
{
    public static void Hook()
    {

        ActionMessage.InvokeAction = context =>
        {

            var values = MessageBinder.DetermineParameters(context, context.Method.GetParameters());
            var returnValue = context.Method.Invoke(context.Target, values);

            var task = returnValue as Task;
            if (task != null)
            {
                returnValue = new TaskResult(task);
            }

            var result = returnValue as IResult;
            if (result != null)
            {
                returnValue = new[] { result };
            }

            var enumerable = returnValue as IEnumerable;
            if (enumerable != null)
            {
                Coroutine.BeginExecute(enumerable.GetEnumerator(), context);
                return;
            }

            var enumerator = returnValue as IEnumerator;
            if (enumerator != null)
            {
                Coroutine.BeginExecute(enumerator, context);
                return;
            }
        };
    }

    private class TaskResult : IResult
    {
        Task task;
        public TaskResult(Task task)
        {
            if (task == null) throw new ArgumentNullException("task");
            this.task = task;
        }

        public event EventHandler Completed = delegate { };

        public void Execute(ActionExecutionContext context)
        {
            task.ContinueWith(t =>
            {
                Completed(this, new ResultCompletionEventArgs {
                                            WasCancelled = t.IsCanceled,
                                            Error = t.Exception }
                                       );
            });
        }
    }

}

L’estensione viene agganciata durante l’inizializzazione del bootstrapper:

protected override void Configure()
{
    base.Configure();

    AsyncAwaitSupport.Hook();

    //...
}

Una volta inserito il codice precedente, posso convertire il seguente codice (esempio preso da CoroutineViewModel.cs all’interno di Caliburn.Micro.WinRT.Sample):

public IEnumerable ExecuteCoroutine()
{
    yield return new VisualStateResult("Loading");
    yield return new DelayResult(2000);
    yield return new VisualStateResult("LoadingComplete");
    yield return new MessageDialogResult("This was executed from a custom IResult, MessageDialogResult.", "IResult Coroutines");
}

in qualcosa di questo tipo:

public async Task ExecuteTask()
{
    this.SetVisualStateOnView("Loading");

    await Task.Delay(2000);

    this.SetVisualStateOnView("LoadingComplete");

    //This is just a sample: I don't actually recommend calling UI code from here in real code.
    var dialog = new Windows.UI.Popups.MessageDialog("This was executed with a regular MessageDialog.", "Async/await");
    await dialog.ShowAsync();
}

[Tradotta dalla versione inglese]

Hello world Dec 06

Salve a tutti. Mi chiamo Marco Amendola e sono un nuovo inquilino di questo interessante spazio di approfondimento e condivisione.

Mi occupo, come immagino la maggior parte di voi, di progettare e sviluppare software su piattaforma Microsoft .NET.
Dopo aver lavorato a lungo su applicazioni web, negli ultimi anni ho utilizzato prevalentemente tecnologie WPF e Silverlight.
Sono particolarmente interessato al design Object Oriented e allo studio dell'architettura del software.

Dalla fine dello scorso anno sto collaborando al progetto Caliburn, un framework MVVM (e non solo...) per applicazioni WPF, Silverlight e WP7.
E' il primo progetto open source a cui contribuisco, e devo dire che si è  rivelata un'esperienza davvero entusiasmante ed istruttiva, seppure abbastanza impegnativa.

Da alcuni mesi, insieme a questo gruppo di pazzi scatenati seriosi professionisti, con cui condivido la passione per lo sviluppo di software, partecipo alla community DomusDotNet.
In questo periodo divertente e intenso ho avuto modo di conoscere la realtà veramente stimolante delle community tecniche locali, che spero di riuscire a seguire con sempre maggior frequenza.

E' principalmente questo, oltre al provvidenziale incoraggiamento di Nick, il motivo che mi ha spinto ad aprire questo blog; a questo proposito colgo l'occasione per ringraziare Andrea per aver provveduto prontamente a creare il mio account, scusandomi per aver aggiunto un ulteriore task alla sua nutrita inbox.

Non so ancora bene cosa scriverò: penso che parlerò di Silverlight, MVVM e Caliburn.Micro, che è quello di cui mi occupo quotidianamente, seguendo un po' la linea del mio blog attuale; quello di cui sono sicuro è che sarà un blog piuttosto "lento" perché il lavoro mi lascia davvero poco tempo libero.
Spero comunque di riuscire a contribuire a questo spazio con qualche argomento interessante.

A presto!