Windows Azure AppFabric サービス バス

ポータブル クラス ライブラリを使って連続性のあるクライアントを作成する

David Kean

コード サンプルのダウンロード

デバイスを常時接続できる時代に生活できて幸運だと思います。家に帰るバスの中で携帯電話からメールに返信できるのはすばらしいことです。地球の裏側にいる家族に Skype で話かけられるのも、全国のゲーム仲間と Xbox で対戦できるのも、すごいことです。しかし、このインターネットへの常時接続の世界には、Joshua Topolsky が「コンピューティング エクスペリエンスに欠けているリンク」 (engt.co/9GVeKl、英語) と称した事象があります。

「欠けているリンクとは、連続性のあるクライアントがないことだ」と Topolsky は語っています。つまり、あるデバイスから別のデバイスへ移動するときにワークフローが分断されることに対するソリューションが欠けています。1 日の中で PC、タブレット、携帯電話を切り替えながら使う場合、現在のブラウズ セッション、ドキュメント、ウィンドウ、アプリの状態などは、あらゆるデバイス間で自然な形で受け渡されるべきです。そうすれば、時間をかけてコンテキストを切り替える必要がなくなり、その時間を仕事や娯楽に使えます。

今回の記事では、複数のデバイスとプラットフォームをまたいで連続性のある簡単なクライアント アプリをビルドする方法を説明します。ここでは新しいポータブル クラス ライブラリ (PCL) を活用して、クロス プラットフォーム アプリケーションの開発を容易にします。また、クラウド、特に Windows Azure AppFabric サービス バスを使って、デバイス間の通信を処理します。

帰宅途中に...

夕方も遅くなり、職場で最後のバグ修正を急いで済ませて、ラッシュ アワー前に帰宅したいところです。そこに家から電話です。「あなた、帰る途中でミルクとパンとヘーゼル ナッツを買ってきてもらえる?」電話を切って家路につき、店に着いたところで何を買うかを忘れてしまったことに気づきました。このままでは、家にあるものをまた買って帰ってしまうかもしれません。もどかしいことですが、電話をかけなおして確認するのが今日のソリューションです。「カシュー ナッツとヘーゼル ナッツのどっちだっけ?」「ヘーゼル ナッツよ。ついでに、トイレット ペーパーもお願いね」。

この問題に限って夫婦間のストレスを和らげるため (別の問題についてはまたの機会にしましょう)、「帰宅途中に」という簡単なアプリを作成することにします。これは Windows Phone デバイス と Windows 8 ベータ タブレットで動き、妻と私が買い物リストを簡単にチェックできます。これによって 2 人はリアルタイムに情報を共有して、買い物リストの変更があっても、必要なものをいつでも正確に知ることができます。

Window Phone のスマートフォンと Windows 8 ベースのタブレットは種類が異なるデバイスで、やや趣が異なる Microsoft .NET Framework と Windows が稼働していますが、PCL を使ってプラットフォームの相違を抽象化し、Windows Azure AppFabric サービス バスとの通信など、アプリ ロジックをできるだけ共通化します。また、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) パターン (bit.ly/GW7l) を使って、デバイス固有のビューから、同じモデルとビュー モデルを使用します。

ポータブル クラス ライブラリ

.NET Framework でのクロス プラットフォーム開発は、これまであまり容易ではありませんでした。.NET Framework にはクロス プラットフォーム ランタイムという壮大な夢がありましたが、マイクロソフトはその公約をまだ完全には果たしていません。複数のデバイスにまたがる .NET Framework ベースのアプリケーションまたはフレームワークの提供を試みた開発者は、いくつかの問題に直面したことでしょう。

ランタイム側: アセンブリのファクタリング、バージョン管理、アセンブリ名が .NET プラットフォーム間で異なります。たとえば、.NET Framework の System.Net.dll はピアツーピア ネットワーク API を含みますが、Silverlight ではコア ネットワーク スタックを含み、意味がまったく異なります。.NET Framework でそれらの API を見つけるには、System.dll を参照する必要があります。アセンブリのバージョンも同じではありません。Silverlight のバージョン 2.0 から 4 ではバージョン 2.0.5.0 のアセンブリを使っていますが、.NET Framework のバージョン 2.0 から 4 ではバージョン 2.0.0.0 と 4.0.0.0 のアセンブリを使っています。これまでは、このような相違点のため、あるプラットフォームでコンパイルされたアセンブリは別のプラットフォームでは機能しませんでした。

