June 2010

Volume 25 Number 06

Silverlight オンライン - 不定期接続環境での Silverlight

Mark Bloodworth | June 2010

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

人々は、オンラインの世界で暮らしています。少なくとも、私たち数人は、ある時間オンラインの世界を利用しています。将来いつの日か、需要を上回る帯域幅が用意され、常時接続が広い範囲で利用できるようになるでしょうが、現時点ではそういうわけにはいきません。実際には、時折十分な帯域幅が利用できるようになるときに、不定期に接続されています。特定の時点にどのような接続状態になっているかがわかることはめったにありません。

このような状況で最高のユーザー エクスペリエンスを提供するアプリケーションを設計するためには、アーキテクチャの選択肢をいくつも検討することになります。

スマート クライアントは、リッチ クライアントかどうかにかかわらず、ローカル コンピューターに展開されるという共通の特性があります。このため、ネットワークに接続していなくても、アプリケーションを実行できます。しかし、従来のブラウザー ベースのアプリケーションを実行するには、リモート Web サーバーに接続しなければなりません。

この 2 種類の両極端なアプリケーションの間には、かつてないほど多彩な選択肢が増えています。こうした選択肢すべてによって、アプリケーションをオフラインで実行するためのさまざまな機能が提供され、UI 設計にさまざまなレベルの柔軟性と対話性がもたらされると同時に、さまざまなレベルのセキュリティ制限が課されます。今回の記事では、対話性に優れたユーザー エクスペリエンスを提供してブラウザー内でもブラウザー外でも実行できる、最新形態の不定期接続アプリケーションについて説明します。また、ネットワーク接続を検出するコード サンプルや、オンライン時にデータをアップロードおよびダウンロードするバックグラウンド ワーカーを示します。

背景

ここで説明する内容に関連性の深い、典型的なアプリケーションの進化について考えてみましょう。当初アプリケーションは、Windows オペレーティング システムのみで実行されるシンプルなシック クライアント アプリケーションでした。ユーザーはオフラインで作業できましたが、この初期ソリューションに関して、次のような制限事項が徐々に明らかになってきました。

  • 複数のオペレーティング システムをサポートするための要件が追加されました。最初のバージョンのままでは、見込みユーザーの一部しかサポートできません。
  • 展開の問題が原因で、ユーザー ベースにインストールされた複数のバージョンで矛盾が発生しました。

複数のオペレーティング システムで機能し、展開の問題を最小限に抑えた、より軽量なソリューションのサポートへの要請が高まったことから、アプリケーションはシンプルなシン クライアント HTML アプリケーションに書き直されるようになりました。しかし、今度は次のように別の問題が発生しました。

  • UI の機能が制限され、ユーザー エクスペリエンスがあまり直感的ではなくなりました。
  • 多くの時間を要するブラウザー互換性テストが必要になりました。
  • 多数のユーザーが利用するネットワーク インフラストラクチャでのパフォーマンスが低下しました。たとえば、ユーザーがフォームに入力しなければならなくなるたびに、大量の参照データをダウンロードしたり、検証に必要なロジックを提供するための大量のスクリプトをダウンロードしたりする必要がありました。
  • ユーザーがアプリケーションをオフラインで使用できなくなりました。

明らかにこのバージョンでも十分とは言えませんでした。

この場合の理想的ながらも実現困難なソリューションが、直感的で柔軟な UI を備えたリッチ インターネット アプリケーション (RIA: Rich Internet Application) です。RIA では、UI に縛られずに、オンラインになったときに、ユーザーが大量のデータを管理できるようにし、データのアップロードやデータ検証を非同期に実行できるようにする必要があります。オフライン作業や、クライアント上のデータ ストアへのアクセスをサポートすることも必要です。カメラなど、クライアント上のハードウェアとも一体化させる必要もあります。最後に、このような理想的なソリューションは、[スタート] メニューやアプリケーション アイコンから起動されるようにすべきです。つまり、Web ブラウザーの制約外に存在すべきです。

Silverlight は、このような要件を満たすことができます。Silverlight 3 ではブラウザー外での操作という考え方が導入され、Silverlight 4 で拡張されています。さらに、Silverlight 4 では、特定のフォルダー (マイ ピクチャなど) やハードウェア デバイス (Web カメラなど) を操作する機能も導入されました (この拡張機能を使用するアプリケーションでは、そのアプリケーションの信頼度を上げる必要があることがユーザーに通知され、このアプリケーションをインストールするにはユーザーの同意が必要になります。信頼されたアプリケーションの詳細については、msdn.microsoft.com/library/ee721083(v=VS.95) (英語) を参照してください)。今回の記事では、オンライン作業とオフライン作業の両方をサポートするアプリケーションを設計する際に発生する一般的な問題を中心に説明します。

