2013-02-21 3 views
20

Si une méthode asynchrone que je veux déclencher dans un IValueConverter.Implémentation asynchrone de IValueConverter

Y a-t-il une meilleure attente Attendez-vous à ce qu'il soit synchrone en appelant le résultat Propriété?

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

Répondre

34

Vous ne voulez probablement pas appeler Task.Result, pour deux raisons. Tout d'abord, comme je l'explique en détail sur mon blog, you can deadlock sauf si votre code async a été écrit en utilisant ConfigureAwait partout. Deuxièmement, vous ne voulez probablement pas (synchrone) bloquer votre interface utilisateur; il serait préférable d'afficher temporairement un "chargement ..." ou une image vide lors de la lecture du disque, et de mettre à jour lorsque la lecture est terminée. Donc, personnellement, je voudrais faire de cette partie de mon ViewModel, pas un convertisseur de valeur. J'ai un billet de blog décrivant quelques databinding-friendly ways to do asynchronous initialization. Ce serait mon premier choix. Il ne semble pas juste d'avoir un convertisseur de valeur coup d'envoi des opérations d'arrière-plan asynchrones. Cependant, si vous avez considéré votre conception et pensez vraiment qu'un convertisseur de valeur asynchrone est ce dont vous avez besoin, alors vous devez être un peu inventif. Le problème avec les convertisseurs de valeur est qu'ils ont pour être synchrone: la liaison de données démarre au contexte de données, évalue le chemin d'accès, puis appelle une conversion de valeur. Seul le contexte de données et le support de chemin modifient les notifications.

Donc, vous devez utiliser un (synchrone) convertisseur de valeur dans votre contexte de données pour convertir votre valeur initiale en un objet DataBinding convivial Task -comme puis votre propriété de liaison utilise seulement l'une des propriétés du Task -comme objet pour obtenir le résultat.

Voici un exemple de ce que je veux dire:

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

Le TextBox est juste une boîte d'entrée. Le TextBlock définit d'abord son propre DataContext au texte d'entrée TextBox en l'exécutant via un convertisseur "asynchrone". TextBlock.Text est défini sur Result de ce convertisseur.

Le convertisseur est assez simple:

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

Le convertisseur premier commence une opération asynchrone attendre 5 secondes puis ajouter « fait! » à la fin de la chaîne d'entrée. Le résultat du convertisseur ne peut pas être simplement un Task car Task n'implémente pas IPropertyNotifyChanged, donc j'utilise un type qui sera dans la prochaine version de mon AsyncEx library. Il ressemble à quelque chose comme ça (pour cet exemple simplifié, 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; 
} 

En mettant ces pièces ensemble, nous avons créé un contexte de données asynchrone qui est le résultat d'un convertisseur de valeur. L'enveloppe Task conviviale pour la connexion de données utilisera simplement le résultat par défaut (généralement null ou 0) jusqu'à ce que le Task soit terminé. Donc, le Result de l'encapsuleur est assez différent de Task.Result: il ne bloquera pas de manière synchrone et il n'y a aucun risque d'interblocage.Mais pour réitérer: je choisirais de mettre la logique asynchrone dans le ViewModel plutôt que dans un convertisseur de valeur.

+0

Salut Merci pour votre réponse. Faire l'opération asynchrone dans viewmodel est en effet la solution que j'ai actuellement comme une solution de contournement. mais c'est très agréable. Il y a quelques soucis pour lesquels je pense qu'ils avaient raison dans un convertisseur. J'espérais que j'ignorais quelque chose comme un IAsyncValueConverter. Mais il semble qu'il n'y a rien comme ça :-(. Marqueront votre message bien que comme une réponse parce que je pense qu'il aidera d'autres gars avec les mêmes problèmes :-) –

+0

Très gentil, mais je veux vous poser une question : pourquoi le convertisseur devrait étendre 'MarkupExtension' et pourquoi' ProvideValue' renvoie lui-même? – Alberto

+1

@Alberto: C'est juste une commodité XAML donc vous n'avez pas besoin de déclarer une instance globale dans un dictionnaire de ressources et de la référencer à partir de votre balisage. –

Questions connexes