2013-02-21 34 views
20

Nếu một Phương pháp không đồng bộ mà tôi muốn kích hoạt bên trong IValueConverter.Thực hiện Async của IValueConverter

Có thời gian chờ tốt hơn sau đó buộc phải đồng bộ hóa bằng cách gọi thuộc tính kết quả không?

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"); 
    } 
} 

Trả lời

34

Có thể bạn không muốn gọi Task.Result, vì một vài lý do.

Thứ nhất, khi tôi giải thích chi tiết trên blog của mình, you can deadlock trừ khi mã async của bạn đã được viết bằng cách sử dụng ConfigureAwait ở mọi nơi. Thứ hai, có thể bạn không muốn (đồng bộ) chặn giao diện người dùng của mình; sẽ tốt hơn nếu tạm thời hiển thị hình ảnh "tải ..." hoặc trống trong khi đọc từ đĩa và cập nhật khi đọc xong.

Vì vậy, cá nhân, tôi sẽ làm cho phần này của ViewModel của tôi, không phải là công cụ chuyển đổi giá trị. Tôi có một bài đăng trên blog mô tả một số databinding-friendly ways to do asynchronous initialization. Đó sẽ là lựa chọn đầu tiên của tôi. Nó không cảm thấy đúng khi có một bộ chuyển đổi giá trị khởi động các hoạt động nền không đồng bộ.

Tuy nhiên, nếu bạn đã xem thiết kế của mình và thực sự nghĩ rằng trình chuyển đổi giá trị không đồng bộ là những gì bạn cần, thì bạn phải có chút sáng tạo. Vấn đề với bộ chuyển đổi giá trị là chúng để đồng bộ: bắt buộc dữ liệu bắt đầu ở ngữ cảnh dữ liệu, đánh giá đường dẫn và sau đó gọi chuyển đổi giá trị. Chỉ bối cảnh dữ liệu và thông báo thay đổi hỗ trợ đường dẫn.

Vì vậy, bạn phải sử dụng bộ chuyển đổi giá trị (đồng bộ) trong ngữ cảnh dữ liệu để chuyển đổi giá trị ban đầu thành đối tượng Task thân thiện với dữ liệu và sau đó liên kết thuộc tính của bạn chỉ sử dụng một trong các thuộc tính trên Task để có được kết quả.

Dưới đây là một ví dụ về những gì tôi có nghĩa là:

<TextBox Text="" Name="Input"/> 
<TextBlock DataContext="{Binding ElementName=Input, Path=Text, Converter={local:MyAsyncValueConverter}}" 
      Text="{Binding Path=Result}"/> 

Các TextBox chỉ là một hộp đầu vào. Trước tiên, TextBlock đặt DataContext riêng thành văn bản đầu vào của TextBox chạy nó thông qua trình chuyển đổi "không đồng bộ". TextBlock.Text được đặt thành Result của trình chuyển đổi đó.

Bộ chuyển đổi là khá đơn giản:

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; 
    } 
} 

Bộ chuyển đổi đầu tiên bắt đầu một hoạt động không đồng bộ để chờ 5 giây và sau đó thêm "đã hoàn tất!" đến cuối chuỗi đầu vào. Kết quả của bộ chuyển đổi không thể chỉ đơn giản là TaskTask không triển khai IPropertyNotifyChanged, vì vậy tôi đang sử dụng loại sẽ có trong bản phát hành tiếp theo của AsyncEx library. Nó trông giống như sau (đơn giản hóa ví dụ này; 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; 
} 

Bằng cách đặt những mảnh lại với nhau, chúng tôi đã tạo một bối cảnh dữ liệu không đồng bộ mà là kết quả của một chuyển đổi giá trị. Trình bao bọc Task thân thiện với dữ liệu sẽ chỉ sử dụng kết quả mặc định (thường là null hoặc 0) cho đến khi hoàn tất Task. Vì vậy, các wrapper của Result là khá khác nhau hơn Task.Result: nó sẽ không đồng bộ khối và không có nguy cơ bế tắc.

Nhưng để nhắc lại: Tôi muốn đặt logic không đồng bộ vào ViewModel thay vì một trình chuyển đổi giá trị.

+0

Xin chào Cảm ơn bạn đã trả lời yoru. Làm cho hoạt động async trong viewmodel thực sự là giải pháp mà tôi hiện đang có để giải quyết. nhưng cảm giác này rất đẹp. Có một số mối quan tâm mà tôi cảm thấy họ đã đúng trong một công cụ chuyển đổi. Tôi hy vọng rằng tôi bỏ qua một cái gì đó giống như một IAsyncValueConverter. Nhưng có vẻ như không có gì giống như vậy :-( Sẽ đánh dấu bài đăng của bạn mặc dù là câu trả lời vì tôi nghĩ nó sẽ giúp một số người khác có cùng vấn đề :-) –

+0

Rất hay, nhưng tôi muốn hỏi bạn một câu hỏi : tại sao trình biến đổi phải mở rộng 'MarkupExtension' và tại sao' ProvideValue' trả về chính nó? – Alberto

+1

@Alberto: Nó chỉ là một tiện ích XAML, do đó bạn không phải khai báo một cá thể toàn cục trong từ điển tài nguyên và tham khảo nó từ đánh dấu của bạn. –

Các vấn đề liên quan