図 1 に、有力な候補となる単純なアーキテクチャを示します。

候補となるアーキテクチャの概要
図 1 候補となるアーキテクチャの概要

この場合の代表的なユーザー シナリオには、次のようなものがあります。

  • ノート PC を携行するモバイル ワーカー。ノート PC に 3G カードが搭載されている場合もあれば、社内やインターネットのホット スポットでワイヤレス ネットワークに接続される場合もあります。
  • 古いオフィス ビルやプレハブ オフィス ビルなど、接続性が制限される環境でデスクトップ PC を使用するユーザー。

ネットワーク状態の検出

不定期接続環境で操作することを想定するアプリケーションでは、ネットワーク接続の現在状態を確認できなければなりません。Silverlight 3 では、NetworkInterface.GetIsNetworkAvailable メソッドによってこの機能が導入されました。また、このようなアプリケーションでは、ネットワーク インターフェイスの IP アドレスが変化すると発生する NetworkChange.NetworkAddressChangedEvent イベントも使用できます。

したがって、不安定な接続に対応するアプリケーションを構築する場合は、まず NetworkChange.NetworkAddressChangedEvent を処理します。このイベントを処理するわかりやすい場所は、Silverlight アプリケーションのエントリ ポイントとして機能する App クラスです。既定では、このクラスは App.xaml.cs ファイル (C# で作成する場合) または App.xaml.vb ファイル (VB.NET で作成する場合) に実装されます。ここからの例は、C# を使用することにします。次の Application_Startup イベント ハンドラーは、イベントをサブスクライブするのに適した場所でしょう。

private void Application_Startup(object sender, StartupEventArgs e)
{
  NetworkChange.NetworkAddressChanged += new
    NetworkAddressChangedEventHandler(NetworkChange_ 
    NetworkAddressChanged);
  this.RootVisual = newMainPage();
}

次の using ステートメントが必要です。

using System.Net.NetworkInformation;

NetworkChange_NetworkAddressChanged イベント ハンドラーには、ネットワーク検出の中核となる処理が含まれています。実装例を次に示します。

void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
{
  this.isConnected = (NetworkInterface.GetIsNetworkAvailable());
  ConnectionStatusChangedHandler handler = 
    this.ConnectionStatusChangedEvent;
  if (handler != null)
  {
    handler(this.isConnected);
  }
}

まず、GetIsNetworkAvailable を呼び出して、ネットワーク接続が存在するかどうかを確認します。この例では、次のように、プロパティを通じて公開されるフィールドに確認結果が格納され、使用するアプリケーションの他の部分に対してイベントが発生します。

private bool isConnected = (NetworkInterface.GetIsNetworkAvailable());

public event ConnectionStatusChangedHandlerConnectionStatusChangedEvent;

public bool IsConnected
{
  get
  { 
    return isConnected;
  }
}

この例には、現在のネットワーク接続を検出して処理するためのスケルトンが含まれています。しかし、ネットワーク (ループバック インターフェイスとトンネル インターフェイス以外) にコンピューターが接続されていれば必ず GetIsNetworkAvailable から true が返されるとしても、接続先のネットワークが有効でないことがあります。たとえば、コンピューターがルーターに接続されていてもルーターのインターネット接続が切断されている場合や、ユーザーがブラウザー経由でログインする必要がある公共の Wi-Fi アクセス ポイントにコンピューターが接続されている場合に、このような状態になることがあります。

有効なネットワーク接続が確立されているのを確認することは、堅牢なソリューションにする作業の一部にすぎません。さまざまな理由から、Silverlight アプリケーションが使用する Web サービスにアクセスできないことがあるため、不定期接続アプリケーションではこのような状況に対処することも同じくらい重要です。

Web サービスを使用できるかどうか確認する方法は、数多くあります。ファースト パーティ製の Web サービス (つまり、Silverlight アプリケーション開発者の管理下にある Web サービス) では、可用性の定期的な確認に使用できる、シンプルで何も操作を行わないメソッドを追加することが望ましい場合があります。しかし、このようなメソッドを追加できない場合 (たとえば、サードパーティ製の Web サービスの場合) や追加することが望ましくない場合は、タイムアウトを適切に構成して処理します。Silverlight 3 では、Windows Communication Foundation クライアント構成のサブセットを使用します。このサブセットは、"サービス参照の追加" ツールを使用すると自動的に生成されます。

データの保存

アプリケーションでは、ネットワーク環境の変化に対応できること以外に、オフライン時に入力されたデータに対処することも必要です。Microsoft Sync Framework (msdn.microsoft.com/sync、英語) は、データの種類、データ ストア、プロトコル、およびトポロジを幅広くサポートする包括的なプラットフォームです。この記事の執筆時点の Silverlight では Microsoft Sync Framework を使用できませんが、いずれ使用できるようになる予定です。詳細については、MIX10 のセッション (live.visitmix.com/MIX10/Sessions/SVC10、英語) をご覧になるか、ブログ記事 (blogs.msdn.com/sync/archive/2009/12/14/offline-capable-applications-using-silverlight-and-sync-framework.aspx、英語) を参照してください。Silverlight で Microsoft Sync Framework が使用できるようになれば、これを使用するのが最適な手法になるのは明らかです。それまでは、ギャップを埋めるために、簡単なソリューションが必要です。

監視可能なキュー

アプリケーションが現在オンラインかオフラインかをユーザーに通知することが適切な場合を除いて、UI 要素では、データがローカルに格納されているか、クラウドに格納されているかを意識する必要がないのが理想です。キューの使用は、UI とデータ記憶域のコードをこのように分離する優れた方法です。キューを処理するコンポーネントでは、キューに登録される新しいデータに対応できる必要があります。こうしたことすべてを解決できるのが、監視可能なキューです。図 2 に、監視可能なキューの実装例を示します。

図 2 監視可能なキュー

public delegate void ItemAddedEventHandler();

public class ObservableQueue<T>
{
  private readonly Queue<T> queue = new Queue<T>();

  public event ItemAddedEventHandler ItemAddedEvent;

  public void Enqueue(T item)
  {
    this.queue.Enqueue(item);
    ItemAddedEventHandler handler = this.ItemAddedEvent;
    if (handler != null)
    {
      handler();
    }
  }

  public T Peek()
  {
    return this.queue.Peek();
  }

  public T Dequeue()
  {
    return this.queue.Dequeue();
  }

  public ArrayToArray()
  {
    return this.queue.ToArray();
  }

  public int Count
  {
    get
    {
      return this.queue.Count;
    }
  }
}

このシンプルなクラスでは、標準のキューをラップし、データが追加されるとイベントを発生させます。このクラスは、追加されるデータの形式や型を想定しないジェネリック クラスです。監視可能なキューが役に立つのは、そのキューの監視側が存在する場合だけです。この場合の監視側は QueueProcessor というクラスです。QueueProcessor のコードを確認する前にもう 1 点検討しておく必要があるのはバックグラウンド処理です。QueueProcessor では、新しいデータがキューに追加されたという通知を受け取ると、そのデータをバックグラウンド スレッドで処理して UI の応答性を維持しています。この設計目標を達成するには、System.ComponentModel 名前空間で定義されている BackgroundWorker クラスが最適です。

BackgroundWorker

BackgroundWorker は、バックグラウンド スレッドで処理を実行するのに便利な手段です。このクラスでは、ProgressChanged と RunWorkerCompleted という、BackgroundWorker タスクの進行状況をアプリケーションに通知する手段を提供する 2 つのイベントが公開されています。DoWork イベントは、BackgroundWorker.RunAsync メソッドが呼び出されると発生します。BackgroundWorker の構成例を次に示します。

private void SetUpBackgroundWorker()
{
  backgroundWorker = new BackgroundWorker();
  backgroundWorker.WorkerSupportsCancellation = true;
  backgroundWorker.WorkerReportsProgress = true;
  backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
  backgroundWorker.ProgressChanged += new
    ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
  backgroundWorker.RunWorkerCompleted += new
    RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);
}

