Silverlight Prism フレームワークを利用してMVVMアプリケーションを作成してみる 第3回 Webサービスとの連携

第1回で、モジュールの分割と、UI合成の動作を確認、第2回で、MVVMパターンの骨格ができたので、第2回にダミーでデータを取得していた個所を、Webサービスから取得することを想定し、非同期処理に変更していく。

非同期通信のハンドリング

Implementing the MVVM Pattern

View Model は、非同期でサービスやコンポーネントと通信を行う必要がある場合が有る。ユーザーが、非同期リクエストもしくはバックグラウンドタスクを開始する場合、応答がいつ到着するのかを予測するのは難しい。UIは、UIスレッドからのみ更新されるので、UIスレッドからディスパッチされたリクエストでUIを更新する必要がある。

データの参照とWebサービスとの通信

Webサービスやその他のリモートアクセス通信を行う場合、IAsyncResult パターンにたどり着くだろう。このパターンは、GetXxxxx といったメソッドを実行する代わりに、メソッドのペア、BeginGetXxxxxおよび EndGetXxxxxを利用する。非同期呼び出しを開始するには、BeginGetXxxxxを呼び出し、結果を取得したり、メソッド呼び出しの例外を確認するには、呼び出しが完了したときに、EndGetXxxxxを呼び出す。

EndGetXXXXX いつ呼び出すか決定するには、完了をポーリングするか、できれば、BeginGetXxxxx を呼び出すときに、コールバックを記述するのが望ましい。

IAsyncResult asyncResult = this.service.BeginGetQuestionnaire(GetQuestionnaireCompleted, null);

private void GetQuestionnaireCompleted(IAsyncResult result)
{
   try
   {
     questionnaire = this.service.EndGetQuestionnaire(ar);
   }
   catch (Exception ex)
   {
     // Do something to report the error.
   }
}

EndGetXxxxxメソッドの呼び出しでは、どのような例外も起こりうるため、これらをハンドリングする必要が有り、スレッドセーフな方法でUIを通してレポートする必要がある。

レスポンスは、通常UIスレッド上に無いため、UIの状態に何らか変更を加えたい場合、レスポンスをUIスレッドにディスパッチする必要があり、このために、スレッド Dispatcher もしくは、SynchronizationContext オブジェクトを利用する。 Silverlight では通常、Dispatcer を利用する。

以下のサンプルでは、Questionnaire オブジェクトは、非同期に参照され、QuestionnaireView のデータコンテキストに設定される。 Silverlight では、Dispatcherの CheckAccess メソッド を UIスレッドに属しているかどうかを判定するために利用できる。UIスレッドに無い場合、BeginInvoke メソッド を利用する。

var dispatcher = System.Windows.Deployment.Current.Dispatcher;
if (dispatcher.CheckAccess())
{
    QuestionnaireView.DataContext = questionnaire;
}
else
{
    dispatcher.BeginInvoke(
          () => { Questionnaire.DataContext = questionnaire; });
}

Prism に含まれる、Model-VIew-ViewModel 参照実装(MVVM RI)では、どのように、IAsyncResult ベースのサービスインターフェースを利用するかのサンプルがある。

this.questionnaireRepository.GetQuestionnaireAsync(
    (result) =>
    {
        this.Questionnaire = result.Result;
    });

Result オブジェクトは、発生したエラーもラップしている。エラーを評価する例

this.questionnaireRepository.GetQuestionnaireAsync(
    (result) =>
    {
        if (result.Error == null) {
          this.Questionnaire = result.Result;
          ...
        }
        else
        {  
          // Handle error. 
        }
    })

非同期処理の実装

AsyncResult クラスの作成

BeginXxxxx メソッドが呼び出される際に、そのメソッドでは内部的にIAsyncResultを実装したオブジェクトを構築する必要がある。IAsyncResult を実装したオブジェクトでは、以下の4つの読み取り専用プロパティを実装する。

IAsyncResult

名前 説明
AsyncState 非同期操作についての情報を限定または格納するユーザー定義のオブジェクトを取得
AsyncWaitHandle 非同期操作が完了するまで待機するために使用する WaitHandle を取得
CompletedSynchronously 非同期操作が同期的に完了したかどうかを示す値を取得
IsCompleted 非同期操作が完了したかどうかを示す値を取得

 

