Windows Phone

舞台裏で: Windows Phone のフィード リーダー アプリ

Matt Stroshane

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

私はフィードの愛好者です。特に、RSS フィードや Atom フィードの魔法のような機能、何よりニュースが手元に届くしくみが好きです。しかし、大量の情報に便利にアクセスできることから、情報を意味のある方法で利用することが難しくなります。このような事情から、マイクロソフトの 3 人のインターンが Windows Phone のフィード リーダー アプリを開発中だと聞き、わくわくしながらその問題へのアプローチ方法を見てみました。

Francisco Aguilera、Suman Malani、および Ayomikun (George) Okeowo の 3 人は、インターンシップの一環として、Windows Phone SDK 7.1 の新機能を利用する Windows Phone アプリを 12 週間で開発しました。3 人は Windows Phone の開発が初めてだったため、マイクロソフトが提供しているプラットフォーム、ツール、およびドキュメントの被験者に適任でした。

いくつかの選択肢を検討してから開発することに決めたのは、ローカル データベース、Live タイル、およびバックグラウンド エージェントを備えたフィード リーダー アプリです。ただし、実際にはもっと多くの機能が備わっています。今回は、3 人がこれらの機能を使用した方法を説明します。ぜひ、Windows Phone SDK 7.1 をインストールし、コードをダウンロードして、実行してみてください。では、始めましょう。

アプリを使用する

アプリの中心となるハブは、MainPage.xaml というメイン ページです (図 1 参照)。メイン ページは、[what's new]、[featured]、[all]、および [settings] という 4 つのパノラマ パネルから構成されます。[what's new] パネルには、フィードの最新更新情報を表示します。[featured] パネルには、閲覧履歴に基づいてユーザーの好みに合うと思われる記事を 6 つ、[all] パネルには、すべてのカテゴリとフィードを一覧表示します。Wi-Fi だけを使用して記事をダウンロードするには、[settings] パネルの設定を使用します。

The Main Page of the App After Creating a Windows Phone News Category図 1 "Windows Phone News" カテゴリ作成後のアプリのメイン ページ

[what's new] パネルと [featured] パネルからは、記事に直接移動できます。[all] パネルには、カテゴリとフィードの一覧を表示します。[all] パネルからは、フィードごとやカテゴリごとにまとめられた記事の集まりに移動できます。また、[all] パネルのアプリ バーを使用すると、新しいフィードやカテゴリを追加できます。図 2 に、メイン ページと、アプリに含まれる他の 8 ページの関係を示します。

The Page Navigation Map, with Auxiliary Pages in Gray図 2 ページ ナビゲーション マップ (灰色は補助的なページ)

ピボット コントロールと同様、カテゴリ ページ、フィード ページ、および記事ページでは水平に移動できます。これらのいずれかのページを表示しているときは、アプリ バーに矢印を表示します (図 3)。この矢印を使用すると、前後のカテゴリ、フィード、または記事に関するデータをデータベースから取得して表示できます。たとえば、カテゴリ ページの "Business" カテゴリを表示しているときに [next] という矢印をタップすると、カテゴリ ページの "Entertainment" カテゴリが表示されます。

The Category, Feed and Article Pages with Their Application Bars Expanded図 3 アプリ バーを展開した状態のカテゴリ ページ、フィード ページ、および記事ページ

ただし、矢印ボタンをタップしても、別のカテゴリ ページに移動するわけではなく、同じページに別のデータ ソースがバインドされます。携帯電話の "戻る" ボタンを押すと、特別なナビゲーション コードを使用しなくても [all] パネルに戻ります。

記事ページからは、共有ページに移動して、メッセージ、電子メール、またはソーシャル ネットワーク経由でリンクを送信できます。アプリ バーには、記事を Internet Explorer で閲覧する機能、"お気に入りに追加" する機能、およびデータベースから削除する機能も備わっています。

ソリューション内部の構成

ソリューションを Visual Studio で開くと、これが次の 3 つのプロジェクトに分かれた C# アプリであることがわかります。

  1. FeedCast: ユーザーに表示する部分、つまりフォアグラウンド アプリ (ビューとビューモデルのコード)
  2. FeedCastAgent: バックグラウンド エージェントのコード (定期スケジュール タスク)
  3. FeedCastLibrary: 共有ネットワークとデータのコード

インターンのチームが使用したのは、Silverlight for Windows Phone Toolkit (2011 年 11 月版) と Microsoft Silverlight 4 SDK です。ツールキットのコントロール (Microsoft.Phone.Controls.Toolkit.dll) は、アプリのほとんどのページで使用しています。たとえば、HubTile コントロールを、メイン ページの [featured] パネルに記事を表示するために使用しています。ネットワーク処理をサポートする目的で、Silverlight 4 SDK の System.ServiceModel.Syndication.dll を使用しました。このアセンブリは Windows Phone SDK に含まれておらず、携帯電話アプリ用に最適化されてもいませんが、チーム メンバーはこのアセンブリの機能がアプリのニーズを満たすことに気付きました。

フォアグラウンド アプリ プロジェクトの FeedCast は、ソリューションに含まれる 3 つのプロジェクトのうちで最大のプロジェクトです。既に述べたように、これはアプリの中でユーザーに表示される部分です。このプロジェクトは、次の 9 フォルダーにまとめられています。

  1. Converters: データと UI のギャップを埋める値コンバーター
  2. Icons: アプリ バーに使用されるアイコン
  3. Images: 記事に画像が設定されていない場合に HubTile で使用される画像
  4. Libraries: Toolkit アセンブリと Syndication アセンブリ
  5. Models: バックグラウンド エージェントでは使用されない、データ関連コード
  6. Resources: 英語またはスペイン語のローカライズ リソース ファイル
  7. Themes: HeaderedListBox コントロールのカスタマイズ
  8. ViewModels: ビューモデルとその他のヘルパー クラス
  9. Views: フォアグラウンド アプリのページごとのコード

このアプリは、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) パターンに従っています。Views フォルダーのコードは、主に UI に重点を置いています。各ページに関連付けられるロジックとデータは、ViewModels フォルダーのコードで定義されています。Models フォルダーにはデータ関連コードを含められていますが、データ オブジェクトは FeedCastLibrary プロジェクトで定義されています。このプロジェクトの "モデル" コードは、フォアグラウンド アプリとバックグラウンド エージェントで再利用されます。MVVM の詳細については、wpdev.ms/mvvmpnp (英語) を参照してください。