DoWork イベントのイベント ハンドラーのコードでは、キャンセルが保留中かどうかを定期的に確認する必要があることに注意してください。詳細については、MSDN ドキュメント (msdn.microsoft.com/library/system.componentmodel.backgroundworker.dowork%28VS.95%29) を参照してください。

IWorker

ObservableQueue のジェネリックな性質に合わせて、実行する処理の定義を BackgroundWorker の作成と構成から分離することがお勧めです。IWorker インターフェイスというシンプルなインターフェイスで、DoWork のイベント ハンドラーを定義しています。イベント ハンドラーのシグネチャに含まれている sender オブジェクトが BackgroundWorker になります。このオブジェクトによって、IWorker インターフェイスを実装するクラスから進行状況を通知できるようになり、保留中のキャンセルがあるかどうか確認できるようになります。IWorker の定義を次に示します。

public interface IWorker
{
  void Execute(object sender, DoWorkEventArgs e);
}

設計にこだわりがあって、まったく分離の必要がない箇所でも定義を分離するのは簡単なことです。IWorker インターフェイスの作成という発想は、実体験から生まれました。この記事で示すように、ObservableQueue の目的は確かに不定期接続アプリケーション向けソリューションの一部として使用することです。しかし、デジタル カメラからの写真のインポートなど、他の処理でも ObservableQueue を使用すると実装が簡単になることがわかります。たとえば、写真のパスを ObservableQueue に格納すると、IWorker の実装で画像をバックグラウンド処理できます。ObservableQueue をジェネリックにし、IWorker インターフェイスを作成したことから、元の問題に対処したままでこのようなシナリオが可能になります。

