mvvm - ¿La forma correcta de llamar a métodos asíncronos desde un definidor de propiedades enlazado a datos?
windows-runtime async-await (2)
Tengo un tipo NotifyTaskCompletion
en mi biblioteca AsyncEx que es esencialmente un contenedor INotifyPropertyChanged
para Task
/ Task<T>
. AFAIK, actualmente hay muy poca información disponible sobre async
combinado con MVVM, así que avíseme si encuentra otros enfoques.
De todos modos, el enfoque NotifyTaskCompletion
funciona mejor si sus tareas devuelven sus resultados. Es decir, a partir de su ejemplo de código actual, parece que GetFeedArticles
está configurando propiedades enlazadas a datos como un efecto secundario en lugar de devolver los artículos. Si realiza esta Task<T>
retorno Task<T>
, puede terminar con un código como este:
private Feed selectedFeed;
public Feed SelectedFeed
{
get
{
return this.selectedFeed;
}
set
{
if (this.selectedFeed == value)
return;
this.selectedFeed = value;
RaisePropertyChanged();
Articles = NotifyTaskCompletion.Create(GetFeedArticlesAsync(value.Id));
}
}
private INotifyTaskCompletion<List<Article>> articles;
public INotifyTaskCompletion<List<Article>> Articles
{
get { return this.articles; }
set
{
if (this.articles == value)
return;
this.articles = value;
RaisePropertyChanged();
}
}
private async Task<List<Article>> GetFeedArticlesAsync(int id)
{
...
}
Luego, su enlace de datos puede usar Articles.Result
para acceder a la colección resultante (que es null
hasta que GetFeedArticlesAsync
). También puede utilizar NotifyTaskCompletion
"out of the box" para enlazar datos a errores (por ejemplo, Articles.ErrorMessage
) y tiene algunas propiedades de conveniencia booleanas ( IsSuccessfullyCompleted
, IsFaulted
) para manejar los IsFaulted
de visibilidad.
Tenga en cuenta que esto manejará correctamente las operaciones completadas fuera de orden. Dado que los Articles
representan la operación asíncrona en sí (en lugar de los resultados directamente), se actualiza inmediatamente cuando se inicia una nueva operación. Así que nunca verás resultados desactualizados.
No tiene que usar el enlace de datos para su manejo de errores. Puede crear la semántica que desee modificando GetFeedArticlesAsync
; por ejemplo, para manejar las excepciones pasándolas a su MessengerInstance
:
private async Task<List<Article>> GetFeedArticlesAsync(int id)
{
try
{
...
}
catch (Exception ex)
{
MessengerInstance.Send<string>("Error description", "DisplayErrorNotification");
return null;
}
}
Del mismo modo, no hay una noción de cancelación automática incorporada, pero nuevamente es fácil de agregar a GetFeedArticlesAsync
:
private CancellationTokenSource getFeedArticlesCts;
private async Task<List<Article>> GetFeedArticlesAsync(int id)
{
if (getFeedArticlesCts != null)
getFeedArticlesCts.Cancel();
using (getFeedArticlesCts = new CancellationTokenSource())
{
...
}
}
Esta es un área de desarrollo actual, así que por favor haga mejoras o sugerencias de API.
Ahora sé que las propiedades no son compatibles con async / await por buenas razones. Pero a veces es necesario iniciar un procesamiento en segundo plano adicional desde un establecedor de propiedades; un buen ejemplo es el enlace de datos en un escenario MVVM.
En mi caso, tengo una propiedad que está vinculada al SelectedItem de un ListView. Por supuesto, inmediatamente establezco el nuevo valor en el campo de respaldo y se realiza el trabajo principal de la propiedad. Pero el cambio del elemento seleccionado en la interfaz de usuario también debe activar una llamada de servicio REST para obtener algunos datos nuevos basados en el elemento seleccionado ahora.
Así que necesito llamar a un método asíncrono. No puedo esperar, obviamente, pero tampoco quiero disparar y olvidar la llamada, ya que podría pasar por alto las excepciones durante el procesamiento asíncrono.
Ahora mi toma es la siguiente:
private Feed selectedFeed;
public Feed SelectedFeed
{
get
{
return this.selectedFeed;
}
set
{
if (this.selectedFeed != value)
{
this.selectedFeed = value;
RaisePropertyChanged();
Task task = GetFeedArticles(value.Id);
task.ContinueWith(t =>
{
if (t.Status != TaskStatus.RanToCompletion)
{
MessengerInstance.Send<string>("Error description", "DisplayErrorNotification");
}
});
}
}
}
Ok, además del hecho de que podría mover el manejo del setter a un método sincrónico, ¿es esta la forma correcta de manejar este escenario? ¿Hay una solución mejor y menos abarrotada que no veo?
Estaría muy interesado en ver otras tomas sobre este problema. Siento un poco de curiosidad por no haber podido encontrar otras discusiones sobre este tema concreto, ya que me parece muy común en las aplicaciones de MVVM que hacen un uso intensivo del enlace de datos.
public class AsyncRunner
{
public static void Run(Task task, Action<Task> onError = null)
{
if (onError == null)
{
task.ContinueWith((task1, o) => { }, TaskContinuationOptions.OnlyOnFaulted);
}
else
{
task.ContinueWith(onError, TaskContinuationOptions.OnlyOnFaulted);
}
}
}
Uso dentro de la propiedad.
private NavigationMenuItem _selectedMenuItem;
public NavigationMenuItem SelectedMenuItem
{
get { return _selectedMenuItem; }
set
{
_selectedMenuItem = val;
AsyncRunner.Run(NavigateToMenuAsync(_selectedMenuItem));
}
}
private async Task NavigateToMenuAsync(NavigationMenuItem newNavigationMenu)
{
//call async tasks...
}