FeedCastLibrary プロジェクトには、フォアグラウンド アプリとバックグラウンド エージェントで使用されるデータとネットワークのコードが含められています。このプロジェクトには、Dataと Networking という 2 つのフォルダーがあります。Data フォルダーでは、LocalDatabaseDataContext.cs、Article.cs、Category.cs、および Feed.cs という 4 ファイルに含まれる部分クラスで、FeedCast の "モデル" が記述されています。DataUtils.cs ファイルには、データベースの一般的な操作を実行するコードが含まれています。分離ストレージの設定を使用するためのヘルパー クラスは、Settings.cs ファイルに含まれています。FeedCastLibrary プロジェクトの Networking フォルダーには、Web からコンテンツをダウンロードして解析するために使用するコードが含まれており、中でも WebTools.cs ファイルの Download メソッドは特に重要です。

FeedCastAgent プロジェクトのクラスは ScheduledAgent.cs の 1 つだけで、これは バックグラウンド エージェントのコードです。このクラスの OnInvoke メソッドはエージェントの実行時に呼び出され、SendToDatabase メソッドは、ダウンロードの完了時に呼び出されます。ダウンロード処理の詳細については、後で説明します。

ローカル データベース

生産性を最大限に高めるために、チーム メンバーはそれぞれアプリの異なる分野を担当しました。Aguilera は、フォアグラウンド アプリの UI、ビュー、およびビューモデルを担当しました。Okeowo は、ネットワークと、フィードからのデータ取得に取り組み、Malani は、データベース アーキテクチャとデータベース操作に取り組みました。

Windows Phone では、データをローカル データベースに格納できます。ローカル データベースを使用する理由は、これが分離ストレージ (他のアプリとは分離された、アプリのデバイス上バケット) 内に存在するデータベース ファイルだからです。基本的には、データベース テーブルを Plain Old CLR Object (POCO) として表し、データベースの列を POCO オブジェクトのプロパティで表します。このようにすると、POCO のクラスの各オブジェクトを、対応するテーブルの行として格納できます。データベースを表すには、データ コンテキストと呼ばれる、System.Data.Linq.DataContext を継承する特別なオブジェクトを作成します。

ローカル データベースの魔法のような要素は LINQ to SQL ランタイムであり、データの執事と言えます。データ コンテキストの CreateDatabase メソッドを呼び出すと、LINQ to SQL によって分離ストレージに .sdf ファイルが作成されます。LINQ クエリを作成して必要なデータを指定することで、LINQ to SQL によって厳密に型指定されたオブジェクトが返され、このオブジェクトを UI にバインドできます。LINQ to SQL を使用すると、開発者はコードに集中できる一方で、あらゆる低レベルなデータベース操作が LINQ to SQL によって処理されます。ローカル データベースの使用の詳細については、msdn.microsoft.com/ja-jp/library/hh202860(VS.92).aspx を参照してください。

すべてのクラスを手動で作成する代わりに、Malani は Visual Studio 2010 Ultimate を使用する別の手法を採用しました。Malani は、データベース テーブルを表示しながら作成しました。つまり、サーバー エクスプローラーの [接続の追加] ダイアログ ボックスを使用して、SQL Server CE データベースを作成し、[新しいテーブル] ダイアログ ボックスを使用してテーブルを構築します。

スキーマを設計してから SqlMetal.exe を使用してデータ コンテキストを生成します。SqlMetal.exe は、デスクトップ版 LINQ to SQL のコマンド ライン ユーティリティです。SQL Server データベースに基づくデータ コンテキスト クラスの作成を目的としており、生成されるコードは、Windows Phone のデータ コンテキストによく似ています。この手法を使用することで、テーブルを表示して確認しながら構築してデータ コンテキストを迅速に生成できます。SqlMetal.exe の詳細については、msdn.microsoft.com/ja-jp/library/bb386987.aspx を参照してください。

