Windows Phone 7

Windows Phone 7 の分離ストレージ用の Sterling

Jeremy Likness

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

Windows Phone 7 のリリースにより、約 100 万人の Silverlight 開発者が、ほぼ一夜のうちにモバイル プログラマになるチャンスを得ました。

Windows Phone 7 向けのアプリケーションは、Silverlight と同じ言語 (C# または Visual Basic) を使い、ブラウザー バージョンの Silverlight 3 とほぼ同じフレームワーク上で作成します。そのため、XAML による画面のレイアウトや、Expression Blend による画面の編集が可能です。ただし、携帯電話向けの開発では、ユーザーがアプリケーションを切り替える ("トゥームストーン") ときに必要な特殊な管理と、状態管理の限られたサポートが相まって、独自の課題が生まれます。

Sterling は、分離ストレージを基盤とするオープン ソースのデータベース プロジェクトで、Windows Phone 7 アプリケーションのローカル オブジェクトとリソースの管理や、トゥームストーン プロセスの簡略化に役立ちます。このオブジェクト指向データベースは、軽量で動作が速く、使いやすくなるよう設計され、永続化、キャッシュ、状態管理などの問題を解決します。使い方は簡単で、型定義に変更を加えたりマッピングしたりすることなく、既存の型定義で機能します。

今回の記事では、Windows Phone 7 の開発者が、 Sterling ライブラリを活用してほとんど手間をかけずに携帯電話でデータの保存や照会をローカルに行う方法と、トゥームストーン中にアプリケーションが非アクティブになっているときに状態を管理するための簡単な手法を説明します。

トゥームストーンの基礎

ブラウザーや携帯電話での Silverlight は、特殊な "セキュリティ サンドボックス" で実行され、アプリケーションのランタイム環境とホスト システムが分離されます。携帯電話の環境では、プラットフォーム上に複数のアプリケーションが共存する可能性があるため、複雑な層が追加されています。携帯電話の基盤となる OS はマルチタスクをサポートしますが、サードパーティのアプリケーションはこうした層にアクセスできるようにはなっていません。
アプリケーションが "フォアグラウンド" になれば実行の機会が与えられますが、別のアプリケーションが起動されたとき、電話がかかってきたとき、"戻る" ボタンや検索などのハードウェア機能が操作されたときは、それらの機能を実行するために即座に切り替えられてしまう可能性があります。アプリケーションが非アクティブになると、そのまま "強制終了" されることもあれば、ユーザーがそのアプリケーションに操作を戻せば "生き返る" こともあります。これがトゥームストーンと呼ばれるプロセスです。

Windows Phone 7 の "Mango" アップデートでは、"fast application switching" (アプリケーションの高速切り替え) が提供され、トゥームストーンのシナリオに制限が課されます。その結果、アプリケーションは自動的にトゥームストーン状態になることはなくなります。この機能は Windows Phone 7 Developer Tools には既に搭載されていますが、これによってトゥームストーンのシナリオがまったくなくなるわけではないことを認識しておく必要があります。アプリケーションがトゥームストーン状態になるかどうかに影響を与える要因は、実行中の別のアプリケーションや使用可能なメモリなどです。

トゥームストーンの問題は、アプリケーションが復活するときに、アプリケーションに含まれるページのインスタンスが新しく作り直される点にあります。その結果、切り替えが行われる前にユーザーが作業していたすべての状態 (一覧からの項目の選択やテキストの入力) が失われてしまいます。ユーザーにシームレスなエクスペリエンスを提供するために、この状態を保持し、アプリケーションが復活するときにその状態を復元するかどうかは、開発者しだいです。フォームへの入力中に検索ボタンをクリックして語句を調べてアプリケーションに戻ったら、それまでの入力が消去された空白のページが表示されたときのユーザーの混乱を思い浮かべてみてください。

携帯電話の状態を保存する

さいわい、Windows Phone 7 では、状態を保存するメカニズムがいくつか提供されます。OS 上のアプリケーションを操作するには、このような手法に詳しくなる必要があります。選択肢には、(Mango アップデートでサポートされる) SQL CE、ページ状態ディクショナリ、アプリケーション状態ディクショナリ、分離ストレージなどがあります。今回は分離ストレージに注目しますが、Sterling が携帯電話に有効である理由を理解するため、最初の 3 つを簡単に確認しておきましょう。

SQL CE: Mango アップデートでは、広く採用されている SQL Server データベースのコンパクト版である SQL CE が提供されます。このデータベースと他の選択肢の違いは、リレーショナル データベースである点です。SQL CE は、クラスやコントロールからマップする必要がある、特殊なテーブル形式を使用します。オブジェクト指向のデータベースは、(入れ子になったクラス、リスト、その他の型が含まれていても) 既存のクラス構造を受け取り、追加のマッピングや変更なしにシリアル化できます。

ページ状態: 各 Page オブジェクトは、キー名と関連オブジェクトによって定義されるディクショナリを公開する State プロパティを提供します。さまざまなキーやオブジェクトが使用される可能性がありますが、オブジェクトはシリアル化可能でなければならないため、"簡易" (最上位レベルの) シリアル化のみが行われます。さらに、ページ状態のデータは 1 ページあたり 2 MB まで (アプリケーション全体には 4 MB まで) という制限があり、ページの OnNavigatedTo メソッドを開始してから、OnNavigatedFrom メソッドの前までしか使用できません。そのため、実際に使用できるのは簡単な値型のみです。これは、ページのナビゲーション メソッド内でしか機能しないため、個別のビューモデルを使用して表示状態を同期するモデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) などのパターンにはあまり適していません。

