Mise en œuvre de L'IValueConverter

si une méthode async que je veux déclencher à L'intérieur D'un convertisseur Ivaluec.

y a-t-il une meilleure attente que de la forcer à être synchrone en appelant la propriété 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");
    }
}
22
demandé sur svick 2013-02-21 17:44:49

1 réponses

Vous ne voulez probablement pas à appeler Task.Result, pour un couple de raisons.

tout d'Abord, comme je l'explique en détail sur mon blog, vous pouvez blocage à moins que votre async le code is a été écrit en utilisant ConfigureAwait partout. Deuxièmement, vous ne voulez probablement pas à (de façon synchrone) bloquer votre INTERFACE utilisateur; il serait préférable d'afficher temporairement un "chargement..."ou une image vierge pendant la lecture à partir du disque, et mettre à jour lorsque la lecture est terminée.

donc, personnellement, je ferais cette partie de mon ViewModel, pas un convertisseur de valeurs. J'ai un blog décrivant quelques -méthodes conviviales d'initialisation asynchrone. Ce serait mon premier choix. Il ne se sent pas juste d'avoir un convertisseur de valeurs début des opérations asynchrones de fond.

cependant, si vous avez considéré votre conception et que vous pensez vraiment qu'un convertisseur de valeur asynchrone est ce dont vous avez besoin, alors vous devez devenir un peu inventif. Le problème avec les convertisseurs de valeurs est qu'ils pour être synchrone: la liaison des données commence au contexte des données, évalue le chemin, puis invoque une conversion de valeur. Seules les notifications de changement de contexte de données et de chemin pris en charge.

ainsi, vous devez utiliser un convertisseur de valeurs (synchrone)dans votre contexte de données pour convertir votre valeur originale en unTask-comme l'objet et ensuite votre propriété de liaison utilise l'une des propriétés sur le Task-comme l'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}"/>

TextBox est juste une zone de saisie. TextBlock définit d'abord son propre DataContextTextBox 's texte d'entrée l'exécutant à travers un convertisseur" asynchrone". TextBlock.Text est défini à 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 démarre d'abord une opération asynchrone pour attendre 5 secondes puis ajoute " terminé!"à la fin de la chaîne d'entrée. Le résultat du convertisseur ne peut pas être simplement un simple Task parce que Task ne pas mettre en œuvre IPropertyNotifyChanged, donc j'utilise un type qui sera dans la prochaine version de mon AsyncEx bibliothèque. Il ressemble à quelque chose comme ceci (simplifié pour cet exemple; source complet est disponible à):

// 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 assemblant ces pièces, nous avons créé un contexte de données asynchrones qui est le résultat d'un convertisseur de valeurs. Le databinding-friendly Task wrapper va juste utiliser le résultat par défaut (habituellement null ou 0) jusqu'à ce que le Task terminé. Donc l'emballage est Result tout à fait différent de Task.Result: il ne bloque pas de façon synchrone et il n'y a aucun risque d'impasse.

mais pour réitérer: je choisirais de mettre la logique asynchrone dans le modèle de vue plutôt qu'un convertisseur de valeurs.

36
répondu Stephen Cleary 2013-02-21 16:45:00