Malani が構築したデータベースを図 4 に示します。主なテーブルは、Category、Feed、および Article の 3 つです。また、テーブル間をリンクする Category_Feed テーブルを使用して、カテゴリとフィードの多対多リレーションシップを作成できるようにしています。各カテゴリを複数のフィードに関連付けることができ、各フィードを複数のカテゴリに関連付けることができます。アプリの "お気に入り" 機能は、削除できない特別なカテゴリの 1 つに過ぎないことに注意してください。

The Database Schema図 4 データベース スキーマ

ただし、SqlMetal.exe で生成したデータ コンテキストには、Windows Phone でサポートされないコードがまだ残っています。データ コンテキストのコード ファイルを Windows Phone プロジェクトに追加した後で、Malani はプロジェクトをコンパイルして、機能しないコードを特定しました。Malani の記憶では、1 つのコンストラクターを削除する必要がありましたが、他のコードは正常にコンパイルされました。

データ コンテキスト ファイル (LocalDatabaseDataContext.cs) を調べると、すべてのテーブルが部分クラスであることがわかります。これらのテーブルに関連付けられている他の (SqlMetal.exe で自動生成されなかった) コードは、Article.cs、Category.cs、および Feed.cs の各コード ファイルに含まれています。コードをこのように分離したので、手動で作成した拡張メソッドの定義に影響を及ぼさずに、データベース スキーマを変更できます。コードを分離しなかったとすれば、LocalDatabaseDataContext.cs を自動生成するたびに拡張メソッドを追加し直す必要があったでしょう (SqlMetal.exe によってファイル内のコードがすべて上書きされるため)。

同時実行を管理する

応答性が高く滑らかに動くエクスペリエンスの提供を目的としたほとんどの Windows Phone アプリと同様、このアプリでも同時実行スレッドを使用して処理を実行します。ユーザー入力を受け取る UI スレッドだけでなく、複数のバックグラウンド スレッドで RSS フィードのダウンロードや処理を実行する可能性があります。そのため、各スレッドがデータベースを変更する必要が生じます。

データベース自体には堅牢な同時アクセス機能が備わっていても、DataContext クラスはスレッド セーフではありません。つまり、このアプリで使用する単一のグローバルな DataContext オブジェクトは、いずれかの形式の同時実行モデルを追加しない限り、複数のスレッドで共有できません。この問題に対処するために、Malani は LINQ to SQL の同時実行 API と System.Threading 名前空間のミューテックス オブジェクトを使用しました。

DataUtils.cs ファイルでは、ミューテックスの WaitOne メソッドと ReleaseMutex メソッドを使用して、DataContext クラス間で競合が発生した場合にデータへのアクセスの同期をとります。たとえば、複数の同時実行スレッド (フォアグラウンド アプリまたはバックグラウンド エージェントのスレッド) でほぼ同時に SaveChangesToDB メソッドを呼び出す場合は、WaitOne メソッドを最初に実行するコードが処理を続行できます。他のコードによる WaitOne メソッドの呼び出しは、最初のコードが ReleaseMutex メソッドを呼び出すまで実行されません。このため、データベース操作に try/catch/finally を使用する場合は、finally ステートメントに ReleaseMutex メソッドの呼び出しを配置することが重要です。ReleaseMutex メソッドを呼び出さないと、他のコードは、それぞれが保有しているスレッドが終了するまで WaitOne メソッドの呼び出しで待機します。ユーザーにとっては、この待機が "永遠" に続く可能性があります。

単一のグローバルな DataContext オブジェクトを使用する代わりに、独自のアプリを設計して、サイズの小さい DataContext オブジェクトの作成と破棄を繰り返すこともできます。しかし、チーム メンバーは、グローバルな DataContext の手法を採用すると開発が簡単になることに気が付きました。ここで付け加えておきますが、アプリはスレッドをまたがってアクセスするときだけ保護が必要であり、プロセスをまたがってアクセスするときは保護の必要はないため、ミューテックスの代わりにロックを使用することもできます。ロックを使用すると、パフォーマンスが向上する可能性もあります。

データを利用する

Okeowo は、データをアプリにバインドする機能に取り組みました。WebTools.cs ファイルには、この機能の大半を実行するコードが含まれています。しかし、WebTools クラスの用途はフィードのダウンロードだけではありません。新しいフィードに関するページでも、Bing で新しいフィードを検索するためにこのクラスを使用しています。この機能を実現するために、IXmlFeedParser という共通インターフェイスを作成し、この解析コードを複数のクラスに抽象化しています。SynFeedParser クラスではフィードを解析し、SearchResultParser クラスでは Bing の検索結果を解析します。