アプリケーション状態: これもディクショナリです。ページ状態と同様、アプリケーション状態も文字列キーとオブジェクト値を使用し、オブジェクトはシリアル化できなければなりません。携帯電話のアプリケーション オブジェクトには、トゥームストーン状態が発生したら呼び出される Deactivated イベントと、アプリケーションがトゥームストーン状態から復帰するときに呼び出される Activated イベントがあります。アプリケーション状態には、アクティブになってから非アクティブになるまでの間いつでもアクセスすることができ、(1 つのアプリケーション規模のインスタンスを参照する Current プロパティがある) 静的 PhoneApplicationService クラスを使用してアクセスします。アプリケーション状態ディクショナリのサイズの制限は文書化されていませんが、あまりにも多くの項目を保存しようとすると、COM のハンドルされない例外がスローされます。

分離ストレージ: これは状態を保存するという観点からは、柔軟性の最も高い選択肢です。分離ストレージは携帯電話特有のものではなく、実際は Silverlight や Microsoft .NET Framework ランタイムでもほぼ同じように機能します。分離ストレージは、ホスト ファイル システムからの抽象化層を提供します。このため、直接ファイル ストレージを扱うのではなく、分離されたサンドボックスにフォルダーやファイルを提供する間接メカニズムとのインターフェイスを操作することになります。Windows Phone 7 では、このサンドボックスは、携帯電話のアプリケーション レベルで分離されます。分離ストレージにはアプリケーション内の任意の場所からアクセスできますが、携帯電話上に存在する別のアプリケーションからはアクセスできません。

また、分離ストレージには、いくつか優れたメリットがあります。分離ストレージは、先ほど説明したページ設定やアプリケーション設定に似た設定ディクショナリを提供しますが、データをフォルダーに整理したり、ファイルにまとめたりすることもできます。実際、分離ストレージでは、あらゆるファイルの種類 (XML、バイナリ、テキストなど) の作成やアクセスが可能です。携帯電話の分離ストレージにはサイズ制限はなく、実際には使用可能なメモリとストレージの容量のみに制限されます。唯一のデメリットは、ストレージへの書き込みプロセスとストレージからの取得プロセスが、アクティブ メモリにリストを保存する他の方式に比べていくぶん遅いことです。

シリアル化の選択肢

分離ストレージのもう 1 つのメリットは、シリアル化に複数の手法を選択できることです。キーとオブジェクトを割り当てる設定とは異なり、分離ストレージのメカニズムは、テキスト、XML、JSON、またはバイナリ コードを使用して書き込むことができるファイル ストリームを提供します。これにより、多種類のオブジェクトを簡単にわかりやすくシリアル化できます。これには、複雑なオブジェクト グラフを処理して、操作しているインスタンスの子や孫などもシリアル化する "階層の深い" シリアル化も含まれます。

