web-dev-qa-db-ja.com

IValueConverterの非同期実装

IValueConverter内でトリガーしたい非同期メソッドの場合。

結果のプロパティを呼び出して同期を強制するよりも良い待機はありますか?

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

いくつかの理由から、おそらくTask.Resultを呼び出したくないでしょう。

まず、ブログで詳しく説明しているように、asyncコードがどこでもConfigureAwaitを使用して記述されていない限り、 デッドロックする可能性があります 。次に、UIを(同期して)ブロックしたくない場合があります。ディスクからの読み取り中に一時的に「読み込み中...」または空白のイメージを表示し、読み取りが完了したら更新することをお勧めします。

したがって、個人的には、値コンバーターではなく、ViewModelのこの部分を作成します。いくつかの 非同期初期化を行うためのデータバインディングに適した方法 について説明しているブログ投稿があります。それが私の最初の選択です。 値コンバーターが非同期バックグラウンド操作を開始するのは正しくないと感じています。

ただし、設計を検討し、非同期値コンバーターが必要なものであると本当に考えている場合は、少し独創性を持たせる必要があります。値コンバーターの問題は、それらが同期していることです。データバインディングはデータコンテキストで開始され、パスを評価してから、値変換を呼び出します。 。データコンテキストとパスのみが変更通知をサポートします。

したがって、データコンテキストで(同期)値コンバーターを使用して、元の値をデータバインディングに適したTaskのようなオブジェクトに変換する必要があります。その後、プロパティバインディングは、Taskのようなオブジェクトのプロパティの1つを使用して結果。

これが私が意味することの例です:

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

TextBoxは単なる入力ボックスです。 TextBlockは、最初に独自のDataContextTextBoxの入力テキストに設定し、「非同期」コンバーターを介して実行します。 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秒間待機し、次に「done!」を追加します。入力文字列の最後まで。 TaskTaskを実装していないため、コンバーターの結果は単なるIPropertyNotifyChangedにはなりません。そのため、次のリリースの AsyncExライブラリ に含まれるタイプを使用しています。これは次のようになります(この例では簡略化されています; 完全なソースが利用可能です ):

// 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ラッパーは、nullが完了するまで、デフォルトの結果(通常はTaskまたは0)を使用します。したがって、ラッパーのResultTask.Resultとはまったく異なります。同期的にブロックされず、デッドロックの危険はありません。

ただし、繰り返しになりますが、値コンバーターではなく、非同期ロジックをViewModelに配置することを選択します。

46
Stephen Cleary