ただし、後者の Bing クエリは記事を返すわけではありません (IXmlFeedParser インターフェイスでは Article オブジェクトのコレクションを返しますが、この Bing クエリは返しません)。代わりに、フィードの名前と URI が含まれたリストを返します。事情を説明すると、これはフィードの説明に必要なプロパティが Article クラスに既に存在していたことに気が付いたためです。つまり、別のクラスを作成する必要はありません。検索結果を解析する際、フィード名に ArticleTitle を使用し、フィードの URI に ArticleBaseURI を使用します。詳細については、付属のコード ダウンロードの SearchResultParser.cs を参照してください。

新しいフィードのページ用ビューモデルのコード (サンプル コードの NewFeedPageViewModel.cs) を参照すると、Bing の検索結果を使用する方法がわかります。次のコード スニペットに示すように、GetSearchString メソッドを使用して、ユーザーが NewFeedPage で入力した検索語句に基づく Bing 検索文字列の URI を作成しています。

private string GetSearchString(string query)
{
  // Format the search string.
  string search = "http://api.bing.com/rss.aspx?query=feed:" + query +
    "&source=web&web.count=" + _numOfResults.ToString() +
    "&web.filetype=feed&market=en-us";
  return search;
}

_numOfResults の値は、返される検索結果の件数を制限します。RSS を使用した Bing へのアクセスに関する詳細については、MSDN Library ライブラリのページ「RSS を使用した Bing へのアクセス」(bit.ly/kc5uYO、英語) を参照してください。

GetSearchString メソッドは、実際にデータを Bing から取得する GetResults メソッドで呼び出します (図 5 参照)。ダウンロードを開始するコードを呼び出す前に、AllDownloadsFinished イベントを "インライン" で処理するラムダ式を実行しているので、GetResults メソッドはやや時代遅れの印象を受けます。Download メソッドを呼び出すと、WebTools オブジェクトでは、GetSearchString メソッドで作成した URI に基づいて Bing にクエリします。

図 5 新しいフィードを Bing にクエリする、NewFeedPageViewModel.cs の GetResults メソッド

public void GetResults(string query, Action<int> Callback)
{
  // Clear the page ViewModel.
  Clear();
  // Get the search string and put it into a feed.
  Feed feed = new Feed { FeedBaseURI = GetSearchString(query) };
  // Lambda expression to add results to the page
  // ViewModel after the download completes.
  // _feedSearch is a WebTools object.
  _feedSearch.AllDownloadsFinished += (sender, e) =>
    {
      // See if the search returned any results.
      if (e.Downloads.Count > 0)
      {
        // Add the search results to the page ViewModel.
        foreach (Collection<Article> result in e.Downloads.Values)
        {
          if (null != result)
          {
            Deployment.Current.Dispatcher.BeginInvoke(() =>
              {
                foreach (Article a in result)
                {
                  lock (_lockObject)
                  {
                    // Add to the page ViewModel.
                    Add(a);
                  }
                }
                Callback(Count);
              });
          }
        }
      }
      else
      {  
        // If no search results were returned.
        Deployment.Current.Dispatcher.BeginInvoke(() =>
          {
            Callback(0);
          });
      }
    };
  // Initiate the download (a Bing search).
  _feedSearch.Download(feed);
}

バックグラウンド エージェントでも WebTools クラスの Download メソッドを使用しますが (図 6 参照)、使い方は少し異なります。エージェントでは、フィードを 1 つだけダウンロードするのではなく、複数のフィードのリストを Download メソッドに渡します。結果の取得に関しては、エージェントでは別の方法を採用しています。すべてのフィードに含まれる記事がダウンロードされるまで (AllDownloadsFinished イベントを使用して) 待機する代わりに、エージェントでは各フィードのダウンロードが完了するたびに (SingleDownloadFinished イベントを使用して) 記事を保存します。

図 6 バックグラウンド エージェントによるダウンロードの開始 (デバッグ コメントを省略)

protected override void OnInvoke(ScheduledTask task)
{
  // Run the periodic task.
  List<Feed> allFeeds = DataBaseTools.GetAllFeeds();
  _remainingDownloads = allFeeds.Count;
  if (_remainingDownloads > 0)
  {
    Deployment.Current.Dispatcher.BeginInvoke(() =>
      {
        WebTools downloader = new WebTools(new SynFeedParser());
        downloader.SingleDownloadFinished += SendToDatabase;
        try
        {
          downloader.Download(allFeeds);
        }
        // TODO handle errors.
        catch { }
      });
  }
}

バックグラウンド エージェントの役割は、すべてのフィードを最新状態に保つことです。そのために、Download メソッドにすべてのフィードのリストを渡しています。バックグラウンド エージェントの実行時間は非常に短く、実行時間終了直後にエージェントの処理が停止されます。このため、エージェントでフィードをダウンロードする際は、一度に 1 つのフィードに含まれる記事をデータベースに送信します。このようにすることで、エージェントが停止されるまでに新しい記事を保存できる可能性が大幅に上がります。