Visual Studio 側: .NET Framework、Silverlight、Windows Phone のどのプラットフォームをターゲットとするかを最初に決める必要があります。いったん決定すると、他のプラットフォームへの移行またはそのサポートはきわめて困難です。たとえば、既に .NET Framework をターゲットとしている場合、Silverlight や Windows Phone をターゲットにするには新しいプロジェクトを作成し、既存のファイルをそのプロジェクトにコピーするかリンクしなければなりません。開発者によっては、アプリケーションのプラットフォーム固有の部分を容易に置換できるようにあらかじめファクタリングするかもしれません。そうでなければ (こちらの場合の方が多いでしょう)、ビルド エラーの都度、#if PLATFORM によって回避策を実装することになります。

このような場合にこそ新しい PCL が役立ちます。PCL は、Visual Studio 2010 では無償アドオンとして入手でき (bit.ly/ekNnsN、英語) 、Visual Studio 11 ベータでは製品に組み込まれています。PCL は、1 つのプロジェクトで複数のプラットフォームをターゲットとするための容易な方法を提供します。開発者は新しい PCL を作成し、ターゲットとするフレームワークを選択 (図 1 参照) すれば、コードの作成を開始できます。PCL ツールは API の相違を吸収し、IntelliSense をフィルター処理して、選択したすべてのフレームワークで利用可能で機能するクラスとメンバーだけを表示します。結果のアセンブリは、変更することなく、選択したすべてのフレームワークで参照および実行できます。

Portable Class Library Target Frameworks
図 1 ポータブル クラス ライブラリ の ターゲット フレームワーク

ソリューション レイアウト

PCL を使ってクロス プラットフォームのアプリを作成するには、通常、共有コンポーネントを含むポータブル プロジェクトを 1 つ以上用意し、それらのプロジェクトを参照するプラットフォーム固有のプロジェクトをプラットフォーム別に作成します。今回のアプリには、2 つの Visual Studio ソリューションを用意します。Windows Phone アプリ用の Visual Studio 2010 (OnYourWayHome.VS2010)、および Windows Metro スタイル アプリ用の Visual Studio 11 (OnYourWayHome.VS11) です。この記事の執筆時点では、Windows Phone SDK 7.1 は Visual Studio 2010 上のみで機能し、Windows 8 のツールは Visual Studio 11 の一部としてのみ利用可能なため、複数のソリューションが必要になります。現時点では 1 つのバージョンで両方をサポートするものはありません。ただし、Visual Studio 11 で利用できる新機能が役に立つので、がっかりする必要はありません。以前のバージョンで作成したプロジェクトのほとんどは、新しい形式に変換し直さなくても開けます。これにより PCL プロジェクトを 1 つ用意して、それを両方のソリューションから参照できます。

アプリケーションのプロジェクト レイアウトを図 2図 3 に示します。OnYour­WayHome.Core は PCL プロジェクトで、モデル、ビューモデル、共通サービス、およびプラットフォームの抽象化を含みます。OnYour­WayHome.ServiceBus も PCL プロジェクトで、Windows Azure と通信する API の移植可能なバージョンを含みます。どちらのプロジェクトも Visual Studio 2010 ソリューションと Visual Studio 11 で共有されます。OnYourWayHome.Phone と OnYourWayHome.Metro はそれぞれ Windows Phone 7.5 と Metro スタイル アプリ用 .NET のプラットフォーム固有のプロジェクトです。これらにはデバイス固有のビュー (アプリのページなど)、および OnYourWayHome.Core と OnYourWayHome.ServiceBus の抽象化の実装を含みます。

Windows Phone Project Layout in Visual Studio 2010
図 2 Visual Studio 2010 での Windows Phone プロジェクト レイアウト

Windows Metro-Style App Project Layout in Visual Studio 11
図 3 Visual Studio 11 での Windows Metro スタイル アプリ プロジェクト レイアウト

既存のライブラリを PCL に変換する

