我有一个异步方法,我想在一个叫做 IValueConverter
.
有没有更好的方法,而不是通过调用 Result
属性?
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");
}
}
你可能不想叫 Task.Result
有几个原因。
首先,正如我在博客上详细解释的那样。僵持不下 除非你 async
代码是已经写好的,使用 ConfigureAwait
遍地都是。其次,你可能不想(同步)屏蔽你的UI;最好是在从磁盘读取时暂时显示一个 "加载... "或空白图像,当读取完成后再更新。
所以,就我个人而言,我会把这个作为我的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秒 然后在输入字符串的末尾加上 "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;
}
通过把这些部件放在一起,我们已经创建了一个异步数据上下文,这是一个值转换器的结果。数据绑定友好的 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;
}
}
}
此外,你可以在图像上实现加载活动指示器,直到任务将被解决。