c# - Implementación Async de IValueConverter
windows-runtime async-await (1)
Probablemente no desee llamar a Task.Result
, por un par de razones.
En primer lugar, como explico en detalle en mi blog, puede un punto muerto a menos que su código async
se haya escrito usando ConfigureAwait
todas partes. En segundo lugar, probablemente no desee (sincrónicamente) bloquear su UI; sería mejor mostrar temporalmente una "carga ..." o una imagen en blanco mientras lee desde el disco, y actualizar cuando finalice la lectura.
Entonces, personalmente, haría de esta parte de mi ViewModel, no un convertidor de valor. Tengo una publicación en el blog que describe algunas formas amigables de enlace de datos para hacer una inicialización asincrónica . Esa sería mi primera elección. Simplemente no se siente bien tener un convertidor de valores iniciando operaciones de fondo asincrónicas.
Sin embargo, si ha considerado su diseño y realmente piensa que un convertidor de valor asincrónico es lo que necesita, entonces tiene que ser un poco inventivo. El problema con los convertidores de valor es que tienen que ser sincrónicos: el enlace de datos comienza en el contexto de datos, evalúa la ruta y luego invoca una conversión de valor. Solo el contexto de datos y el soporte de ruta cambian las notificaciones.
Por lo tanto, debe usar un convertidor de valor (sincrónico) en su contexto de datos para convertir su valor original en un objeto similar a Task
vinculante para los datos y luego su enlace de propiedad solo usa una de las propiedades del objeto similar a la Task
para obtener el resultado.
Aquí hay un ejemplo de lo que quiero decir:
<TextBox Text="" Name="Input"/>
<TextBlock DataContext="{Binding ElementName=Input, Path=Text, Converter={local:MyAsyncValueConverter}}"
Text="{Binding Path=Result}"/>
El TextBox
es solo un cuadro de entrada. El TextBlock
primero establece su propio DataContext
en el texto de entrada del TextBox
ejecutándolo a través de un convertidor "asincrónico". TextBlock.Text
se establece en el Result
de ese convertidor.
El convertidor es bastante simple:
public class MyAsyncValueConverter : MarkupExtension, IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var val = (string)value;
var task = Task.Run(async () =>
{
await Task.Delay(5000);
return val + " done!";
});
return new TaskCompletionNotifier<string>(task);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
El convertidor primero inicia una operación asíncrona para esperar 5 segundos y luego agrega "¡listo!" hasta el final de la cadena de entrada. El resultado del convertidor no puede ser solo una Task
simple porque Task
no implementa IPropertyNotifyChanged
, entonces estoy usando un tipo que estará en la próxima versión de mi biblioteca AsyncEx . Se ve algo como esto (simplificado para este ejemplo, fuente completa está disponible ):
// Watches a task and raises property-changed notifications when the task completes.
public sealed class TaskCompletionNotifier<TResult> : INotifyPropertyChanged
{
public TaskCompletionNotifier(Task<TResult> task)
{
Task = task;
if (!task.IsCompleted)
{
var scheduler = (SynchronizationContext.Current == null) ? TaskScheduler.Current : TaskScheduler.FromCurrentSynchronizationContext();
task.ContinueWith(t =>
{
var propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs("IsCompleted"));
if (t.IsCanceled)
{
propertyChanged(this, new PropertyChangedEventArgs("IsCanceled"));
}
else if (t.IsFaulted)
{
propertyChanged(this, new PropertyChangedEventArgs("IsFaulted"));
propertyChanged(this, new PropertyChangedEventArgs("ErrorMessage"));
}
else
{
propertyChanged(this, new PropertyChangedEventArgs("IsSuccessfullyCompleted"));
propertyChanged(this, new PropertyChangedEventArgs("Result"));
}
}
},
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
scheduler);
}
}
// Gets the task being watched. This property never changes and is never <c>null</c>.
public Task<TResult> Task { get; private set; }
Task ITaskCompletionNotifier.Task
{
get { return Task; }
}
// Gets the result of the task. Returns the default value of TResult if the task has not completed successfully.
public TResult Result { get { return (Task.Status == TaskStatus.RanToCompletion) ? Task.Result : default(TResult); } }
// Gets whether the task has completed.
public bool IsCompleted { get { return Task.IsCompleted; } }
// Gets whether the task has completed successfully.
public bool IsSuccessfullyCompleted { get { return Task.Status == TaskStatus.RanToCompletion; } }
// Gets whether the task has been canceled.
public bool IsCanceled { get { return Task.IsCanceled; } }
// Gets whether the task has faulted.
public bool IsFaulted { get { return Task.IsFaulted; } }
// Gets the error message for the original faulting exception for the task. Returns <c>null</c> if the task is not faulted.
public string ErrorMessage { get { return (InnerException == null) ? null : InnerException.Message; } }
public event PropertyChangedEventHandler PropertyChanged;
}
Al unir estas piezas, creamos un contexto de datos asincrónico que es el resultado de un convertidor de valor. El contenedor de Task
amigable para Task
vinculación de datos simplemente usará el resultado predeterminado (normalmente null
o 0
) hasta que la Task
finalice. Por lo tanto, el Result
del contenedor es bastante diferente de Task.Result
: no se bloqueará sincrónicamente y no hay peligro de interbloqueo.
Pero para reiterar: elegiría poner lógica asíncrona en ViewModel en lugar de un convertidor de valor.
Si es un método asíncrono que quiero activar dentro de un IValueConverter.
¿Hay una mejor Espera luego forzando que sea sincrónica llamando al resultado Propiedad?
public async Task<object> Convert(object value, Type targetType, object parameter, string language)
{
StorageFile file = value as StorageFile;
if (file != null)
{
var image = ImageEx.ImageFromFile(file).Result;
return image;
}
else
{
throw new InvalidOperationException("invalid parameter");
}
}