Ответ 1
Вероятно, вы не хотите вызывать Task.Result
по нескольким причинам.
Во-первых, поскольку я подробно объясняю свой блог, вы можете зайти в тупик, если ваш код async
не был написан с помощью ConfigureAwait
везде. Во-вторых, вы, вероятно, не хотите (синхронно) блокировать свой пользовательский интерфейс; было бы лучше временно показать "загрузку..." или пустое изображение при чтении с диска и обновить, когда чтение будет завершено.
Итак, лично я бы сделал эту часть своего ViewModel, а не преобразователя значений. У меня есть сообщение в блоге с описанием дружественных к базе данных способов для асинхронной инициализации. Это был бы мой первый выбор. Просто не кажется правильным, чтобы преобразователь значений начинал выполнять асинхронные фоновые операции.
Однако, если вы рассмотрели свой дизайн и действительно думаете, что конвертер асинхронных значений - это то, что вам нужно, тогда вам нужно немного изобретать. Проблема со значениями преобразователей заключается в том, что они должны быть синхронными: привязка данных начинается в контексте данных, оценивает путь и затем вызывает преобразование значений. Только уведомления об изменениях данных и пути поддерживают уведомления об изменениях.
Итак, вы должны использовать (синхронный) конвертер значений в контексте данных, чтобы преобразовать исходное значение в дружественный databinding Task
-подобный объект, а затем привязка вашего свойства использует только одно из свойств на Task
-like, чтобы получить результат.
Вот пример того, что я имею в виду:
<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 секунд, а затем добавить "done!" до конца входной строки. Результат конвертера не может быть просто простым 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;
}
Объединив эти фрагменты, мы создали асинхронный контекст данных, который является результатом преобразователя значений. Дружественный databinding Task
-сервер будет использовать результат по умолчанию (обычно null
или 0
) до тех пор, пока Task
не завершится. Таким образом, оболочка Result
сильно отличается от Task.Result
: она не будет блокироваться синхронно и нет опасности тупика.
Но повторить: я бы поставил асинхронную логику в ViewModel, а не в конвертер значений.