キューの処理

QueueProcessor は、ObservableQueue と IWorker の実装を統合して、役に立つ処理を実行するクラスです。キューの処理とは、BackgroundWorker を構成する作業です。この作業では、IWorker の Execute メソッドを BackgroundWorker.DoWork イベントのイベント ハンドラーとして設定し、ItemAddedEvent イベントをサブスクライブします。図 3 に、QueueProcessor の実装例を示します。

図 3 QueueProcessor の実装

public class QueueProcessor<T>
{
  private BackgroundWorker backgroundWorker;
  private readonly IWorker worker;

  public QueueProcessor(ObservableQueue<T>queueToMonitor, IWorker worker)
  {
    ((SampleCode.App)Application.Current).ConnectionStatusChangedEvent += new
       ConnectionStatusChangedEventHandler(QueueProcessor_
         ConnectionStatusChangedEvent);
    queueToMonitor.ItemAddedEvent += new
      ItemAddedEventHandler(PendingData_ItemAddedEvent);
    this.worker = worker;
    SetUpBackgroundWorker();
    if ((((SampleCode.App)Application.Current).IsConnected) && 
      (!backgroundWorker.IsBusy) 
      && (((SampleCode.App)Application.Current).PendingData.Count>0))
    {
      backgroundWorker.RunWorkerAsync();
    }
  }

  private void PendingData_ItemAddedEvent()
  {
    if ((((SampleCode.App)Application.Current).IsConnected) && 
      (!backgroundWorker.IsBusy))
    {
      backgroundWorker.RunWorkerAsync();
    }
  }

  private void SetUpBackgroundWorker()
  {
    backgroundWorker = new BackgroundWorker();
    backgroundWorker.WorkerSupportsCancellation = true;
    backgroundWorker.WorkerReportsProgress = true;
    backgroundWorker.DoWork += new DoWorkEventHandler(this.worker.Execute);
    backgroundWorker.ProgressChanged += new
      ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
    backgroundWorker.RunWorkerCompleted += new
      RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);
  }

  private void backgroundWorker_RunWorkerCompleted(object sender,
    RunWorkerCompletedEventArgs e)
  {
    if (e.Cancelled)
    {
      // Handle cancellation
    }
      else if (e.Error != null)
      {
        // Handle error
      }
    else
    {
      // Handle completion if necessary
    }
  }

  private void backgroundWorker_ProgressChanged(object sender, 

    ProgressChangedEventArgs e)
  {
     // Raise event to notify observers
  }

  private void QueueProcessor_ConnectionStatusChangedEvent(bool isConnected)
  {
    if (isConnected)
    {
      if (!backgroundWorker.IsBusy)
      {
        backgroundWorker.RunWorkerAsync();
      }
    }
    else
    {
      backgroundWorker.CancelAsync();
    }
  }
}

図 3 のサンプル コードでは、読者の皆さんの演習として、エラー処理など BackgroundWorker の機能の一部を実装していません。RunAsync メソッドは、アプリケーションが現在接続中の場合に BackgroundWorker でまだキューを処理していないときにのみ呼び出されます。

UploadWorker

QueueProcessor のコンストラクターには IWorker が必要です。つまり、データをクラウドにアップロードするには当然具体的な実装が必要です。UploadWorker は、このようなクラスに適した名前でしょう。図 4 に実装例を示します。

図 4 クラウドへのデータのアップロード