Windows Azure との通信のため、Silverlight ベースの REST のサンプルを servicebus.codeplex.com (英語) からダウンロードし、それを PCL プロジェクトに変換します。ライブラリによって変換の難易度が異なりますが、否応なく、型やメソッドが利用できない状況に陥ります。API が PCL でサポートされない理由をいくつか示します。

API が実装されていないプラットフォームがある: System.IO.File や System.IO.Directory などの従来の .NET Framework のファイル IO がこれに該当します。Silverlight と Windows Phone は System.IO.IsolatedStorage API (.NET Framework バージョンとは異なります) を使いますが、Windows 8 Metro スタイル アプリは Windows.Storage を使います。

API と互換性がないプラットフォームがある: API の中には同じように見えても、移植可能かつ一貫した方法でそれらを使ったコードを記述することは困難または不可能なものがあります。この 1 つの例が、スレッドごとに一意値を持つための静的フィールドを有効にする ThreadStaticAttribute です。これは Windows Phone と Xbox プラットフォームの両方にありますが、どちらのランタイムもサポートしていません。

API が旧形式またはレガシ: このような API は将来のプラットフォームでは存在しなくなる可能性がある動作を含むか、新しいテクノロジに置き換えられています。BackgroundWorker がこの例で、Visual Studio 11 ベータの Task と新しい非同期プログラム機能に置き換えられました。 

時間切れ: 多くの API が移植性を考慮しないで作成されています。マイクロソフトは多くの時間をかけて各 API をレビューし、移植可能な方法でプログラミングできるようにしています。API を移植可能にするため、修正や追加が必要になるものもあります。これには多くの時間と労力を必要とするため、Visual Studio ギャラリーから入手可能な最初のバージョンの PCL では利用度の高い API を優先しています。System.Xml.Linq.dll と System.ComponentModel.DataAnnotations.dll がこの例で、最初のバージョンでは利用できませんでしたが、現在の Visual Studio 11 ベータ リリースでは利用可能になっています。

上記のいずれかに該当する API に対処する方法は、いくつかあります。単に置き換えればよい場合もあります。たとえば、Close メソッド (Stream.Close、TextWriter.Close、など) は PCL には含まれなくなり、Dispose に置き換えられました。このような場合、前者の呼び出しを後者に置き換えるだけで済みます。しかし、場合によってはやや困難で多くの作業を伴うこともあります。サービス バス API の変換中に発生した問題の 1 つが HMAC SHA256 ハッシュ コード プロバイダーです。Windows Phone と Metro スタイル アプリの暗号化の相違により、これは PCL では利用できません。Windows Phone アプリは .NET ベースの API を使って、データの暗号化、復号化、およびハッシュを行いますが、Metro スタイル アプリは新しいネイティブの Windows ランタイム (WinRT) API を使います。

変換後にビルドに失敗したのは、具体的には下記のコードです。

using (HMACSHA256 sha256 = new HMACSHA256(issuerSecretBytes))
{
  byte[] signatureBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(token));
  signature = Convert.ToBase64String(signatureBytes);
}

Windows Phone の暗号化 API と WinRT の暗号化 API のギャップを埋めるため、今回はサービス バスの要件を表すプラットフォームの抽象化を考えました。そのためには、次のようにサービス バスに HMAC SHA256 ハッシュを計算する方法が必要です。

public abstract class ServiceBusAdapter
{
  public static ServiceBusAdapter Current
  {
    get;
    set;
  }
  public abstract byte[] ComputeHmacSha256(byte[] secretKey, byte[] data);
}

ポータブル プロジェクトに ServiceBusAdapter を追加し、現在の抽象化を設定するための静的プロパティも追加しました。これは後ほど重要になります。次に Windows Phone 固有と Windows 8 固有の HMAC SHA256 の抽象化の実装をそれぞれ作成し、これらをそれぞれのプロジェクトに配置します (図 4 参照)。

図 4 Windows Phone と Windows 8 用の HMAC SHA256 のそれぞれの実装