1 つのフィードの Download メソッドと複数のフィードの Download メソッドは、実際には同じコードのオーバーロードです。このダウンロード処理コードでは、フィードごとに HttpWebRequest を (非同期に) 開始します。最初の要求が返されたら、SingleDownloadFinished イベント ハンドラーを呼び出します。続いて、SingleDownloadFinishedEventArgs を使用してフィード情報と記事をイベントにパッケージ化します。図 7 に示すように、SendToDatabase メソッドは SingleDownloadFinshed メソッドに関連付けられています。SingleDownloadFinshed メソッドから結果が返されると、SendToDatabase ではイベント引数から記事を取得して、DataBaseTools という名前の DataUtils オブジェクトに渡します。

図 7 バックグラウンド エージェントによるデータベースへの記事の保存 (デバッグ コメントを省略)

private void SendToDatabase(object sender, 
  SingleDownloadFinishedEventArgs e)
{
  // Ensure download is not null!
  if (e.DownloadedArticles != null)
  {
    DataBaseTools.AddArticles(e.DownloadedArticles, e.ParentFeed);
    _remainingDownloads--;
  }
  // If no remaining downloads, tell scheduler the background agent is done.
  if (_remainingDownloads <= 0)
  {
    NotifyComplete();
  }
}

エージェントでは、すべてのダウンロードが所定の時間内に完了したら、NotifyComplete メソッドを呼び出して、OS にダウンロードの完了を通知します。完了を通知すると、エージェントで使用していないリソースを OS で他のバックグラウンド エージェントに割り当てることができます。

コードをもう少し詳しく説明すると、DataUtils クラスの AddArticles メソッドでは、記事をデータベースに追加する前に、その記事が新しい記事かどうか確認しています。図 8 に示すように、データ コンテキストの競合を避けるために、ここでもミューテックスを使用していることに注意してください。最後に、新しい記事であることを確認できたら、SaveChangesToDB メソッドを使用してその記事をデータベースに保存します。

図 8 DataUtils.cs ファイルにおけるデータベースへの記事の追加

public void AddArticles(ICollection<Article> newArticles, Feed feed)
{
  dbMutex.WaitOne();
  // DateTime date = SynFeedParser.latestDate;
  int downloadedArticleCount = newArticles.Count;
  int numOfNew = 0;
  // Query local database for existing articles.
  for (int i = 0; i < downloadedArticleCount; i++)
  {
    Article newArticle = newArticles.ElementAt(i);
    var d = from q in db.Article
            where q.ArticleBaseURI == newArticle.ArticleBaseURI
            select q;
    List<Article> a = d.ToList();
    // Determine if any articles are already in the database.
    bool alreadyInDB = (d.ToList().Count == 0);
    if (alreadyInDB)
    {
      newArticle.Read = false;
      newArticle.Favorite = false;
      numOfNew++;
    }
    else
    {
      // If so, remove them from the list.
      newArticles.Remove(newArticle);
      downloadedArticleCount--;
      i--;
    }               
  }
  // Try to submit and update counts.
  try
  {
    db.Article.InsertAllOnSubmit(newArticles);
    Deployment.Current.Dispatcher.BeginInvoke(() =>
      {
        feed.UnreadCount += numOfNew;
        SaveChangesToDB();
      });
    SaveChangesToDB();
  }
  // TODO handle errors.
  catch { }
  finally { dbMutex.ReleaseMutex(); }
}

フォアグラウンド アプリでは、バックグラウンド エージェントと同様の手法で、Download メソッドを利用してデータを使用します。対応するコードについては、付属のコード ダウンロードの ContentLoader.cs ファイルを参照してください。

バックグラウンド エージェントのスケジュールを設定する

バックグラウンド エージェントとは、名前のとおり、フォアグラウンド アプリのためにバックグラウンドで処理を実行するエージェントです。図 6図 7 に示すように、エージェントの処理を定義しているコードは、ScheduledAgent クラスです。このクラスは、Microsoft.Phone.Scheduler.ScheduledTaskAgent から派生しています (ScheduledTaskAgent クラスはさらに Microsoft.Phone.BackgroundAgent から派生しています)。エージェントは複雑な処理を行うので非常に注目を集めますが、スケジュールを設定したタスクがなければ実行されません。

スケジュールを設定したタスクとは、バックグラウンド エージェントを実行するタイミングと頻度を指定するために使用するオブジェクトです。このアプリで使用するスケジュールを設定したタスクは、定期タスク (Microsoft.Phone.Scheduler.PeriodicTask) です。定期タスクは、一定間隔で短時間だけ実行されるタスクです。タスクのスケジュールやクエリには、Scheduled Action Service (ScheduledActionService) を使用します。バックグラウンド エージェントの詳細については、msdn.microsoft.com/ja-jp/library/hh202942(VS.92).aspx を参照してください。

このアプリのスケジュールを設定したタスクに関するコードは、フォアグラウンド アプリ プロジェクトの BackgroundAgentTools.cs ファイルに含まれています。このコードでは、StartPeriodicAgent メソッドを定義しています。この StartPeriodicAgent メソッドは、アプリ コンストラクターの App.xaml.cs で呼び出します (図 9 参照)。

図 9 BackgroundAgentTools.cs における定期タスクのスケジュール設定 (コメントを省略)