public class UploadWorker :  IWorker
{
  public override void Execute(object sender, DoWorkEventArgs e)
  {
    ObservableQueue<DataItem>pendingData = 
      ((SampleCode.App)Application.Current).PendingData;
    while (pendingData.Count>0)
    {
      DataItem item = pendingData.Peek();
      if (SaveItem(item))
      {
        pendingData.Dequeue();
      }
    }
  }

  private bool SaveItem(DataItem item)
  {
    bool result = true;
    // Upload item to webservice
    return result;
  }
}

図 4 では、キューに格納した項目を Execute メソッドでアップロードしています。アップロードできなければ、その項目はキューに残ります。BackgroundWorker にアクセスする必要がある場合 (進行状況の通知や保留中のキャンセルの確認など)、sender オブジェクトは BackgroundWorker クラスです。結果が e (DoWorkEventArgs型) の Result プロパティに代入されていれば、RunWorkerCompleted イベント ハンドラーでこの結果を使用できます。

分離ストレージ

アプリケーションが決して終了しないと仮定すれば、データをキューに格納し、接続時に Web サービスにデータを送信する (その結果データがクラウドに格納されるようにする) ことに問題はありません。保留中のデータがキューに残っているときにアプリケーションを終了する場合は、アプリケーションを次回起動するまでデータを格納するための方策が必要です。Silverlight には、このような状況に適した分離ストレージが備わっています。

分離ストレージは Silverlight アプリケーションで使用できる仮想ファイル システムで、ローカルにデータを格納できます。分離ストレージに格納できるデータ量は限られていますが (既定の制限値は 1 MB)、アプリケーションではユーザーからもっと大きな領域を要求できます。この記事の例では、キューを分離ストレージにシリアル化することで、アプリケーションのセッションとセッションの間でキューの状態が保存されます。この処理を実行するのが QueueStore という名前のシンプルなクラスです (図 5 参照)。

図 5 分離ストレージへのキューのシリアル化

public class QueueStore
{
  private const string KEY = "PendingQueue";
  private IsolatedStorageSettings appSettings = 
    IsolatedStorageSettings.ApplicationSettings;

  public void SaveQueue(ObservableQueue<DataItem> queue)
  {
    appSettings.Remove(KEY);
    appSettings.Add(KEY, queue.ToArray());
    appSettings.Save();
  }

  public ObservableQueue<DataItem>LoadQueue()
  {
    ObservableQueue<DataItem> result = new ObservableQueue<DataItem>();
    ArraysavedArray = null;

    if (appSettings.TryGetValue<Array>(KEY, out savedArray))
    {
      foreach (var item in savedArray)
      {
        result.Enqueue(item as DataItem);
      }
    }

  return result;
  }
}

キュー上の項目がシリアル化可能だとすれば、キューの保存と読み込みが QueueStore によって可能になります。App.xaml.cs ファイルの Application_Exit メソッドで SaveQueue メソッドを呼び出し、App.xaml.cs ファイルの App コンストラクターで LoadQueue メソッドを呼び出すと、アプリケーションのセッションとセッションの間で状態を保存できます。

キューに追加される項目は、DataItem 型です。これは、データを表す単純なクラスです。もう少し機能豊富なデータ モデルの場合、DataItem に簡単なオブジェクト グラフが含まれることがあります。さらに複雑なシナリオでは、DataItem を基本クラスとして他のクラスで継承される場合もあります。

データの取得

アプリケーションがときどきしか接続されない場合でも使用できるようにするには、データをローカルにキャッシュする機能が必要です。この機能に関してまず検討することは、アプリケーションで必要なデータのワーキング セットのサイズです。シンプルなアプリケーションであれば、必要なすべてのデータをローカル キャッシュに格納できるかもしれません。たとえば、RSS フィードを使用する単純なリーダー アプリケーションでは、フィードとユーザー設定だけをキャッシュすればよいでしょう。アプリケーションによっては、ワーキング セットのサイズが大きすぎるか、ユーザーが必要とするデータの予測が難しすぎるため、実質的にデータのワーキング セットのサイズが大きくなることがあります。

最初は、シンプルなアプリケーションから始めるのが簡単でしょう。アプリケーション セッション全体で必要なデータ (ユーザー設定など) は、アプリケーションの起動時にダウンロードしてメモリに格納できます。このデータをユーザーが変更するのであれば、前述のアップロードの方針を当てはめることができます。しかし、この手法では起動時にアプリケーションが接続されていることが前提となりますが、必ずしも接続されているとは限りません。ここでも、解決策は分離ストレージです。先ほど説明したサンプルはこのシナリオの要件を満たしているうえに、必要に応じてサーバー バージョンのデータをダウンロードするための呼び出しも含まれています。ユーザーが仕事用の PC と家庭用の PC の両方にアプリケーションをインストールしていることがあるため、ダウンロードに適したタイミングは、長時間中断した後にユーザーがアプリケーションを再開したときでしょう。

