Асинхронная реализация 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;
            });
        }
    }
}
Другие вопросы по тегам