JSON および XML: JSON と XML は、Silverlight におけるシリアル化の 2 つの主要手法です。XML は、(XML を生成する) DataContractSerializer か XMLSerializer のいずれかを通じて使用できます。DataContractSerializer は、Windows Communication Foundation (WCF) を使用する開発者になじみがあり、Web サービス経由で送信されるメッセージの取り込みや格納にフレームワークが使用するものです。データは、DataContract 属性と DataMember 属性を使用してマークする必要があります。これは、シリアル化するフィールドに明示的にフラグを設定するため、本質的には "オプトイン" のアプローチです。XmlSerializer は、getter と setter を使ってすべてのパブリック プロパティをシリアル化します。この動作は、特殊な XML 固有の属性を使用して変更できます。DataContractSerializer の詳細については https://msdn.microsoft.com/ja-jp/library/system.runtime.serialization.datacontractserializer(VS.95).aspx を、XmlSerializer の詳細については https://msdn.microsoft.com/ja-jp/library/system.xml.serialization.xmlserializer(VS.95).aspx を参照してください。

JSON は JavaScript Object Notation の略で、JavaScript オブジェクトに簡単に変換できるため Web でよく使用されます。XML よりもコンパクトな形式でシリアル化されるオブジェクトに読みやすいテキストを提供するため、この手法を好む開発者もいます。JSON のシリアル化は、DataContractJsonSerializer という特別なバージョンのデータ コントラクト シリアライザーを使用して実現できます。DataContractJsonSerializer が生成する出力のサイズは、XML よりもはるかに少なく、ディスク領域をあまり占有しません。DataContractJsonSerializer の詳細については、https://msdn.microsoft.com/ja-jp/library/system.runtime.serialization.json.datacontractjsonserializer.aspx を参照してください。

バイナリ: Silverlight と Windows Phone 7 では、バイナリ シリアル化も使用できます。Silverlight では BinaryFormatter を使用できないため (このクラスを使用すると、自動的にオブジェクトをバイナリにシリアル化することができます)、シリアル化を独自に処理する必要があります。バイナリ シリアル化を使用するには、バイナリ ライターを作成して、ストリームに書き込むだけです。バイナリ ライターは多くのプリミティブに対処するため、開発者はインスタンスをシリアル化するためにクラスのプロパティを出力して、インスタンスを作成し、リーダーを使用してプロパティを設定することに専念できます。ただし、クラスがたくさんあるプロジェクトでは、これは非常に煩わしい作業になります。そこでいよいよ Sterling の登場です。

Sterling: Silverlight でのオブジェクトのシリアル化とシリアル化解除の煩わしさを緩和することに特化して設計されています。Sterling は、元はブラウザーの Silverlight 4 用に作成されたものでしたが、ほぼ同一のコードベースで Windows Phone 7 をサポートするよう進化しました。内部では、バイナリ シリアル化を使用しているため、ディスク上では非常にコンパクトな形式になります。ほぼすべてのクラスをシリアル化でき、指定したキーを使用してインスタンスを整理します (インスタンスのすべてのプロパティをキーとして指定できます)。また、処理速度を上げるために、ディスクからインスタンス全体を読み込む前にメモリ内でクエリできるインデックスも提供します。Sterling は、基盤となるシリアル化ストリームを完全に制御できるため、バイナリ ストリームを暗号化、圧縮、またはオーバーライドして、思いどおりに型をシリアル化することができます。

Sterling が提供するメリットは、オブジェクトを迅速かつ簡単にシリアル化できることと、LINQ to Objects を使用して驚くほど高速にキーとインデックスをクエリできることです。Sterling は、外部キーとリレーションシップを適切に処理します。これらのメリットすべてが、シリアル化およびシリアル化解除の速度をわずかに犠牲にするだけで手に入ります。

図 1 は、シリアル化のさまざまな手法と、ディスク上の相対サイズを比較したものです。

Size-on-Disk Comparison of Various Serialization Strategies

図 1 シリアル化のさまざまな手法のディスク上のサイズの比較

この図を生成するために、ランダムな名前と住所を使用して 2000 件の連絡先のコレクションを作成しました。連絡先レコードには、完全な名前、住所、および一意 ID を含みます (図 2 参照)。

The Contact Class

図 2 Contact クラス