// Windows Phone implementation
public class PhoneServiceBusAdapter : ServiceBusAdapter
{
  public override byte[] ComputeHmacSha256(byte[] secretKey, byte[] data)
  {
    using (var cryptoProvider = new HMACSHA256(secretKey))
    {
      return cryptoProvider.ComputeHash(data);
    }
  }
}
// Windows 8 implementation
public class MetroServiceBusAdapter : ServiceBusAdapter
{
  private const string HmacSha256AlgorithmName = "HMAC_SHA256";
  public override byte[] ComputeHmacSha256(byte[] secretKey, byte[] data)
  {
    var provider = MacAlgorithmProvider.OpenAlgorithm(HmacSha256AlgorithmName);
    var key = provider.CreateKey(_secretKey.AsBuffer());
    var hashed = CryptographicEngine.Sign(key, buffer.AsBuffer());
    return hashed.ToArray();
  }
}

Windows Phone プロジェクトのスタートアップで、Windows Phone 固有のアダプターを現在のアダプターに設定して、サービス バスをブートストラップします。

ServiceBusAdapter.Current = new PhoneServiceBusAdapter();

同じことを Windows 8 プロジェクトでも行います。

ServiceBusAdapter.Current = new MetroServiceBusAdapter();

これらを行ってから、オリジナルの非コンパイル コードをアダプターから呼び出すように変更します。

var adapter = ServiceBusAdapter.Current;
byte[] signatureBytes = adapter.ComputeHmacSha256(issuerSecretBytes, Encoding.UTF8.GetBytes(token));

このように、各プラットフォームそれぞれにハッシュを計算する別の方法を持ちますが、ポータブル プロジェクトでは 1 つのインターフェイスを使ってそれぞれと対話します。これには事前準備が少々必要ですが、プラットフォーム間のギャップを埋める必要がある別の API に出くわした場合にも、このインフラストラクチャを容易に再利用できます。

ところで、ここではアダプターにアクセスして登録するために静的プロパティを使いましたが、これによって既存の API をアダプターを使って容易に移動できます。Managed Extensibility Framework (MEF)、Unity、Autofac などの依存関係挿入フレームワークを使う場合、プラットフォーム固有のアダプターをコンテナーに登録し、コンテナーからアダプターをポータブル コンポーネントに挿入させるのが自然だということがわかります。

アプリのレイアウト

この「帰宅途中に」という買い物リスト アプリには 2 つのシンプルなビューがあります。買い物リストの現在の商品を表示する ShoppingListView と、ユーザーがリストに品目を追加できる AddGroceryItemView です。Windows Phone バージョンのこれらのビューを図 5図 6 に示します。

ShoppingListView
図 5 ShoppingListView

AddGroceryItemView
図 6 AddGroceryItemView

ShoppingListView は購入前のすべての商品を表示し、店の中を回りながらカートに入れるたびに、該当商品にチェック マークを付けていくことを想定しています。商品購入後、[Check Out] をクリックすると、チェックした商品がリストから消え、それ以上その商品を買う必要がないことを示します。買い物リストを共有しているデバイスの間では、他のユーザーが行った変更を (ネットワークによって可能な限り) 直ちに参照することができます。

プラットフォーム固有のプロジェクト中のビューは主に XAML とわずかな分離コードのみを含みます。そのため、2 つのプラットフォーム間で複製するコードは少なくて済みます。ビューは XAML のデータ バインドを使って、ビューを実行するコマンドとデータを提供する、移植可能なビューモデルにバインドします。すべてのプラットフォームにわたる共通の UI フレームワークは存在しないため、PCL プロジェクトから UI 固有の API は参照できません。ただし、それをサポートするフレームワークをターゲットとする場合は、ビューモデルが通常使う API を活用できます。これは XAML のデータ バインド実行の中核となる型、INotifyPropertyChanged、ICommand、INotifyCollectionChanged などを含みます。また、WinRT XAML フレームワークはこれらをサポートしませんが、完全性のため System.ComponentModel.DataAnnotations と INotifyDataErrorInfo が追加されています。これは移植可能なビューモデルとモデルをサポートするため、XAML のカスタム検証フレームワークを有効にします。

ビューとビューモデルの操作の例を図 7図 8 に示します。Window Phone バージョンの AddGroceryItemView とそのバインドを図 7 に示します。これらのコントロールは、Windows Phone と Windows 8 のプロジェクトで共有される AddGroceryItemViewModel のプロパティにバインドされます(図 8 参照)。

図 7 Windows Phone の AddGroceryItemView コントロール

