LoginSignup
5
6

More than 5 years have passed since last update.

Windows Store App のViewModelにおいてAsyncAwaitの落とし穴

Last updated at Posted at 2015-02-25

AsyncAwaitは非常に便利で、お手軽に非同期処理を実装でき
バックグランドの処理とUIの処理を分離することなく実装できます。
ですが、UIの処理可能なスレッドは決まっています。

問題

これは、実装によって発生しなかったりします。
// かなり簡略してます。

INotifyPropertyChangedを実装するViewModelを作って、
PropertyChangeイベントを実装しました。

Xamlにprop1prop2をデータバインドします。

XamlのイベントからAsyncを利用して、
ViewModelで外部リソースにアクセスするためのAsyncAwaitなモデル処理を実装しました。
その中で、ViewModelのpropertyが変更されたときにViewに変更を通知するための
PropertyChangedを呼びだすまでは良かったのですが
例外が発生してしまいました。

ViewModel.cs
    /// <summary>
    /// ViewModel
    /// </summary>
    public class ViewModel: INotifyPropertyChanged
    {

        public string Prop1 { get; private set; }

        public string Prop2 { get; private set; }

        /// <summary>
        /// プロパティの変更通知
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// プロパティの変更通知イベントを発生させる
        /// </summary>
        /// <param name="name"></param>
        protected async virtual void OnPropertyChanged(string name)
        {
             PropertyChanged(this, new PropertyChangedEventArgs(name));
        }

        public async void ProceedAsync()
        {
             await Proceed();
        }

        protected async void Proceed()
        {
            Func<Task<{戻り値}>> method = async () =>
            {
                var result = await {外部リソースアクセス!};
         return result;
            };
             await Operation(method);
        }

        protected async Task Operation(Func<Task<{戻り値}>> method)
        {
            var result = await Task.Run(method);
            this.prop1 = "プロパティの更新" + result.ToString();
            this.OnPropertyChanged("prop1");
        }

    }

エラー

なぜか、ComExceptionが発生している。

型 'System.Runtime.InteropServices.COMException' の初回例外が System.dll で発生しました
アプリケーションは、別のスレッドにマーシャリングされたインターフェイスを呼び出しました。

原因

UIを処理できるスレッドは決まっている。
それ以外でアクセスするとエラーとなる。

対応策

AsyncAwaitをやめてしまえば大丈夫です。
いや、それは回避策ですね。

この問題は、非同期処理が起因ですが、実行しているスレッドが問題。
なので、適切なスレッドで処理させればよいんです。

それを解決してくれるのが

windows.ui.core.coredispatcher

ところが、データバインドは対象を指定するのみで
実際、Viewのcsで実装するところはありません。

じゃあどうするかというと。

ViewModel2.cs
    /// <summary>
    /// ViewModel
    /// </summary>
    public class ViewModel: IDisposable, INotifyPropertyChanged
    {
        public Windows.UI.Core.CoreDispatcher Dispatcher;

        /// <summary>
        /// プロパティの変更通知イベントを発生させる
        /// </summary>
        /// <param name="name"></param>
        protected async virtual void OnPropertyChanged(string name)
        {
            if (Dispatcher != null)
            {
                await Dispatcher.RunAsync(  Windows.UI.Core.CoreDispatcherPriority.Normal,
                    () =>  PropertyChanged(this, new PropertyChangedEventArgs(name)));
            }
            else
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }

こんな風にしちゃいます。
あとは、View側でViewModelをバインドするついでに

View.cs
    public class View
    {
        public void Init()
        {
            this.ViewModel = new ViewModel();
            this.DefaultViewModel = this.ViewModel;
            this.ViewModel.Dispatcher = this.Dispatcher;
        }
    }

こんな風にしてDispatherを渡してしまいます。
これで、スレッドがなんであるかを意識する必要がなくなりました。

まとめ

AsyncAwaitは、非同期のわずらわしさを軽減してくれますが、
それでは解決できないUIスレッドが足を引っ張ります。
そこで、AsyncAwaitがそもそもやろうとしていること、
タスクベースの非同期にフォーカスをあてて考えてみます。
そうしたとき、プログラムを書く側は
実行しているスレッドがなんであるかを意識することよりも
スレッドが適切なものになっていることのほうが都合が良いわけです。
もし意識してしまえば、AsyncAwaitの中見やライブラリの中身を知る必要が出てきてしまうでしょう。それは、仕様ではない実装に依存関係を生み出す結果となり
潜在的な不具合となってしまいます。
なので、一見遠回しのようでわかりにくいですが、ViewModelにDispatcherを
渡しました。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6