2013-02-21 76 views
20

如果我想在IValueConverter内部触发一个异步方法。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"); 
    } 
} 

回答

34

您可能不想拨打Task.Result,原因有两个。

首先,正如我在我的博客上详细解释的,you can deadlock除非您的async代码已被写入使用ConfigureAwait无处不在。其次,你可能不想(同步)阻止你的用户界面;在从磁盘读取数据时暂时显示“正在加载...”或空白图像并在读取完成时更新会更好。

所以,我个人认为我的ViewModel的这一部分不是价值转换器。我有一篇博客文章描述了一些databinding-friendly ways to do asynchronous initialization。那将是我的第一选择。如果有一个价值转换器启动了异步后台操作,那不太合适。然而,如果你已经考虑过你的设计,并且真的认为你需要一个异步值转换器,那么你必须有点创新。值转换器的问题是它们的具有是同步的:数据绑定从数据上下文开始,评估路径,然后调用值转换。只有数据上下文和路径支持才能更改通知。

因此,您必须在您的数据上下文中使用(同步)值转换器将您的原始值转换为类似数据绑定的类对象,然后您的属性绑定仅使用类似于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 library的下一个版本中。它看起来像这样(简化这个例子; full source is available):

// 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包装将只使用默认结果(通常为null0),直到Task完成。所以包装的ResultTask.Result完全不同:它不会同步阻塞,也没有死锁的危险。

但要重申:我会选择将异步逻辑放入ViewModel而不是值转换器。

+0

嗨感谢您的回复。在viewmodel中进行异步操作确实是我目前作为解决方法的解决方案。但是这感觉非常好。有一些担心,我觉得他们在转换器是正确的。我希望我忽略了像IAsyncValueConverter这样的东西。但似乎没有这样的事情:-( 将标记您的帖子,虽然作为一个答案,因为我认为它会帮助一些其他人有同样的问题:-) – 2013-02-21 16:57:48

+0

非常好,但我想问你一个问题:为什么转换器应该扩展'MarkupExtension'和为什么'ProvideValue'返回自己? – Alberto 2013-10-15 11:09:53

+1

@Alberto:这只是一个XAML便利,因此您不必在资源字典中声明全局实例并从标记中引用它。 – 2013-10-15 12:22:49