次に、Sterling を含むさまざまなシリアライザーを使用して、これらを保存します。計算したサイズはディスク上の合計サイズで、これには Sterling がインデックスとキーを追跡するために必要な追加ファイルも含みます。

ディスクとの完全なシリアル化は、オブジェクト グラフを調べてインデックスとキーを処理するというオーバーヘッドのため、Sterling はやや遅くなります。図 3 のグラフは、各選択肢が 2,000 件すべての連絡先を保存して読み込む際の速度を比較しています。

Comparison of Speed

図 3 速度の比較

3 つ目のクエリの統計では、"L" で始まる名前で連絡先のコレクションをスキャンしてその連絡先をすべて読み込むという処理を行いましたが、Sterling が最も迅速にこれを処理しています。実行した例では、このクエリは 2,000 件の中から 65 件の連絡先を返しました。Sterling は、別の手法が 2 秒以上かかったのに対し、たった 110 ミリ秒でそれらをフィルター処理して読み込みました。

レシピ アプリケーション

Windows Phone 7 での Sterling の使用例を実際に示すため、レシピの参照と編集や、新しいレシピの追加が可能な簡単なレシピ アプリケーションを作成しました。各レシピは、名前、手順、カテゴリ (昼食や夕食など)、および材料から構成されます。材料には、量、軽量の単位、食品があります。食品は実行中に追加できます。図 4 は、アプリケーションからサンプル ページを実行したところです。

Edit Recipe Screen

図 4 [Edit Recipe] (レシピの編集) 画面

このアプリケーションでは、Sterling が 3 つの異なる役割を果たします。1 つ目は、カテゴリ、計量単位、最初の食品、サンプル レシピなど、アプリケーションが使用する参照データをアプリケーションの起動時に読み込めるようにします。2 つ目は、ユーザーが独自のレシピや食品を追加するときに入力したデータを保存します。最後に、アプリケーションが非アクティブになったとき、アプリケーションの状態をシリアル化することでトゥームストーンを支援します。サンプル アプリケーションは MVVM パターンを使用しており、ビューモデルからトゥームストーンを行う方法を実証しています。

最初の手順は、当然、Sterling をプロジェクトに追加することです。そのためには、NuGet ("Sterling" で検索してください) を使用するか、CodePlex (sterling.codeplex.com、英語) からバイナリと完全なソースをダウンロードします。

データベースを設定する

次に、データベースを設定します。データベースの構成では、保存する型の種類と、使用するキーとインデックスを定義します。Sterling データベースは、BaseDatabaseInstance から派生することによって作成するクラスです。提供する必要があるオーバーロードは 2 つあり、データベースの一意名 (名前は、アプリケーションごとに一意です。データを分離したり、異なるバージョンを管理したりするために、複数のデータベースをホストすることができます) と、テーブル定義のリストです。基本クラスは、テーブル、キー、およびインデックスを定義するためのヘルパー メソッドを提供します。

キーとインデックス: このレシピ アプリケーションでは、FoodModel 型のテーブルを定義し、FoodName プロパティをインデックス、オブジェクト Id をキーとして使用します。これにより、オブジェクトのメモリ内リストを作成し、瞬時にクエリおよびフィルター処理できるようにします。

次のコードは、整数キーと文字列インデックスを備えた、食品の "テーブル" を定義します。

CreateTableDefinition<FoodModel, int>(f => f.Id)
.WithIndex<FoodModel, string, int>(IDX_FOOD_NAME, f=>f.FoodName)

テーブル定義を作成する呼び出しは、クラス型とキー型を受け取り、これをキーの解決に使用するラムダ式に渡します。クラスで一意になる null 以外の値をキーに使用できます。インデックス拡張メソッドには、テーブルの型、インデックスの型、およびキーの型が必要です。ここではインデックス名と、インデックスが使用する値を提供するラムダ式を、定数で定義します。

トリガーを使用する ID: Sterling では、キーに任意の型を使用できるため、新しいキーを自動生成する組み込み機能はありません。代わりに、トリガーを使用してキーの生成方法を指定できます。トリガーは Sterling データベースに登録され、保存前、保存後、および削除前に呼び出されます。保存前の呼び出しでは、インスタンスを検査して、キーがまだ存在していなければ生成します。

