Асинхронная реализация IValueConverter
Если асинхронный метод, который я хочу вызвать внутри IValueConverter.
Есть ли лучшее ожидание, чем заставить его быть синхронным, вызывая свойство результата?
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");
}
}
1 ответ
Вы, вероятно, не хотите звонить Task.Result
по нескольким причинам.
Во-первых, как я подробно объясняю в моем блоге, вы можете зайти в тупик, если ваш async
код был написан с использованием ConfigureAwait
везде. Во-вторых, вы, вероятно, не хотите (синхронно) блокировать ваш пользовательский интерфейс; было бы лучше временно показать "загрузку..." или пустое изображение во время чтения с диска и обновить после завершения чтения.
Так что лично я сделал бы эту часть моей ViewModel, а не конвертер значений. У меня есть сообщение в блоге, описывающее некоторые дружественные для данных способы выполнения асинхронной инициализации. Это был бы мой первый выбор. Просто нехорошо иметь конвертер значений, запускающий асинхронные фоновые операции.
Однако, если вы продумали свой дизайн и действительно считаете, что асинхронный преобразователь значений - это то, что вам нужно, то вам нужно немного изобретательнее. Проблема с преобразователями значений заключается в том, что они должны быть синхронными: привязка данных начинается с контекста данных, оценивает путь и затем вызывает преобразование значений. Только контекст данных и поддержка пути уведомлений об изменениях.
Таким образом, вы должны использовать (синхронный) преобразователь значений в контексте данных, чтобы преобразовать исходное значение в ориентированное на данные Task
-подобный объект, а затем привязка вашего свойства просто использует одно из свойств на Task
объект, чтобы получить результат.
Вот пример того, что я имею в виду:
<TextBox Text="" Name="Input"/>
<TextBlock DataContext="{Binding ElementName=Input, Path=Text, Converter={local:MyAsyncValueConverter}}"
Text="{Binding Path=Result}"/>
TextBox
это просто поле ввода. TextBlock
сначала устанавливает свой собственный DataContext
к TextBox
Ввод текста выполняется через "асинхронный" преобразователь. TextBlock.Text
установлен на Result
этого конвертера.
Конвертер довольно прост:
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;
}
}
Преобразователь сначала запускает асинхронную операцию, чтобы подождать 5 секунд, а затем добавляет "готово!" до конца входной строки. Результат конвертера не может быть простым Task
так как Task
не реализует IPropertyNotifyChanged
, поэтому я использую тип, который будет в следующей версии моей библиотеки AsyncEx. Это выглядит примерно так (упрощенно для этого примера; доступен полный исходный код):
// 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;
}
Соединяя эти части вместе, мы создали асинхронный контекст данных, который является результатом преобразования значений. Привязка к данным Task
Оболочка будет просто использовать результат по умолчанию (обычно null
или же 0
) до Task
завершается. Так что обертка Result
довольно сильно отличается от Task.Result
: он не будет синхронно блокироваться и нет опасности тупика.
Но повторюсь: я бы предпочел поместить асинхронную логику в ViewModel, а не в преобразователь значений.
Альтернативный подход - создать собственный элемент управления, поддерживающий асинхронные источники или данные.
Вот пример с изображением
public class AsyncSourceCachedImage : CachedImage
{
public static BindableProperty AsyncSourceProperty = BindableProperty.Create(nameof(AsyncSource), typeof(Task<Xamarin.Forms.ImageSource>), typeof(AsyncSourceSvgCachedImage), null, propertyChanged: SourceAsyncPropertyChanged);
public Task<Xamarin.Forms.ImageSource> AsyncSource
{
get { return (Task<Xamarin.Forms.ImageSource>)GetValue(AsyncSourceProperty); }
set { SetValue(AsyncSourceProperty, value); }
}
private static async void SourceAsyncPropertyChanged(BindableObject bindable, object oldColor, object newColor)
{
var view = bindable as AsyncSourceCachedImage;
var taskForImageSource = newColor as Task<Xamarin.Forms.ImageSource>;
if (taskForImageSource != null)
{
var awaitedImageSource = await taskForImageSource;
view.Source = awaitedImageSource;
}
}
}
Более того, вы можете реализовать индикатор активности загрузки над изображением, пока задача не будет решена.
Код ниже идеально подходит для меня.
Example.xaml
<Window xmlns:converters = "namespace your converter"
<Grid>
<Grid.Resources>
<converters:ExampleConverterWithAsync x:Name = "TestConverter"/>
</Grid.Resources>
<TextBlock DataContext="{Binding YourNeedsItem, Converter={StaticResource TestConverter}}" Text="{Binding AsyncValue}"/>
</Grid>
</Window>
ExampleConverterWithAsync.cs
public class ExampleConverterWithAsync : IValueConverter
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return new TaskNotifier<string>(Task.run(async() =>
{
Task.Delay(1_000); // replace your job
return "Hello World";
}));
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => Binding.DoNothing;
public class TaskNotifier<T> : ViewModelBase
where T : class
{
private T _asyncValue;
public T AsyncValue
{
get => _asyncValue;
set { this.SetValue(ref _asyncValue, value); }
}
public TaskNotifier(Task<T> valueFunc)
{
Task.Run(async () =>
{
AsyncValue = await valueFunc;
});
}
}
}