実装の内容は、MVVM RI を参考に。

AsyncResult.cs

using System;
using System.Threading;

namespace ProjectModule.Services
{
    public class AsyncResult : IAsyncResult
    {
        private readonly object lockObject;
        private readonly AsyncCallback asyncCallback;
        private readonly object asyncState;
        private ManualResetEvent waitHandle;
        private bool completedSynchronously;
        private bool isCompleted;
        private T result;
        private bool endCalled;
        private Exception exception;

        public AsyncResult(AsyncCallback asyncCallback, object asyncState)
        {
            this.lockObject = new object();
            this.asyncCallback = asyncCallback;
            this.asyncState = asyncState;
        }

        public T Result
        {
            get { return this.result; }
        }

        public static AsyncResult End(IAsyncResult asyncResult)
        {
            var localResult = asyncResult as AsyncResult;
            if (localResult == null)
            {
                throw new ArgumentNullException("asyncResult");
            }

            lock (localResult.lockObject)
            {
                if (localResult.endCalled)
                {
                    throw new InvalidOperationException("End method already called");
                }

                localResult.endCalled = true;
            }

            if (!localResult.IsCompleted)
            {
                localResult.AsyncWaitHandle.WaitOne();
            }

            if (localResult.waitHandle != null)
            {
                localResult.waitHandle.Close();
            }

            if (localResult.exception != null)
            {
                throw localResult.exception;
            }

            return localResult;
        }

        public void SetComplete(T result, bool completedSynchronously)
        {
            this.result = result;

            this.DoSetComplete(completedSynchronously);
        }

        public void SetComplete(Exception e, bool completedSynchronously)
        {
            this.exception = e;

            this.DoSetComplete(completedSynchronously);
        }

        private void DoSetComplete(bool completedSynchronously)
        {
            if (completedSynchronously)
            {
                this.completedSynchronously = true;
                this.isCompleted = true;
            }
            else
            {
                lock (this.lockObject)
                {
                    this.isCompleted = true;
                    if (this.waitHandle != null)
                    {
                        this.waitHandle.Set();
                    }
                }
            }

            if (this.asyncCallback != null)
            {
                this.asyncCallback(this);
            }
        }

        #region Implementation of IAsyncResult
        
        public object AsyncState
        {
            get { return this.asyncState; }
        }

        public WaitHandle AsyncWaitHandle
        {
            get
            {
                lock (this.lockObject)
                {
                    if (this.waitHandle == null)
                    {
                        this.waitHandle = new ManualResetEvent(this.IsCompleted);
                    }
                }

                return this.waitHandle;
            }
        }

        public bool CompletedSynchronously
        {
            get { return this.completedSynchronously; }
        }

        public bool IsCompleted
        {
            get { return this.isCompleted; }
        }

        #endregion
    }
}

データサービスの作成

Implementing the MVVM Pattern の記述を参考に、データ取得サービスのインターフェースに、BeginGetXxxxx メソッドと、EndGetXxxxx を追加する(元々のGetXxxxx は削除)

IProjectDataService.cs

using System;
using ProjectModule.Models;

namespace ProjectModule.Services
{
    public interface IProjectDataService
    {
        IAsyncResult BeginGetProjects(AsyncCallback callback, object userState);
        Projects EndGetProjects(IAsyncResult asyncResult);
    }
}

上記インターフェースを実装したクラスを作成

  • BeginGetProjects メソッドのなかで擬似的に遅延をさせるために、Thread.Sleep を実施した後、AsyncResultのSetCompleteメソッドを呼び出す。SetCompleteに渡す結果(CreateProjectsメソッドの戻り値)が、Result として取得できるようになる
  • CreateProjectsメソッドでは、第2回のまま、ダミーのデータを返すままとしておく。
  • EndGetProjectsメソッドでは、結果を取り出し返却

ProjectDataService.cs

using System;
using System.ComponentModel.Composition;
using ProjectModule.Models;
using System.Collections.Generic;
using System.Threading;
using System.Net;

namespace ProjectModule.Services
{