この記事付属のコード ダウンロードに含まれるサンプル コードでは、すべてのエンティティが整数キーを使用しています。そのため、キーを生成するには、インスタンスの型に基づいてジェネリックなトリガーを作成できます。トリガーのインスタンスが最初に作成されるとき、トリガーは、データベースに既存のキーをクエリして、最も高い値のキーを見つけます。レコードが存在しなければ、トリガーは開始キーを 1 に設定します。0 以下のキーを持つインスタンスが保存されるたびに、トリガーは次の数値を割り当て、キーを増加します。この基本トリガーのコードを図 5 に示します。

図 5 自動キー生成を行う基本のトリガー

public class IdentityTrigger<T> : BaseSterlingTrigger<T,int>  
  where T: class, IBaseModel, new()
{
  private static int _idx = 1;

  public IdentityTrigger(ISterlingDatabaseInstance database)
  {
    // If a record exists, set it to the highest value plus 1
    if (database.Query<T,int>().Any())
    {
      _idx = database.Query<T, int>().Max(key => key.Key) + 1;
    }
  }

  public override bool BeforeSave(T instance)
  {
    if (instance.Id < 1)
    {
      instance.Id = _idx++;
    }

    return true;
  }

  public override void AfterSave(T instance)
  {
    return;
  }

  public override bool BeforeDelete(int key)
  {
    return true;
  }
}

クエリでは、Any や Max など、標準の LINQ 式を使用できます。また、開発者は、トリガーのメカニズムをスレッド セーフにする必要があります。

トリガーを使用するのは簡単です。トリガーをデータベースに登録して、インスタンスを渡すだけです (これにより、必要なコンストラクターのパラメーターを渡すことが可能になります)。同じような呼び出しで、トリガーの登録を解除できます。

カスタム シリアライザー: Sterling は、追加の設定なしに使用できるさまざまな型をサポートします。クラスと構造体を見つけると、コンテンツをシリアル化するために、それらのパブリック プロパティとフィールドを反復処理します。サブクラスと構造体も、再帰的に反復処理します。Sterling には、直接シリアル化できない基本型がいくつかあります。たとえば System.Type は、多くの可能性のある派生クラスを持つ抽象クラスとして定義されます。Sterling は、この型を直接シリアル化またはシリアル化解除することはできません。トゥームストーンをサポートするために、ビューモデルのプロパティを格納し、ビューモデルの型をキーとして使用する特殊なクラスが作成されます。Sterling は、こうした型を扱うため、カスタム シリアライザーの作成を可能にします。

カスタム シリアライザーを作成するには、BaseSerializer から派生し、オーバーロードを処理します。カスタム TypeSerializer クラスでは、System.Type から派生するすべてのクラスをサポートし、シリアル化は単にアセンブリ修飾型名を書き出します。シリアル化解除では、アセンブリ修飾名から型を返すために、Type クラスの静的 GetType メソッドを使用します。この結果を図 6 に示します。これは、System.Type から派生する (System.Type に割り当てることができる) すべての型を明示的にサポートします。

図 6 TypeSerializer

public class TypeSerializer : BaseSerializer 
{
  /// <summary>
  ///     Return true if this serializer can handle the object, 
  ///     that is, if it can be cast to type
    /// </summary>
  /// <param name="targetType">The target</param>
  /// <returns>True if it can be serialized</returns>
  public override bool CanSerialize(Type targetType)
  {
    return typeof (Type).IsAssignableFrom(targetType);
  }

  /// <summary>
  ///     Serialize the object
  /// </summary>
  /// <param name="target">The target</param>
  /// <param name="writer">The writer</param>
  public override void Serialize(object target, 
    BinaryWriter writer)
  {
    var type = target as Type;
    if (type == null)
    {
      throw new SterlingSerializerException(
        this, target.GetType());
    }
    writer.Write(type.AssemblyQualifiedName);
  }

  /// <summary>
  ///     Deserialize the object
  /// </summary>
  /// <param name="type">The type of the object</param>
  /// <param name="reader">A reader to deserialize from</param>
  /// <returns>The deserialized object</returns>
  public override object Deserialize(
    Type type, BinaryReader reader)
  {
    return Type.GetType(reader.ReadString());
  }
}

すべてのカスタム シリアライザーは、アクティブにする前に Sterling エンジンに登録します。