public bool StartPeriodicAgent()
{
  periodicDownload = ScheduledActionService.Find(periodicTaskName) as PeriodicTask;
  bool wasAdded = true;
  // Agents have been disabled by the user.
  if (periodicDownload != null && !periodicDownload.IsEnabled)
  {
    // Can't add the agent. Return false!
    wasAdded = false;
  }
  // If the task already exists and background agents are enabled for the
  // application, then remove the agent and add again to update the scheduler.
  if (periodicDownload != null && periodicDownload.IsEnabled)
  {
    ScheduledActionService.Remove(periodicTaskName);
  }
  periodicDownload = new PeriodicTask(periodicTaskName);
  periodicDownload.Description =
    "Allows FeedCast to download new articles on a regular schedule.";
  // Scheduling the agent may not be allowed because maximum number
  // of agents has been reached or the phone is a 256MB device.
  try
  {
    ScheduledActionService.Add(periodicDownload);
  }
  catch (SchedulerServiceException) { }
  return wasAdded;
}

定期タスクのスケジュールを設定する前に、StartPeriodicAgent メソッドではいくつかの点を確認しています。これは、スケジュールを設定したタスクにスケジュールを設定できない可能性が常に付きまとうためです。まず、ユーザーが [設定] の [アプリケーション] パネルに表示されるバックグラウンドタスクの一覧で、スケジュールを設定したタスクを無効にしていることがあります。また、デバイスで同時に有効にできるタスク数にも制限があります。上限値はデバイスの構成によって異なりますが、下限は 6 つです。この上限数を超えてタスクのスケジュールを設定しようとした場合、メモリが 256 MB のデバイスでアプリを実行している場合、または既に同一タスクのスケジュールを設定している場合は、Add メソッドから例外がスローされます。

このアプリでは、起動するたびに StartPeriodicAgent メソッドを呼び出します。これは、バックグラウンド エージェントの有効期間が 14 日間のためです。起動するたびにエージェントを更新することで、アプリをその後数日間起動しない場合でも、エージェントを引き続き実行できます。

図 9 の periodicTaskName 変数は、既存のタスクの検出に使用し、FeedCastAgent という値を格納しています。この名前は、対応するバックグラウンド エージェントのコードを特定する名前ではないことに注意してください。ScheduledActionService を操作する際に使用できる、単なるフレンドリ名です。バックグラウンド エージェントのコードへの参照をフォアグラウンド アプリ プロジェクトに追加しているため、フォアグラウンド アプリはバックグラウンド エージェントのコードを既に認識しています。バックグラウンド エージェントのコードを Windows Phone スケジュール設定したタスク エージェントという種類のプロジェクトとして作成したので、参照を追加したときに開発ツールでコードを正しく関連付けることができました。フォアグラウンド アプリとバックグラウンド エージェントの関係は、フォアグラウンド アプリのマニフェスト (サンプル コードの WMAppManifest.xml) で次のように指定されています。

<Tasks>
  <DefaultTask Name="_default" 
    NavigationPage="Views/MainPage.xaml" />
  <ExtendedTask Name="BackgroundTask">
    <BackgroundServiceAgent Specifier="ScheduledTaskAgent" 
      Name="FeedCastAgent"
      Source="FeedCastAgent" Type="FeedCastAgent.ScheduledAgent"/>
  </ExtendedTask>
</Tasks>

タイル

Aguilera は、UI、ビュー、およびビューモデルを担当しました。また、ローカライズ機能とタイル機能にも取り組みました。タイルは Live タイルとも呼ばれ、動的コンテンツを表示したり、スタート画面からアプリにリンクする機能があります。すべてのアプリのアプリ タイルは、スタート画面に追加できます (開発者は特に処理を指定する必要がありません)。ただし、アプリのメイン ページ以外の場所にリンクする場合は、セカンダリ タイルを実装する必要があります。セカンダリ タイルを使用すると、メイン ページよりも深い階層にあるページにユーザーを誘導できます。リンク先のページは、セカンダリ タイルが表す内容に合わせてカスタマイズできます。

FeedCast では、ユーザーはフィードやカテゴリ (セカンダリ タイル) をスタート画面に追加できます。1 回タップするだけで、そのフィードやカテゴリに関連する最新記事をすぐに閲覧できます。この機能を実現するには、まずユーザーがフィードやカテゴリをスタート画面に追加できるようにする必要があります。Aguilera は、Silverlight for Windows Phone Toolkit の ContextMenu コントロールを使用して、この作業を簡略化しました。メイン ページの [all] パネルでフィードやカテゴリをタップしたまま押さえると、コンテキスト メニューが表示されます。このメニューで、フィードやカテゴリの削除とスタート画面への追加を選択できます。図 10 は、この一連の操作の開始から完了までをユーザーの視点から示しています。


図 10 "Windows Phone News" カテゴリのスタート画面への追加とカテゴリ ページの起動