    [Export(typeof(IProjectDataService))]
    public class ProjectDataService : IProjectDataService
    {
        private Projects projects;

        public IAsyncResult BeginGetProjects(AsyncCallback callback, object userState)
        {
            var asyncResult = new AsyncResult(callback, userState);
            ThreadPool.QueueUserWorkItem(
                o =>
                {
                    Thread.Sleep(1500);
                    asyncResult.SetComplete(CreateProjects(), false);
                });

            return asyncResult;
        }

        public Projects CreateProjects()
        {
            if (this.projects == null)
            {
                this.projects = new Projects { 
                    new Project(){
                        Key = "001",
                        SystemName = "Sys2",
                        ApplicationName="Test App1"
                    },
                    new Project(){
                        Key = "002",
                        SystemName = "Sys2",
                        ApplicationName="Test App2"
                    },
                    new Project(){
                        Key = "003",
                        SystemName = "Sys2",
                        ApplicationName="Test App3"
                    },
                };
            }
            return this.projects;
        }


        Projects IProjectDataService.EndGetProjects(IAsyncResult asyncResult)
        {
            var localAsyncResult = AsyncResult.End(asyncResult);

            return localAsyncResult.Result;
        }
    }
}

ViewModelの変更

  • 今まで、コンストラクタで、データの取得と表示を行っていたのを、コンストラクタでは、上記で作成した、BeginGetProjects メソッドを、コールバック GetProjectCompleted を指定して非同期で呼び出す
  • GetProjectCompleted は、処理が終了したら、AsyncResult から呼び出される
  • 処理が完了し、GetProjectCompleted が呼び出されたら、EndGetProjects を呼び出して、結果データを取得する。
  • このままだと、「レスポンスは、通常UIスレッド上に無いため、UIの状態に何らか変更を加えたい場合、レスポンスをUIスレッドにディスパッチする必要」があるため、

    var dispatcher = System.Windows.Deployment.Current.Dispatcher; 以下の判定で、UIスレッドか否かによって、プロパティ変更通知を NotifyPropertyChanged メソッドを直接呼ぶか、ディスパッチャに呼び出しを任せるか判定して、プロパティの変更をView に通知

using System;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Windows.Data;
using Microsoft.Practices.Prism.Events;
using ProjectModule.Events;
using ProjectModule.Models;
using ProjectModule.Services;

namespace ProjectModule.ViewModels
{
    
    [Export]
    public class ProjectListViewModel : INotifyPropertyChanged
    {

        private readonly IEventAggregator eventAggregator;
        private IProjectDataService dataService;
        public ICollectionView Projects { get; set;}

        [ImportingConstructor]
        public ProjectListViewModel(IProjectDataService dataService, IEventAggregator eventAggregator)
        {
            if (dataService == null) throw new ArgumentNullException("dataService");
            if (eventAggregator == null) throw new ArgumentNullException("eventAggregator");

            this.eventAggregator = eventAggregator;
            this.dataService = dataService;

            IAsyncResult asyncResult = this.dataService.BeginGetProjects(GetProjectsCompleted, null);
        }

        private void GetProjectsCompleted(IAsyncResult result)
        {
            try
            {
                this.Projects = new PagedCollectionView(this.dataService.EndGetProjects(result));
                
                if (this.Projects != null)
                {
                    this.Projects.CurrentChanged += new EventHandler(this.SelectedProjectChanged);
                }
                var dispatcher = System.Windows.Deployment.Current.Dispatcher;
                if (dispatcher.CheckAccess())
                {
                    this.NotifyPropertyChanged("Projects");
                }
                else
                {
                    dispatcher.BeginInvoke(
                          () => { this.NotifyPropertyChanged("Projects"); });
                }
  
            }
            catch (Exception ex)
            {
                // TODO
            }
        }


        private void SelectedProjectChanged(object sender, EventArgs e)
        {
            Project project = this.Projects.CurrentItem as Project;
            if (project != null)
            {
                this.eventAggregator.GetEvent().Publish(project.Key);
            }
        }
        
        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        
        #endregion

    }
}

実行

表示結果は、第2回 と変わらないが、内部的には、データを非同期で取得できるようになった。

prism_handson16

Follow me!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です