データのシードと保存

データベースを定義したら、"データをシードする" のが一般的です。サンプル アプリケーションでは、ユーザーがアプリケーションを使い始められるように、カテゴリの一覧、標準の計量単位、および食品を提供します。また、レシピのサンプルも提供します。データの埋め込みにはいくつか方法がありますが、最も簡単なのは、データを XAP ファイルにリソースとして含めることです。このデータは、その後、アプリケーションが初めて実行されてデータベースに格納されるときに、リソース ストリームとして解析することができます。

トゥームストーンのプロセスを操作するために、アプリケーション自体がアクティブになるときに Sterling データベース エンジンもアクティブになります。また、アプリケーション自体がトゥームストーン状態になるか終了されると、非アクティブになります。これにより、データベース キーとインデックスがディスクに確実にフラッシュされるようになり、データベースを再び使用するときに状態が安定するようになります。App.xaml.cs ファイルでは、これらのイベントを携帯電話のライフサイクルと結び付けることができます。データベースの設定に必要なのは、次のようにわずか数行のコードだけです。

_engine = new SterlingEngine();
_engine.SterlingDatabase.RegisterSerializer<TypeSerializer>();
_engine.Activate();
Database =  
  _engine.SterlingDatabase.RegisterDatabase<RecipeDatabase>();

このコード スニペットでは、エンジンを作成して、カスタム シリアライザーを登録してからエンジンをアクティブにして、データベースの使用準備を整えるという手順を示しています。次のコードは、アプリケーションがトゥームストーン状態になるとき、または終了されるときに、エンジンとデータベースをシャットダウンする方法を示しています。

Database.Flush();
_engine.Dispose();
Database = null;
_engine = null;

データベースがアクティブになると、データを受け取れるようになります。データをパッケージ化する最も一般的な方法は、データを XML、JSON、CSV などの読みやすい形式で埋め込みリソースとして含めることです。データベースへのクエリによって、データが既に存在しているかどうかを判断でき、存在しなければデータを読み込みます。Sterling へのデータの保存は簡単です。単に保存するインスタンスを渡せば、あとは Sterling が処理します。図 7 にカテゴリをチェックするクエリを示します。カテゴリが存在しなければ、データベースをシードするために埋め込みリソースのファイルからデータを読み取ります。図 7 では、最初にテーブルをクリアするために切り捨て操作が使用されていることに注目してください。

図 7 データのシード

if (database.Query<CategoryModel, int>().Any()) return;

// Get rid of old data
database.Truncate(typeof(MeasureModel));
database.Truncate(typeof(FoodModel));
                
var idx = 0;

foreach(var measure in ParseFromResource(FILE_MEASURES,
  line =>
  new MeasureModel
  { Id = ++idx, Abbreviation = line[0], FullMeasure = line[1]} ))
{
  database.Save(measure);
}

// Sample foods auto-generate the id
foreach (var food in
  ParseFromResource(FILE_FOOD, line 
    => new FoodModel { FoodName = line[0] })
    .Where(food => !string.IsNullOrEmpty(food.FoodName)))
{
  database.Save(food);
}

var idx1 = 0;

foreach (var category in ParseFromResource(FILE_CATEGORIES,
  line =>
  new CategoryModel { Id = ++idx1, CategoryName = line[0] }))
{
  database.Save(category);
}

メインのビューモデル: カテゴリとレシピ

シードしたデータベースの解析と読み込みが終わると、アプリケーションの残りの部分では、既存のデータを使用してリストとプロンプトをユーザーに表示し、ユーザーが入力した情報をすべて保存します。以下に、シードしたデータをアプリケーションが使用する方法を示す例を紹介します。次のコード スニペットは、すべてのカテゴリをメインのビューモデルの識別可能なコレクションに読み込みます。

Categories = new ObservableCollection<CategoryModel>();
foreach(var category in App.Database.Query<CategoryModel,int>())
{
  Categories.Add(category.LazyValue.Value);
}

Categories コレクションを、ピボット コントロールに直接バインドできるようになります。ピボット コントロールは、通常、大量のデータ セットをフィルター処理したビューを提供する目的で使用します。カテゴリは、食事の種類 (朝食、昼食といった分類) を表し、ピボットは、カテゴリが選択されたときに関連するレシピを表示します。各カテゴリのレシピは、現在選択されているカテゴリに基づいてフィルター処理するクエリによって公開されます。