<StackPanel>
  <TextBox Text="{Binding Name, Mode=TwoWay}"
           Width="459"
           Height="80" />
  <Button Command="{Binding Add}"
          Content="add"
          Margin="307,5,0,0" />
  <TextBlock Text="{Binding NotificationText}"
             Margin="12,5,0,0"/>
</StackPanel>

図 8 Windows 8 の AddGroceryItemViewModel クラス

public class AddGroceryItemViewModel : NavigatableViewModel
{
  private string _name;
  private string _notificationText;
  private ICommand _addCommand;
  [...]
  public ICommand AddCommand
  {
    get { return _addCommand ?? (_addCommand = new ActionCommand(Add)); }
  }
  public string Name
  {
    get { return _name ?? String.Empty; }
    set { base.SetProperty(ref _name, value, "Name"); }
  }
  public string NotificationText
  {
    get { return _notificationText ?? string.Empty; }
    set { base.SetProperty(ref _notificationText, value, "NotificationText"); }
  }
}

イベント ソースを登録する

「帰宅途中に」はイベント ソースの考え方に大きく基づいています (bit.ly/3SpC9h、英語)。これは、アプリのすべての状態変化がイベントのシーケンスとして発行および格納されるという考えです。今回の場合のイベントとは、C# の Event キーワードで定義されるものを指すわけではなく (考え方は同じですが)、システムの 1 つの変化を表す具体的なクラスを指します。それらはイベント アグリゲーターと呼ばれるものにより発行され、イベントに反応して作用する 1 つ以上のハンドラーに通知します (イベント アグリゲーターに関する詳細については、 Shawn Wildermuth の記事「Prism を使用した複合 Web アプリケーション」 msdn.microsoft.com/magazine/dd943055 を参照してください)。

たとえば、買い物リストに 1 つの商品を追加するイベントは図 9 のようになります。

図 9 商品の追加イベント

// Published when a grocery item is added to a shopping list
[DataContract]
public class ItemAddedEvent : IEvent
{
  public ItemAddedEvent()
  {
  }
  [DataMember]
  public Guid Id
  {
    get;
    set;
  }
  [DataMember]
  public string Name
  {
    get;
    set;
  }
}

ItemAddedEvent クラスは商品追加のイベントに関する情報を含みます。この例では、追加した商品の名前と、買い物リスト上で商品を一意に表すための ID です。またイベントを [DataContract] でマークし、ディスクへのシリアル化やネットワークへの送信を容易にしています。

このイベントは、ユーザーが AddGroceryItemView の [add] ボタンをクリックすると作成および発行されます (図 10 参照)。

図 10 イベントの発行

public class AddGroceryItemViewModel : NavigatableViewModel
{
  private readonly IEventAggregator _eventAggregator;
  [...]
  // Adds an item to the shopping list
  private void Add()
  {
    var e = new ItemAddedEvent();
    e.Id = Guid.NewGuid();
    e.Name = Name;
    _eventAggregator.Publish(e);
    NotificationText = String.Format("{0} was added to the shopping list.", Name);
    Name = string.Empty;
  }
}

このメソッドは買い物リストに直接の変更を加えるのではなく、単にイベント アグリゲーターに ItemAddedEvent を発行しているだけです。買い物リストに変更を加えるのは、このイベントをリッスンするイベント ハンドラーの 1 つです。この場合は、ShoppingList というクラスがこのイベントをサブスクライブして対応します (図 11 参照)。

図 11 ShoppingList クラス

public class ShoppingList : IEventHandler<ItemAddedEvent>                                        
{
  public ShoppingList(IEventAggregator eventAggregator)
  {
    Requires.NotNull(eventAggregator, "eventAggregator");
    _eventAggregator = eventAggregator;
    _eventAggregator.Subscribe<ItemAddedEvent>(this);
  }
  [...]
  public ReadOnlyObservableCollection<GroceryItem> GroceryItems
  {
    get { return _groceryItems; }
  }
  public void Handle(ItemAddedEvent e)
  {
    var item = new GroceryItem();
    item.Id = e.Id;
    item.Name = e.Name;
    item.IsInCart = false;
    _groceryItems.Add(item);
  }
}
}