もう 1 つのシナリオとして、ニュースなど、比較的静的なデータを表示するシンプルなアプリケーションを使用していることがあります。このシナリオにも同様の方針を当てはめることができます。つまり、ダウンロードできる場合はデータをダウンロードし、そのデータをメモリに格納して、アプリケーションのシャットダウン時に分離ストレージに保存します (その後、起動時に分離ストレージから再度読み込みます)。接続を使用できるときは、キャッシュされたデータを無効にして最新の情報に更新できます。数分以上にわたって接続を使用できるときは、ニュースなどのデータを定期的に最新の情報に更新します。先ほど説明したように、UI ではこのバックグラウンド処理を意識すべきではありません。

ニュースをダウンロードする場合、次のようにシンプルな NewsItem クラスを最初に構築します。

public class NewsItem
{
  public string Headline;
  public string Body;

  public override stringToString()
  {
    return Headline;
  }
}

このクラスは、例として使用するために大幅に簡略化しており、UI にバインドしやすいよう ToString をオーバーライドしています。ダウンロードしたニュースを格納するには、バックグラウンドでニュースをダウンロードするシンプルな Repository クラスが必要です (図 6 参照)。

図 6 Repository クラスでのダウンロードしたニュースの格納

public class Repository
{
  private ObservableCollection<NewsItem> news = 
    new ObservableCollection<NewsItem>();
  private DispatcherTimer timer = new DispatcherTimer();
  private const int TIMER_INTERVAL = 1;

public Repository()
{
  ((SampleCode.App)Application.Current).ConnectionStatusChangedEvent += 
    new ConnectionStatusChangedHandler(Repository_
    ConnectionStatusChangedEvent);
  if (((SampleCode.App)Application.Current).IsConnected)
  {
    RetrieveNews();
    StartTimer();
  }
}

private void Repository_ConnectionStatusChangedEvent(bool isConnected)
{
  if (isConnected)
  {
    StartTimer();
  }
  else
  {
    StopTimer();
  }
}

private void StopTimer()
{
  this.timer.Stop();
}

private void StartTimer()
{
  this.timer.Interval = TimeSpan.FromMinutes(1);
  this.timer.Tick += new EventHandler(timer_Tick);
  this.timer.Start();
}

voidtimer_Tick(object sender, EventArgs e)
{
  if (((SampleCode.App)Application.Current).IsConnected)
  {
    RetrieveNews();
  }
}

private void RetrieveNews()
{
  // Get latest news from server
  List<NewsItem> list = GetNewsFromServer();
  if (list.Count>0)
  {
    lock (this.news)
    {
      foreach (NewsItem item in list)
      {
        this.news.Add(item);
      }
    }
  }
}

private List<NewsItem>GetNewsFromServer()
{
  // Simulate retrieval from server
  List<NewsItem> list = new List<NewsItem>();
  for (int i = 0; i <5; i++)
  {
    NewsItemnewsItem = new NewsItem()
    { Headline = "Something happened at " + 
        DateTime.Now.ToLongTimeString(),
        Body = "On " + DateTime.Now.ToLongDateString() + 
        " something happened.  We'll know more later." };
      list.Add(newsItem);
    }
    return list;
  }

  public ObservableCollection<NewsItem> News
  {
    get
    {
      return this.news;
    }
    set
    {
      this.news = value;
    }
  }
}

図 6 では、例を簡潔にするために、ニュース取得のシミュレーションを行っています。Repository クラスでは、ConnectionStatusChangedEvent をサブスクライブします。接続時には、DispatcherTimer を使用して、指定された間隔でニュースを取得します。シンプルなデータ バインドを実現するために、DispatcherTimer を ObservableCollection と組み合わせて使用しています。DispatcherTimer は、Dispatcher キューに統合されているため UI スレッドで実行されます。ObservableCollection が更新されるとイベントが発生し、UI のバインド コントロールが自動的に更新されます。この動作は、ニュースのダウンロードに最適です。Silverlight では System.Threading.Timer も使用できますが、UI スレッドでは実行されません。この場合、UI スレッドのオブジェクトにアクセスするすべての操作を Dispatcher.BeginInvoke を使用して呼び出す必要があります。