図 11 は、コンテキスト メニューを作成する XAML を示しています。2 つ目の MenuItem では、"pin to start" というテキストを表示します (表示言語が英語の場合)。ユーザーがこのメニュー項目をタップしたら、クリック イベントで OnCategoryPinned メソッドを呼び出し、スタート画面への追加処理を開始します。このアプリはローカライズされているので、コンテキスト メニューのテキストはリソース ファイルから取得します。Header 属性の値を LocalizedResources.ContextMenuPinToStartText にバインドしているのはこのためです。

図 11 カテゴリを削除するかスタート画面に追加するコンテキスト メニュー

<toolkit:ContextMenuService.ContextMenu>
  <toolkit:ContextMenu>
    <toolkit:MenuItem Tag="{Binding}"
      Header="{Binding LocalizedResources.ContextMenuRemoveText,
               Source={StaticResource LocalizedStrings}}"
      IsEnabled="{Binding IsRemovable}"
      Click="OnCategoryRemoved"/>
    <toolkit:MenuItem Tag="{Binding}"
      Header="{Binding LocalizedResources.ContextMenuPinToStartText,
               Source={StaticResource LocalizedStrings}}"
      IsEnabled="{Binding IsPinned, 
      Converter={StaticResource IsPinnable}}"
      Click="OnCategoryPinned"/>
  </toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>

このアプリのリソース ファイルは 2 つだけです。1 つはスペイン語、もう 1 つは英語 (既定の言語) です。しかし、ローカライズに対応しているため、他の言語を追加するのも実に簡単です。図 12 に、既定のリソース ファイル (AppResources.resx) を示します。詳細については、wpdev.ms/globalized (英語) を参照してください。

The Default Resource File, AppResources.resx, Supplies the UI Text for All Languages Except Spanish図 12 スペイン語以外の全言語向け UI テキストを指定する、既定のリソース ファイル (AppResources.resx)

チームでは当初、スタート画面に追加する必要があるカテゴリやフィードを正確に特定する方法がよくわかりませんでした。その後、Aguilera が XAML の Tag 属性に気が付きました (図 11 参照)。チーム メンバーは、この属性をビューモデルのカテゴリ オブジェクトまたはフィード オブジェクトにバインドして、後からプログラムで個別のオブジェクトを取得できることを突き止めました。メイン ページでは、カテゴリのリストを MainPageAllCategoriesViewModel オブジェクトにバインドしています。OnCategoryPinned メソッドを呼び出したら、次のようにこのメソッドで GetTagAs メソッドを使用して、リスト内の特定の項目に対応する Category オブジェクト (Tag 属性にバインド) を取得します。

private void OnCategoryPinned(object sender, EventArgs e)
{
  Category tappedCategory = GetTagAs<Category>(sender);
  if (null != tappedCategory)
  {
    AddTile.AddLiveTile(tappedCategory);
  }
}

GetTagAs メソッドは、コンテナーの Tag 属性にバインドしているオブジェクトを取得するジェネリック メソッドです。このメソッドは効果的ですが、MainPage.xaml.cs で使用している箇所のほとんどで、実際に必要なわけではありません。リスト内の項目は既にオブジェクトにバインドされているので、Tag 属性にバインドと、処理が多少重複します。Tag 属性を使用する代わりに、sender オブジェクトの DataContext を使用することもできます。たとえば、図 13 は、お勧めする DataContext による手法を使用した場合の OnCategoryPinned を示しています。

図 13 GetTagAs の代わりに DataContext を使用した場合の例

private void OnCategoryPinned(object sender, EventArgs e)
{
  Category tappedCategory = null;
  if (null != sender)
  {
    FrameworkElement element = sender as FrameworkElement;
    if (null != element)
    {
      tappedCategory = element.DataContext as Category;
      if (null != tappedCategory)
      {
        AddTile.AddLiveTile(tappedCategory);
      }
    }
  }
}

この DataContext による手法は、MainPage.xaml.cs では、OnHubTileTapped メソッドを除くすべての使用例で役に立ちます。OnHubTileTapped メソッドは、メイン ページの [featured] パネルに表示されている特集記事をユーザーがタップすると呼び出されます。問題は、sender が Article クラスにバインドされていないことです。実際には MainPageFeaturedViewModel にバインドされています。このビューモデルには 6 つの記事が含まれているので、DataContext ではユーザーがタップした記事を明確に特定できません。この場合、Tag プロパティを使用すると、正しい Article クラスへのバインドが非常に容易になります。

フィードとカテゴリをスタート画面に追加できることから、AddLiveTile メソッドには 2 つのオーバーロードが存在します。オブジェクトとセカンダリ タイルが大きく異なるので、チームは機能を 1 つのジェネリック メソッドにまとめないことにしました。図 14 に、Category バージョンの AddLiveTile メソッドを示します。

図 14 カテゴリ オブジェクトのスタートへの追加