ItemAddedEvent が発行されるたびに、ShoppingList はイベントのデータを使って新しい GroceryItem を作成し、買い物リストに追加します。ShoppingListViewModel によって同じリストに間接的にバインドされている ShoppingListView も更新されます。これで、ユーザーが買い物リスト ページに戻ったときには、リストに追加したばかりの商品が想定どおりに表示されます。買い物リストからの商品の削除、カートへの商品の追加、カートの清算は、すべてこれと同じイベントの発行とサブスクライブのパターンを使います。

買い物リストに商品を追加するだけなのに、多くの回り道をしているように思えるかもしれません。AddGroceryItemViewModel.Add メソッドがイベントを IEventAggregator に発行し、IEventAggregator がイベントのデータを ShoppingList に渡し、ShoppingList が買い物リストに追加しています。AddGroceryItemViewModel.Add メソッドが IEventAggregator をスキップして、新しい GroceryItem を直接 ShoppingList に追加しないのはなぜでしょう。よくぞたずねてくださいました。システムでのすべての状態変化をイベントとして扱うメリットは、アプリの個々の構成要素が疎結合されるようになることです。イベントを発行する側とイベントを利用する側は相互に認識しないため、たとえばクラウドとのデータ同期のような、新しい機能をパイプラインに挿入することが実に容易になります。

データをクラウドと同期する

1 つのデバイスで稼働するアプリの基本機能は説明しました。次の問題は、ユーザーが買い物リストに行った変更を別のデバイスに伝えること、および他のデバイスが行った変更を認識することです。ここで Windows Azure AppFabric サービス バスが役立ちます。

Windows Azure AppFabric サービス バスは、アプリケーションとサービスが、ファイアウォールやネットワーク アドレス変換 (NAT) デバイスなどの複雑な通信上の問題に対応することなく、インターネットを通じて容易に相互通信できるようにする機能です。Windows Azure によってホストされる REST と Windows Communication Foundation (WCF) HTTP エンドポイントの両方を提供し、イベントの発行側とイベントの利用側の中間に位置します。

Windows Azure AppFabric サービス バスを使った通信には 3 つの主要な方法がありますが、今回のアプリ向けにはトピックのみを説明します。完全な概要については、「Windows Azure AppFabric サービス バスの概要」 (bit.ly/uNVaXG、英語) を参照してください。

イベントの発行側にとっては、サービス バスのトピックはクラウド上の大きなキューがあると考えます (図 12 参照)。イベントの発行側は、リッスンしている相手をまったく知ることなく、メッセージをトピックにプッシュし、そのメッセージはイベントの利用側が要求するまでキューに留まり続けます。キューからメッセージを取得するには、イベントの利用側はサブスクリプションからプルを行います。サブスクリプションは、トピックに発行されたメッセージをフィルター処理します。サブスクリプションは特定のキューのように機能し、あるサブスクリプションから削除されたメッセージでも、他のサブスクリプションではフィルターがそれを含む限り、依然として参照できます。

Service Bus Topic
図 12 サービス バス トピック

「帰宅途中に」では、AzureServiceEventHandler クラスがアプリとサービス バスをつなぐ役割をします。このクラスは ShoppingList と同様、IEventHandler<T> も実装しますが、特定のイベントでなく、AzureServiceEventHandlers がすべてのイベントに対応します (図 13 参照)。

The AzureServiceEventHandler Class
図 13 AzureServiceEventHandler クラス

public class AzureServiceBusEventHandler : DisposableObject, IEventHandler<IEvent>, IStartupService
{
  private readonly IAzureServiceBus _serviceBus;
  private readonly IAzureEventSerializer _eventSerializer;
  public AzureServiceBusEventHandler(IEventAggregator eventAggregator,
    IAzureServiceBus serviceBus, IAzureEventSerializer eventSerializer)
  {
    _eventAggregator = eventAggregator;
    _eventAggregator.SubscribeAll(this);
    _serviceBus = serviceBus;
    _serviceBus.MessageReceived += OnMessageReceived;
    _eventSerializer = eventSerializer;
  }
  [...]
  public void Handle(IEvent e)
  {
    BrokeredMessage message = _eventSerializer.Serialize(e);
    _serviceBus.Send(message);
  }
}

ユーザーが行う買い物リストのすべての状態変化に AzureServiceBusEventHandler が対応し、クラウドに直接プッシュします。この挙動は、イベントを発行する AddGroceryItemViewModel にも、ローカル デバイス上で対応する ShoppingList にも関与しません。