Repository を使用するために必要なのは、App.xaml.cs のプロパティだけです。Repository のコンストラクターで ConnectionStatusChangedEvent をサブスクライブしているので、Repository のインスタンス作成に最適な場所は、App.xaml.cs の Application_Startup イベントです。

アプリケーションがオンラインの間にそのアプリケーションのユーザー以外がユーザー設定などのデータを変更することはあまりありませんが、同じユーザーがさまざまなデバイスでアプリケーションを使用することで、このようなデータが変更されることがあります。ニュース記事などもあまり変更されないデータです。つまり、キャッシュされたデータをそのまま使えることが多く、再接続時に同期の問題が発生することは少なくなります。ただし、揮発性データの場合は別の手法が必要になることがあります。たとえば、データを最後に取得したタイミングをユーザーに通知して、ユーザーが適切に対応できるようにすることが望ましいでしょう。アプリケーションで揮発性データに基づく判断が必要な場合、揮発性データの有効期限に関する規則を設定し、オフラインのときは必要なデータを使用できないことをユーザーに通知する必要があります。

データが変更される可能性がある場合は、データが変更された時刻と場所を比較すれば、たいていは競合を解決できます。たとえば、オプティミスティックな手法では、最新バージョンが最も有効で、すべての競合でこのバージョンが優先されることを前提にしていますが、データが更新された場所に基づいて競合を解決することもできます。適切な手法を選択するうえで重要なのは、アプリケーション トポロジと使用シナリオを把握することです。バージョン間の競合を解決できない (解決してはならない) 場所では、このようなバージョンのログを保存して適切なユーザーに通知し、ユーザーが判断できるようにします。

また、同期ロジックの配置場所も検討する必要があります。最も単純な結論は、同期ロジックをサーバー上に配置して、バージョン間の競合を解決できるようにすることです。この場合、UploadWorker を少し変更して、DataItem が最新のサーバー バージョンに更新することになります。既に説明したように、いずれは Microsoft Sync Framework がこのような問題の多くに対処し、開発者はアプリケーション ドメインに集中できるようになります。

各部を組み合わせる

すべての機能の説明が完了したので、ここまで説明してきた機能を不定期接続環境で公開するシンプルな Silverlight アプリケーションを簡単に作成できます。図 7 に、このアプリケーションのスクリーンショットを示します。

ネットワークの状態とキューを使用するサンプル アプリケーション
図 7 ネットワークの状態とキューを使用するサンプル アプリケーション

図 7 の例の Silverlight アプリケーションはブラウザー外で実行されています。不定期接続アプリケーションの性質を考えると、ユーザーが [スタート] メニューやデスクトップから簡単にこのようなアプリケーションにアクセスでき、ネットワーク接続とは無関係に実行できるため、Silverlight アプリケーションの多くがブラウザー外で実行される可能性があります。一方、ブラウザーのコンテキスト内で実行される Silverlight アプリケーションには、そのアプリケーションが配置されている Web サーバーで Web ページや Silverlight アプリケーションを提供できるように、ネットワーク接続が必要です。

図 7 のシンプルな UI がデザイン賞を獲得できる水準に達していないのはほぼ確実ですが、この UI にはサンプル コードを実行する手段が用意されています。現在のネットワーク状態を表示する以外に、この UI には、データの入力用フィールドと、Repository の News プロパティにバインドされるリスト ボックスが備わっています。図 8 に、この画面を生成する XAML を示します。また、図 9 に分離コードを示します。

図 8 サンプル アプリケーションの UI の XAML

<UserControl x:Class="SampleCode.MainPage" 
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
  xmlns:dataInput="clr-namespace:System.Windows.Controls;
    assembly=System.Windows.Controls.Data.Input" 
    Width="400" Height="300">
      <Grid x:Name="LayoutRoot" Background="White" ShowGridLines="False">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="12" />
          <ColumnDefinition Width="120" />
          <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="12" />
          <RowDefinition Height="64" />
          <RowDefinition Height="44" />
          <RowDefinition Height="34" />
          <RowDefinition Height="34" />
          <RowDefinition Height="100" />
        </Grid.RowDefinitions>
        <Ellipse Grid.Row="1" Grid.Column="1" Height="30" HorizontalAlignment="Left" 
          Name="StatusEllipse" Stroke="Black" StrokeThickness="1" 
          VerticalAlignment="Top" Width="35" />
        <Button Grid.Row="4" Grid.Column="1" Content="Send Data" Height="23" 
          HorizontalAlignment="Left" Name="button1" VerticalAlignment="Top" 
          Width="75" Click="button1_Click" />
        <TextBox Grid.Row="2" Grid.Column="2" Height="23" HorizontalAlignment="Left" 
          Name="VehicleTextBox" VerticalAlignment="Top" Width="210" />
        <TextBox Grid.Row="3" Grid.Column="2" Height="23" HorizontalAlignment="Left" 
          Name="TextTextBox" VerticalAlignment="Top" Width="210" />
        <dataInput:Label Grid.Row="2" Grid.Column="1" Height="28" 
          HorizontalAlignment="Left" Name="VehicleLabel" VerticalAlignment="Top" 
          Width="120" Content="Title" />
        <dataInput:Label Grid.Row="3" Grid.Column="1" Height="28" 
          HorizontalAlignment="Left" Name="TextLabel" VerticalAlignment="Top" 
          Width="120" Content="Detail" />
        <ListBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Height="100" 
          HorizontalAlignment="Left" Name="NewsListBox" 
          VerticalAlignment="Top" Width="376" />
      </Grid>