public static void AddLiveTile(Category cat)
{
  // Does Tile already exist? If so, don't try to create it again.
  ShellTile tileToFind = ShellTile.ActiveTiles.FirstOrDefault(x => 
    x.NavigationUri.ToString().Contains("/Category/" + 
    cat.CategoryID.ToString()));
  // Create the Tile if doesn't already exist.
  if (tileToFind == null)
  {
    // Create an image for the category if there isn't one.
    if (cat.ImageURL == null || cat.ImageURL == String.Empty)
    {
      cat.ImageURL = ImageGrabber.GetDefaultImage();
    }
    // Create the Tile object and set some initial properties for the Tile.
    StandardTileData newTileData = new StandardTileData
    {
      BackgroundImage = new Uri(cat.ImageURL, 
      UriKind.RelativeOrAbsolute),
      Title = cat.CategoryTitle,
      Count = 0,
      BackTitle = cat.CategoryTitle,
      BackContent = "Read the latest in " + cat.CategoryTitle + "!",
    };
    // Create the Tile and pin it to Start.
    // This will cause a navigation to Start and a deactivation of the application.
    ShellTile.Create(
      new Uri("/Category/" + cat.CategoryID, UriKind.Relative), 
      newTileData);
    cat.IsPinned = true;
    App.DataBaseUtility.SaveChangesToDB();
  }
}

カテゴリのタイルを追加する前に、AddLiveTile メソッドでは ShellTile クラスを使用してすべてのアクティブなタイルからそのカテゴリへのナビゲーション URI を検索し、カテゴリを既にスタート画面に追加済みかどうか特定します。まだ追加していない場合は、処理を続行して、新しいタイルに関連付ける画像 URL を取得します。新しいタイルを作成する場合、背景画像には必ずローカル リソースの画像を使用します。この場合は、ImageGrabber クラスを使用して、ランダムに割り当てたローカル画像ファイルを取得します。ただし、タイルの作成後に、リモート URL で背景画像を更新することもできます。しかし、このアプリにはタイルの画像の更新機能は備わっていません。

新しいタイルの作成時に指定する必要があるすべての情報は、StandardTileData クラスに格納しています。このクラスを使用して、テキスト、数値、および背景画像をタイルに表示します。Create メソッドでタイルを作成する際に、StandardTileData をパラメーターとして渡します。メソッドに渡すもう 1 つの重要なパラメーターは、タイルのナビゲーション URI です。これは、アプリ内の意味のある場所に移動するための URI です。

このアプリでは、タイルのナビゲーション URI はアプリ内だけにリンクしています。他の場所に移動するには、基本的な UriMapper クラスを使用してユーザーを適切なページにルーティングします。App.xaml の navigation 要素では、アプリにおけるすべての URI マッピングを指定しています。各 UriMapping 要素の Uri 属性で指定している値が、受信 URI です。MappedUri 属性で指定している値は、ユーザーの移動先です。特定のカテゴリ、フィード、または記事のコンテキストを保持するために、次のように、中かっこで囲まれた ID 値を受信 URI からマッピング先 URI に渡しています。

<navigation:UriMapping Uri="/Category/{id}" MappedUri=
  "/Views/CategoryPage.xaml?id={id}"/>

URI マッパーを使用する目的は他にもさまざまなものがありますが (たとえば、検索の機能拡張など)、セカンダリ タイルを使用するうえでは必要ありません。このアプリでは、URI マッパーを使用するかどうかは開発スタイル上の判断でした。チームは、短い URI の方が簡潔で使いやすいと考えました。別の手法として、セカンダリ タイルでページ固有の URI (MappedUri の値など) を指定したとしても、同じ効果が得られます。

どの手法を採用した場合でも、セカンダリ タイルの URI を適切なページにマッピングすれば、ユーザーは記事一覧が表示されたカテゴリ ページに移動できます。任務完了です。タイルの詳細については、msdn.microsoft.com/ja-jp/library/hh202948(VS.92).aspx を参照してください。

でも待ってください、まだ続きがあります

このアプリには、今回説明しなかったさまざまな機能が備わっています。ぜひコードをご覧になって、この記事で説明した問題やその他の問題にチームが対処した方法を確認してください。たとえば、SynFeedParser.cs では、HTML タグが混入することが多いフィードのデータから不要な部分を取り除く、優れた方法を使用しています。

ただし、これはインターンが 12 週間で開発し、もう少し改良を加える前の時点のアプリであることに注意してください。プロフェッショナルの開発者であれば別のコーディング方法を選択する箇所も、いくらかあります。それでも、インターンがこのアプリでローカル データベース、バックグラウンド エージェント、およびタイルを統合するという大きな仕事を成し遂げたと考えています。今回の「舞台裏で」をお楽しみいただければさいわいです。コーディングを楽しんでください。

Matt Stroshane は、Windows Phone チーム向けに開発者ドキュメントを執筆しています。ほかにも、SQL Server、SQL Azure、Visual Studio など、MSDN ライブラリで特集する製品の記事も執筆しています。執筆活動をしていないときは、シアトルの通りで、次回のマラソン大会に備えてトレーニングしている姿を見かけます。Twitter (twitter.com/mattstroshane、英語) で彼をフォローしてください。

この記事のレビューに協力してくれた技術スタッフの Francisco AguileraThomas FennelJohn GallardoSean McKennaSuman MalaniAyomikun (George) Okeowo、および Himadri Sarkar に心より感謝いたします。