次の XAML スニペットは、コレクションと選択されたカテゴリにコントロールを直接バインドするようすを示しています。

<controls:Pivot        
  x:Name="pivotMain"
  Title="Sterling Recipes"
  ItemsSource="{Binding Categories}"
  SelectedItem="{Binding CurrentCategory,Mode=TwoWay}">

食材の編集: 外部キー

もちろん、レシピで重要なのは使う食材です。アプリケーションは、食品と計量単位からなる "マスター リスト" を保持します。各レシピは、量、計量の単位、食品などからなる "食材" のリストを保持します。[Ingredients] (食材) ボタン (図 4 参照) は、ユーザーが既存の食材を追加、削除、または編集できる食材リストにユーザーを移動します。

この機能は、外部キーのように動作するナビゲーション プロパティをサポートする、Sterling の重要な機能によって可能になります。今回のアプリケーションで使用するモデルの階層を図 8 に示します。

Recipe Class Hierarchy

図 8 レシピのクラス階層

レシピには、親レシピへの循環参照のある食材のリストが含まれています。食材には、単位と量を含む "量" モデルも含まれています。

レシピを保存するとき、Sterling は、各食材を別のテーブル定義の別のエントリとして自動的に認識します。Sterling は、レシピ オブジェクトを使って食材をシリアル化するのではなく、インデックスのキーをシリアル化してから食材を個別に保存します。また、そのレシピを含む循環参照を認識して、オブジェクト グラフの再帰処理を停止します。食材にレシピが含まれていることにより、食材を直接クエリして、その食材と対応するレシピを読み込むことができるようになります。

食材を変更または追加するときは、保存操作によって関連テーブルも自動保存されます。外部キーは、読み込まれるとき、常にディスクから取り出され、最新のバージョンと必ず同期されます。

食品の検索: インデックスを使ったクエリ

Sterling は、メモリ内のキーとインデックスを使用して、クエリとフィルターの使用を支援します。フィルター処理とクエリの例は、食品検索の例に示しています。テキスト ボックスをビューモデルにバインドし、ユーザーが入力するにつれて入力テキストを更新します。ユーザーは、入力中に、結果 (入力中のテキストを名前に含む食品) を即座に確認できます。これにより、ユーザーが検索を絞り込むことや、既存の食品を選択したり新しい食品を入力したりすることが簡単になります。"pe" という文字列を含む食品が選択されている検索結果ページを図 9 に示します。

Food Search for Items that Include the Letters “pe”

図 9 "pe" という文字列を含む食品を検索している [Food Search] (食品の検索)

ユーザーが入力するたびに、ビューモデルのプロパティ セッターが検索テキストを更新します。このセッターは、食品リストのプロパティ変更イベントを発生します。食品リストは、食品データベースに対して新しいクエリを実行して、LINQ to Objects を使用して結果を返します。インデックスを使用してデータのフィルター処理や並べ替えを行うクエリを図 10 に示します。クエリにアクセスするため、クラス型、インデックス型、およびキー型を指定してデータベースを呼び出し、データベースにインデックスの名前を渡します。図 10 では、新しい食品モデルを、インデックスから返されるキーとインデックス値に基づいて作成していることに注目してください。

図 10 食品クエリ

public IEnumerable<FoodModel> Food
{
  get
  {
    if (string.IsNullOrEmpty(_foodText))
    {
      return Enumerable.Empty<FoodModel>();
    }
    var foodTextLower = _foodText.ToLower();
    return from f in App.Database.Query<FoodModel, 
      string, int>(RecipeDatabase.IDX_FOOD_NAME)
      where f.Index.ToLower().Contains(foodTextLower)
      orderby f.Index
      select new FoodModel { Id = f.Key, FoodName = f.Index };
  }
}

実際のトゥームストーン

トゥームストーンを機能させるには、アプリケーションの現在状態を保存し、ユーザーがアプリケーションに戻るときに保存した状態を復元しなければなりません。アプリケーションがトゥームストーン状態から戻った後にユーザーがそのアプリケーションに戻る場合もあるため、トゥームストーンの要件は複雑になる可能性があります。そのため、シームレスなエクスペリエンスを実現するには、すべてのページが各ページの値を保持できる必要があります。

