domingo, 7 de junio de 2015

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.

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

Etiquetas: , , ,


This page is powered by Blogger. Isn't yours?