クラウドからの戻りは、まさにイベント ベースのアーキテクチャが役立つところです。サービス バスが新しいメッセージを受信したことを (C# イベント IAzureServiceBus.MessageReceived によって) AzureServiceEventHandler が検知すると、先の説明の逆を行って、受信メッセージをイベントにシリアル化解除します。ここで、イベント アグリゲーターがイベントを発行してアプリに戻し、そのイベントはアプリ内で発生したかのように扱われます (図 14 参照)。

図 14 受信メッセージをシリアル化解除する

public class AzureServiceBusEventHandler : DisposableObject, IEventHandler<IEvent>,
  IStartupService
{
  private readonly IAzureServiceBus _serviceBus;
  private readonly IAzureEventSerializer _eventSerializer;
  [...]
  private void OnMessageReceived(object sender, MessageReceivedEventArgs args)
  {
    IEvent e = _eventSerializer.Deserialize(args.Message);
    _eventAggregator.Publish(e);
  }
}

ShoppingList はイベントの発行元を認識することなく (考慮することもなく)、クラウドからサービス バス経由で発生したイベントをユーザー入力から直接来たかのように扱います。そして買い物リストを更新し、そのリストにバインドされたすべてのビューも更新します。

注意深く見ると、このワークフローには 1 つ小さな問題があることがわかります。ローカル デバイスからクラウドに送られたイベントは同じデバイスにも戻り、データの重複が生じます。さらに悪いことに、他の無関係な買い物リストの変更イベントもそのデバイスに到着します。皆さんがどう思うかはわかりませんが、少なくとも私は、自分の買い物リストに他人の買い物リストが表示されては困ります。これを避けるには、サービス バス トピックをリストごとに作成し、サブスクリプションをデバイスごとに作成し、トピックをリッスンするようにします。メッセージがデバイスからトピックに発行されると、デバイス ID を含むプロパティがメッセージと一緒に送信されます。サブスクリプション フィルターはこのプロパティを使ってデバイス自体が発生したメッセージを除外します。このワークフローを図 15 に示します。

Device-to-Device Workflow
図 15 デバイスからデバイスへのワークフロー

まとめ

今回は多くのことを説明しました。ポータブル クラス ライブラリのおかげで今回のソリューションが容易になり、2 つのプラットフォームをターゲットにして記述すべきコードの量がとても少なくなりました。またアプリの状態変化をイベントによって扱うことにより、クラウドを使った状態の同期が実に簡単になりました。しかし、連続性のあるクライアントを開発する際に考慮する必要がある点について、まだ説明していないこともたくさんあります。具体的には、オフラインのイベント キャッシュ、フォールト トレランス (イベント発行時にネットワークが利用不可の場合の対応)、マージの競合 (他のユーザーが自分の変更と競合する変更を行った場合の対応)、再生 (買い物リストに新しいデバイスを参加させたときの状態の更新方法)、アクセス制御 (未承認ユーザーのデータ アクセスへの対応)、さらに最後に永続化です。この記事のサンプル コードのアプリは買い物リストの保存機能を備えていません。これは皆さんへの課題としておきます。これらの課題に対応するためのコードの改良は興味深いものとなるでしょう。永続化に関する単純な (または従来型の) 対応策の 1 つは、ShoppingList クラスに直接フックを置き、ShoppingList オブジェクトをシリアル化可能とマークし、ファイルへ保存する方法です。しかしこの方法を採る前に、ちょっと考えてみてください。ShoppingList は既にイベントにネイティブに対応しており、イベントの発生元には依存しないため、クラウドとの双方向のデータ同期とディスクへの保存と復元とはほぼ同様に扱うことができるのではありませんか。

David Kean はマイクロソフトの .NET Framework チームの開発者で、基本クラス ライブラリ (BCL) チームに所属しています。彼はそれ以前には、愛されもするが誤解されることも多いツール FxCop とその兄弟の Visual Studio Code Analysis に携わっていました。彼はオーストラリアのメルボルン出身で、今は米国シアトルに妻の Lucy と 3 人の子供 Jack 、Sarah、Ben と住んでいます。彼のブログは davesbox.com です。

この記事のレビューに協力してくれた技術スタッフの Nicholas BlumhardtImmo Landwerth に心より感謝いたします。