Mejorando la “responsividad”: asyc y await, AsyncLazy<T> y MVVM asíncrono (NotifyTaskCompletion<T>)

Muy buenas,

Hoy me gustaría comentar algunos “Tips“ que creo, deberíamos conocer cuando desarrollamos aplicaciones WinRT, o incluso cuando desarrollamos casi cualquier aplicación .NET, al menos 4.0 o superior.

En primer lugar, simplemente recordar / repasar el patrón async y await, que cada día cobra más y más importancia debido a los dispositivos móviles y a las aplicaciones responsivas.

  • Se trata de un patrón que hace más ágil la interacción con  nuestras aplicaciones evitando incluso alguna larga espera (“congelación”) de la misma durante su ejecución.
  • Permite el lanzamiento de operaciones asíncronas sin bloqueos en la Interfaz de Usuario
  • Su implementación es muy sencilla y mantiene un código legible.
  • Se introdujo a partir de NetFX 4.5 / Windows Phone 8 y WinRT.
  • Podemos utilizarlo en NETFX 4.0 / Windows Phone 7.1 / Silverlight 4 / MonoTouch / MonoDroid  y  Librerías de clases Portables, con Visual Studio 2012 o posterior  y el paguete NuGet, Microsoft.Bcl.Async.

Nota: Asyn no implica necesariamente el uso de hilos (threads), esto es opcional.

Los métodos de pruebas unitarias (Tests), tendrán que indicarse como “Public async Task TestMethod1() { … }”. ¡No sé porqué, pero siempre se me olvida. Y hasta que no pretendo lanzarlos y no los veo en la consola, no lo recuerdo! grrr…

 

En segundo lugar. Para retrasar la carga de un costoso consumo de recursos, hasta el momento en el que sea realmente necesario su uso. Utilizaremos el patrón de instanciación perezosa, o, mas comúnmente conocido como ” Lazy<T>. Sin embargo, si para dicha carga se requiere no bloquear la UI, necesitaremos por tanto, un método asíncrono, con lo que perderemos de vista el objetivo principal del patrón Lazy. Éste, explicitamente no lo permite.

Por ejemplo, si en WinRT queremos ejecutar la instrucción

this.folder = new Lazy<StorageFolder>(() => this.CreateFolderIfNotExistsAsync(folderName));

Podemos pensar  en cambiarla para que sea asíncrona:

this.folder = new Lazy<StorageFolder>(async () => this.CreateFolderIfNotExistsAsync(folderName));”

No obstante, obtendremos un error en tiempo de diseño: “Cannot convert lambda expression to type ‘System.Threading.LazyThreadSafeMode’ because it is not a delegate type.

Podemos seguir intentándolo, pero en lugar complicar el código, echemos un vistazo a la siguiente clase:

1 public class AsyncLazy<T> : Lazy<Task<T>> 2 { 3 public AsyncLazy(Func<T> valueFactory) : 4 base(() => Task.Factory.StartNew(valueFactory)) { } 5 6 public AsyncLazy(Func<Task<T>> taskFactory) : 7 base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap()) { } 8 9 public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); } 10 }

Gracias a ella y de manera muy sencilla, nuestra instanciación perezosa se ejecutará de manera asíncrona y no bloqueará el UI.

this.folder = new AsyncLazy<StorageFolder>(() => this.CreateFolderIfNotExistsAsync(folderName));

 

Por último. Cuando trabajamos con WinRT siguiendo el patrón MVVM y queremos abrir una nueva ventana/pantalla, los enlaces a datos (o Bindings) se realizan de manera síncrona y automática en el momento de la carga de esa nueva ventana. Puede ocurrir por tanto, que el tiempo de espera se vea incrementado dando la sensación de una aplicación poco responsiva. Si además, la ventana es nuestro “Home”, el impacto en el usuario puede ser mayor. Para evitarlo, entre otras opciones, la clase “NotifyTaskCompletion<T>”, como la siguiente, puede ayudarnos:

1 public sealed class NotifyTaskCompletion<TResult> : INotifyPropertyChanged 2 { 3 public NotifyTaskCompletion(Task<TResult> task) 4 { 5 Task = task; 6 if (!task.IsCompleted) 7 { 8 var watcher = WatchTaskAsync(task); 9 } 10 } 11 private async Task WatchTaskAsync(Task task) 12 { 13 try 14 { 15 await task; 16 } 17 catch 18 { 19 } 20 21 var propertyChanged = PropertyChanged; 22 if (propertyChanged == null) return; 23 24 propertyChanged(this, new PropertyChangedEventArgs("Status")); 25 propertyChanged(this, new PropertyChangedEventArgs("IsCompleted")); 26 propertyChanged(this, new PropertyChangedEventArgs("IsNotCompleted")); 27 if (task.IsCanceled) 28 { 29 propertyChanged(this, new PropertyChangedEventArgs("IsCanceled")); 30 } 31 else if (task.IsFaulted) 32 { 33 propertyChanged(this, new PropertyChangedEventArgs("IsFaulted")); 34 propertyChanged(this, new PropertyChangedEventArgs("Exception")); 35 propertyChanged(this, new PropertyChangedEventArgs("InnerException")); 36 propertyChanged(this, new PropertyChangedEventArgs("ErrorMessage")); 37 } 38 else 39 { 40 propertyChanged(this, new PropertyChangedEventArgs("IsSuccessfullyCompleted")); 41 propertyChanged(this, new PropertyChangedEventArgs("Result")); 42 } 43 } 44 45 public Task<TResult> Task { get; private set; } 46 public TResult Result 47 { 48 get 49 { 50 return (Task.Status == TaskStatus.RanToCompletion) ? Task.Result : default(TResult); 51 } 52 } 53 public TaskStatus Status { get { return Task.Status; } } 54 public bool IsCompleted { get { return Task.IsCompleted; } } 55 public bool IsNotCompleted { get { return !Task.IsCompleted; } } 56 57 public bool IsSuccessfullyCompleted 58 { 59 get 60 { 61 return Task.Status == TaskStatus.RanToCompletion; 62 } 63 } 64 public bool IsCanceled { get { return Task.IsCanceled; } } 65 public bool IsFaulted { get { return Task.IsFaulted; } } 66 public AggregateException Exception { get { return Task.Exception; } } 67 public Exception InnerException 68 { 69 get 70 { 71 return (Exception == null) ? null : Exception.InnerException; 72 } 73 } 74 public string ErrorMessage 75 { 76 get 77 { 78 return (InnerException == null) ? null : InnerException.Message; 79 } 80 } 81 public event PropertyChangedEventHandler PropertyChanged; 82 }

La usaremos en uno de los métodos de inicio (o constructor),  de nuestra pantalla, de la siguiente manera:

this.MyListAsync = new NotifyTaskCompletion<Project>(this.service.GetProject(projectId));

En el XAML realizaremos el enlace con la instrucción: “MyListAsync.Result”:

1 <ListView Grid.Row="1" x:Name="listViewTasks" 2 Margin="10,0,5,0" 3 ItemsSource="{Binding MyListAsync.Result, Mode=TwoWay}" 4 ItemTemplate="{StaticResource SmallImageDetailTemplate}" >

Y, por ejemplo, habilitaremos o no un botón de edición sólo si la carga se ha realizado con éxito.

1 <Button Content="&#xE104;" Margin="10,0,0,0" 2 FontSize="18" FontFamily="Segoe Ui Symbol" 3 Visibility="{Binding ProjectAsync.IsSuccessfullyCompleted, Converter={StaticResource BooleanToVisibilityConverter}}" 4 Command="{Binding EditProjectCommand}" />

Así mismo y aunque algo menos elegante, podríamos interactuar en el ViewModel de la siguiente manera, o incluso exponiendo eventos tales como “NotifySuccessfullyCompleted, “NotifyFaulted” y “”NotifyCanceled” dentro de la clase NotifyTaskCompletion<T>.

1 this.ProjectAsync.PropertyChanged += (sender, e) => 2 { 3 if (e.PropertyName == "IsSuccessfullyCompleted") 4 { 5 // Iniciar, cargar o ejecutar métodos una 6 // vez obtenido un proyecto y toda su información 7 } 8 else if (e.PropertyName == "IsFaulted") 9 { 10 // Tratar casos de error 11 } 12 };

Referencias:

Espero que estos cuantos Tips hayan sido de utilidad.

Saludos desde lo que ha sido un gran puente del Corpus: Cervezas, buen pescadito, descanso y playa. Risa

Juanlu,ElGuerre