2013-02-21 12 views
20

Wenn eine asynchrone Methode, die ich in einem IValueConverter auslösen möchte.Async-Implementierung von IValueConverter

Gibt es eine bessere Wartezeit, dann erzwingen Sie es synchron zu sein, indem Sie die result-Eigenschaft aufrufen?

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

Antwort

34

Sie wollen wahrscheinlich nicht Task.Result nennen, für ein paar Gründe.

Erstens, wie ich im Detail auf meinem Blog, you can deadlock erklären, es sei denn Ihre async Code wurde mit ConfigureAwait überall geschrieben. Zweitens möchten Sie Ihre Benutzeroberfläche wahrscheinlich nicht (synchron) blockieren. Es wäre besser, während des Lesens von der Diskette vorübergehend ein "Laden ..." oder ein leeres Bild anzuzeigen und zu aktualisieren, wenn der Lesevorgang abgeschlossen ist.

Also, persönlich würde ich diesen Teil meines ViewModel machen, kein Wertkonverter. Ich habe einen Blogeintrag, der einige databinding-friendly ways to do asynchronous initialization beschreibt. Das wäre meine erste Wahl. Es fühlt sich einfach nicht richtig an, einen Wertkonverter zu haben, der asynchrone Hintergrundoperationen startet.

Wenn Sie jedoch über Ihr Design nachgedacht haben und wirklich denken, dass ein asynchroner Wertkonverter das ist, was Sie brauchen, dann müssen Sie ein wenig erfinderisch werden. Das Problem mit Wertkonvertern ist, dass sie synchron sein müssen: Die Datenbindung beginnt am Datenkontext, wertet den Pfad aus und ruft dann eine Wertkonvertierung auf. Nur der Datenkontext und die Pfadunterstützung ändern Benachrichtigungen.

Also, müssen Sie ein (synchron) Werteumwandler in Ihrem Datenkontext verwenden, um Ihren ursprünglichen Wert in eine Datenbindung freundliche Task -ähnlichen Objekt zu konvertieren und dann die Bindung Ihrer Eigenschaft verwendet, nur eine der Eigenschaften auf den Task -ähnlichen Objekt, um das Ergebnis zu erhalten.

Hier ist ein Beispiel dafür, was ich meine:

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

Die TextBox ist nur ein Eingabefeld ein. Die TextBlock setzt zuerst ihren eigenen DataContext zu dem TextBox 's Eingangstext, der es durch einen "asynchronen" Konverter führt. TextBlock.Text wird auf Result dieses Konverters gesetzt.

Der Konverter ist ziemlich einfach:

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

Der Konverter zunächst einen asynchronen Vorgang beginnt 5 Sekunden zu warten, und fügen Sie dann „fertig!“ bis zum Ende der Eingabezeichenfolge. Das Ergebnis des Konverters kann nicht einfach Task sein, weil TaskIPropertyNotifyChanged nicht implementiert, also verwende ich einen Typ, der in der nächsten Version meines AsyncEx library sein wird. Es sieht ungefähr so ​​aus (für dieses Beispiel vereinfacht; 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; 
} 

Durch diese Stücke zusammen setzen, haben wir einen asynchronen Datenkontext erstellt, die das Ergebnis eines Wertwandler ist. Der Datenbindung-freundliche Task Wrapper wird nur das Standardergebnis (normalerweise null oder) verwenden, bis die Task abgeschlossen ist. So ist der Wrapper Result ganz anders als Task.Result: es wird nicht synchron blockieren und es gibt keine Gefahr von Deadlock.

Aber um es noch einmal zu wiederholen: Ich würde asynchrone Logik lieber in das ViewModel einfügen als in einen Wertkonverter.

+0

Hallo Danke für deine Antwort. Die async-Operation in Viewmodel ist in der Tat die Lösung, die ich derzeit als Workaround habe. aber das fühlt sich sehr gut an. Es gibt einige Bedenken, für die ich glaube, dass sie in einem Konverter richtig waren. Ich hoffte, dass ich etwas wie einen IAsyncValueConverter übersehen habe. Aber es scheint, als gäbe es nichts in der Art :-(. Wird deinen Beitrag als Antwort markieren, weil ich denke, dass es anderen Leuten mit gleichen Problemen helfen wird :-) –

+0

Sehr nett, aber ich möchte dir eine Frage stellen : Warum sollte der Konverter 'MarkupExtension' erweitern und warum' ProvideValue' selbst zurückgibt? – Alberto

+1

@Alberto: Es ist nur eine XAML-Funktion, so dass Sie keine globale Instanz in einem Ressourcenwörterbuch deklarieren müssen und es von Ihrem Markup aus referenzieren müssen. –

Verwandte Themen