il blog di Marco Amendola

ottobre 2012 Blog Posts

Le Coroutine sono morte. Lunga vita alle Coroutine.

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]