MVVM パターンは、ビュー中心の XAML と分離コードからトゥームストーンを行う役割があります。これは、ビューの状態が、データ バインドを通じてビューモデルと同期されるためです。そのため、各ビューモデルが独自の状態の保存と復元の役割を担うことができます。バインドは、それに応じてビューを更新します。トゥームストーンの処理を支援するため、ここでは TombstoneModel というクラスを作成しました。

TombstoneModel には、(保存対象のビューモデルのインターフェイスになる) 型に基づいてキーが付けられ、キーとオブジェクトのディクショナリを保持します。これにより、ビューモデル状態を保存するために必要に応じて型またはクラスを保存する際に、究極の柔軟さがもたらされます。

プロパティがどのように定義されるかにかかわらず (汎用オブジェクトでも、インターフェイスでも、抽象基本クラスでも)、シリアル化ではオブジェクトを実装済みの型として書き出すため、Sterling は TombstoneModel をサポートします。これは、組み込みのシリアライザーでは不可能です。共通インターフェイスを提供するため、軽量の MVVM フレームワークは ITombstoneFriendly インターフェイスを提供します。このインターフェイスは、バインドされたビューに移動するときに呼び出されるアクティブ化メソッドと非アクティブ化メソッドを定義します (たとえば、トゥームストーンは、"移動元からの" ナビゲーションをトリガーします)。

その後のトゥームストーンの実行は、単なるモデルの作成、型の設定、保存する必要のある値の設定になります。トゥームストーン状態から戻る際は、ビューモデルでのモデルの読み込み、値の読み取り、および状態の復元が必要です。図 11 は、渡されたタイトルと、ユーザーが入力したテキストを保存する必要がある、テキスト エディター ビューモデルでこの手順を例示しています。

図 11 トゥームストーン化

/// <summary>
///     Tombstone
/// </summary>
public void Deactivate()
{
  var tombstone = new TombstoneModel 
    {SyncType = typeof (ITextEditorViewModel)};
  tombstone.State.Add(ExtractPropertyName(()=>Title), Title);
  tombstone.State.Add(ExtractPropertyName(() =>Text), Text);
  App.Database.Save(tombstone);
}

/// <summary>
///     Returned from tombstone
/// </summary>
public void Activate()
{
  var tombstone = App.Database.Load<TombstoneModel>
    (typeof(ITextEditorViewModel));
  if (tombstone == null) return;
  Title = tombstone.TryGet(ExtractPropertyName(() => 
    Title), string.Empty);
  Text = tombstone.TryGet(ExtractPropertyName(() => 
    Text), string.Empty);
}

ビューが (トゥームストーンではなく) 通常の方法で閉じられるときは、レコードを簡単に削除できます。アプリケーションの再起動時に状態が保存されないようにするため、アプリケーションの終了時には、すべてのレコードを削除するためにテーブルを切り詰めます。

軽量かつ柔軟

ここまで説明したように、Sterling は、開発者が処理しなければならない複雑なシリアル化手法の負担を取り除きます。エンティティの保存は、クラス型と、一意のキーを返すラムダ式を定義する程度の簡単な処理です。軽量 (現時点では 100 KB 未満の DLL) かつ柔軟 (トリガーのためのフック、暗号化、圧縮、およびカスタム シリアル化) であることから、Sterling は、ローカルの埋め込み型のデータベース、キャッシュ、およびトゥームストーンに関連するほぼすべてのニーズを満たすことができます。ここで紹介したレシピ アプリケーションでは、Sterling が Windows Phone 7 と統合され、こうしたニーズを簡単かつ効果的に満たすようすを示しました。

Jeremy Likness は、アトランタに拠点を置く Wintellect LLC のシニア コンサルタント兼プロジェクト マネージャーです。彼は Microsoft Silverlight MVP で、Silverlight 開発者としてマイクロソフト認定テクノロジ スペシャリストの称号も得ています。Likness は、会議やユーザー グループで、Silverlight の基幹業務アプリケーションに関連するトピックを頻繁に扱っています。また、Silverlight についてのブログ (csharperimage.jeremylikness.com、英語) を定期的に更新しています。

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