</UserControl>

図 9 サンプル アプリケーションの UI の分離コード

public delegate void DataSavedHandler(DataItem data);

public partial class MainPage : UserControl
{
  private SolidColorBrush STATUS_GREEN = new SolidColorBrush(Colors.Green);
  private SolidColorBrush STATUS_RED = new SolidColorBrush(Colors.Red);

  public event DataSavedHandlerDataSavedEvent;

  public MainPage()
  {
    InitializeComponent();
    ((SampleCode.App)Application.Current).ConnectionStatusChangedEvent += new
      ConnectionStatusChangedHandler(MainPage_ConnectionStatusChangedEvent);
    IndicateStatus(((NetworkStatus.App)Application.Current).IsConnected);
    BindNews();
  }

  private void MainPage_ConnectionStatusChangedEvent(bool isConnected)
  {
    IndicateStatus(isConnected);
  }

  private void IndicateStatus(bool isConnected)
  {
    if (isConnected)
    {
      StatusEllipse.Fill = STATUS_GREEN;
    }
    else
    {
      StatusEllipse.Fill = STATUS_RED;
    }
  }

  private void BindNews()
  {
    NewsListBox.ItemsSource = 
      ((SampleCode.App)Application.Current).Repository.News;
  }

  private void button1_Click(object sender, RoutedEventArgs e)
  {
    DataItem dataItem = new DataItem
    {
      Title = this.TitleTextBox.Text,
      Detail = this.DetailTextBox.Text
    };
    DataSavedHandler handler = this.DataSavedEvent;
    if (handler != null)
    {
      handler(dataItem);
    }

    this.TitleTextBox.Text = string.Empty;
    this.DetailTextBox.Text = string.Empty;
  }
}

図 9 のクラスでは、データが保存されたことを示すイベントが発生します。監視側 (この場合は App.xaml.cs) では、このイベントを監視して、ObservableQueue にデータを格納します。

新しい種類のアプリケーション

ときどきしか接続されないアプリケーションが Silverlight でサポートされることで、新しい種類のアプリケーションが実現します。この新しい種類のアプリケーションによって、開発者には接続時と切断時のアプリケーションの動作を検討するという新たな課題がもたらされます。今回の記事では、このような問題点を説明し、問題に対処するための方針とサンプル コードを紹介しました。もちろん、不定期接続環境にさまざまなシナリオが存在することを考えれば、このようなサンプルは出発点にすぎません。

Mark Bloodworth は、マイクロソフトの開発者プラットフォーム エバンジェリズム (DPE) チームのアーキテクトであり、革新的なプロジェクトで複数の企業と連携しています。マイクロソフトに入社する前は、BBC Worldwide のチーフ ソリューション アーキテクトとして、アーキテクチャとシステム分析を担当するチームを率いていました。ほとんどの仕事では、マイクロソフト テクノロジの使用、特に Microsoft .NET Framework の使用を中心に取り組んできましたが、Java の経験もあります。彼のブログは remark.wordpress.com (英語) です。

Dave Brown は、インターネット関連技術を扱う Microsoft Consulting Services から始まって、9 年以上マイクロソフトに在籍してきました。現在は、英国の開発者プラットフォーム エバンジェリズム (DPE) チームで Microsoft Technology Centre のアーキテクトとして、顧客シナリオのビジネス分析、ソリューション アーキテクチャの設計、および概念実証ソリューション用コードの管理と開発の両方に携わっています。彼のブログは drdave.co.uk/blog (英語) です。

技術スタッフの Ashish Shetty に心より感謝いたします。