第7回 ほかのアプリにデータを送る:連載:Windowsストア・アプリ開発入門(4/4 ページ)
アプリ上のデータをほかのアプリで利用できるようにしてみよう。また、WebサービスからRSSフィードを再取得するリロード機能も追加する。
リロード機能を追加する
本アプリを使っていて気になることとして、データが更新されないことも挙げられるだろう。アプリ起動時にWebサービスからRSSフィードをダウンロードするだけなので、アプリが終了させられるまで*5、最初に読み込んだデータだけを表示し続けるのだ。この問題を解決するために、Webサービスからデータをリロードする機能を追加しよう。
*5Windowsストア・アプリでは、エンド・ユーザーがアプリを終了させるためのUIは提供されておらず、システムが管理するアプリのライフサイクルに従って終了させられる。システムのリソースがひっ迫しているような状況でない限りは、本アプリはWindowsを再起動するかエンド・ユーザーがログオフするまでメモリ上にとどまり続けるため、いつまでたっても新しいデータが表示されることがないのだ。アプリのライフサイクルについて、詳しくは「特集:Windows 8開発に向けて準備しよう〜Metroスタイル・アプリの開発者が知るべき3つのこと」の「その3: [設計の原則] 万全のリジューム機能を装備すべし!」を参照してほしい。
リロード機能のスペックを考える
いつリロードするかは、2通り考えられる。1つは、エンド・ユーザーに指示してもらう。もう1つは、適当なタイミングで自動的に行う。今回は、自動的に行う方法を考えてみよう。
自動的にリロードする場合は、そのタイミングをまず考える。エンド・ユーザーが記事の一覧を真剣に読んでいるときにリロードして画面を書き換えてしまっては、エンド・ユーザーにとっては迷惑なだけだろう。画面を表示した直後なら、画面を書き換えてもまだ許されるだろう。そこで、メイン画面が表示されたタイミングと、中断から再開されたタイミングでリロードし、データが新しくなっていたら画面を書き換えることにしよう。
今、「画面を書き換える」といったが、データ・バインドで画面に表示しているので、実際には保持しているデータを書き換えるだけで画面も変わる。
自動的にリロードする時間間隔も考慮する。あまり短いインターバルでWebサービスにアクセスするのは、Webサービスへの負荷とインターネットのトラフィックを増やすだけだ。上述したリロードのタイミングが来たときでも、一定時間が経過していないときにはリロードしないようにしよう。また、インターネットに接続していないときもリロードしないようにしよう。
以上を整理して次の表にまとめておく。
項目 | 内容 |
---|---|
リロード機能を呼び出すタイミング | ・メイン画面を表示したとき ・アプリで中断から再開されたとき |
Webアクセスする条件 | ・インターネットに接続していること(切断時はリロードしない) ・前回のアクセスから一定の時間が経過していること(今回は15分とする) |
データを書き換える条件 | Webサービスからダウンロードしたデータが既存のデータと変わっているとき |
リロード機能のスペック |
リロード機能を実装する
Webサービスにアクセスしてデータを処理する部分は、リロードの場合でもほぼ同じなので、従来のコードを改修することにしよう。改修点は、既存のデータがあるときはそれを使い、変化があったときだけデータを書き換えるようにすることだ(=スペックの3番目)。そのほかのスペックを実現するには、新たにコードを付け加えないといけない。
それでは、スペックに書いた順にコードを紹介していこう。なお、実際には、逆の順序で開発した方がやりやすい。
リロード機能を呼び出す
まず、リロード機能を呼び出す部分から。これは2カ所ある。1つ目は、メイン画面が表示されたときだ(次のコード)。
private void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
// TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
// var sampleDataGroups = await SampleDataSource.GetGroupsAsync();
// this.DefaultViewModel["Groups"] = sampleDataGroups;
// 上の2行は不要なので削除する
// 【第7回】画面表示時にデータをダウンロードし直す
// (これは一定時間内の呼び出しの場合には無視される)
(App.Current as App).ReloadStart();
}
「HubPage.xaml.cs」ファイルのnavigationHelper_LoadStateメソッドの末尾に、太字の部分を追加する。
AppクラスのReloadStartメソッドはまだ存在しないので、それを作るまではビルドできない。
なお、コメントにあるように、リロード機能の側で一定時間内の呼び出しを無視するように作るので、ここでは無条件にReloadStartメソッドを呼び出して構わないのである。
リロード機能を呼び出す2つ目のタイミングは、中断から再開されたときだ。これはAppクラスに実装する。イベント・ハンドラの追加とそのメソッドの実装を行う(次の2つのコード)。
public App()
{
this.InitializeComponent();
this.Suspending += OnSuspending;
// 【第7回】中断→再開時の処理
this.Resuming += OnResuming;
……省略……
「App.xaml.cs」ファイルのコンストラクタに、太字の部分を追加する。
Appクラスの継承元であるApplicationクラス(Windows.UI.Xaml名前空間)に、アプリが中断から再開されたときに発生するResumingイベントがあるので、それを利用する。
private void OnResuming(object sender, object e)
{
ReloadStart();
}
「App.xaml.cs」ファイルにこのメソッドを追加する。
ReloadStartメソッドはまだ存在しないので、それを作るまではビルドできない。
リロード機能
次に、リロード機能そのものを実装していく。まず、AppクラスにReloadStartメソッドを作る。これは、App.xamlファイルにスタティック・リソースとして定義してあるFeedsDataオブジェクトを取り出して、それを引数としてDataLoaderクラスのReloadStartメソッド(まだ存在していない)を呼び出すだけのものだ。AppクラスのOnLaunchedメソッドにあるLoadStartメソッドの呼び出し(前回に記述したコード)と同様な呼び出し方法である。
public void ReloadStart()
{
var feedsData = App.Current.Resources["feedsDataSource"] as DataModel.FeedsData;
Logic.DataLoader.ReloadStart(feedsData);
}
「App.xaml.cs」ファイルにこのメソッドを追加する。
DataLoaderクラスのReloadStartメソッドはまだ存在しないので、それを作るまではビルドできない。
リロード機能の本体は、Logicフォルダの「DataLoader.cs」ファイルに追加する(次のコード)。
public class DataLoader
{
static readonly string[] URLs = new []
{
"http://rss.rssad.jp/rss/itmatmarkit/fdotnet/rss.xml", // @IT Insider.NETフォーラム
"http://rss.rssad.jp/rss/itmatmarkit/news/rss.xml", // @IT News 最新記事一覧
};
// 【第7回】一定時間間隔内に呼び出された場合は無視するための、定数と変数
private const double LOAD_INTERVAL_MINUTE = 15.0; // ★1
private static DateTimeOffset _lastLoaded;
// 【第7回】リロードする
public static async void ReloadStart(DataModel.FeedsData feedsData)
{
// 一定時間間隔内に呼び出された場合は無視する
double elapsedMinutes = DateTimeOffset.Now.Subtract(_lastLoaded).TotalMinutes;
if (elapsedMinutes < LOAD_INTERVAL_MINUTE)
return;
// インターネット接続がないときは無視する
var connectionProfile
= Windows.Networking.Connectivity.NetworkInformation
.GetInternetConnectionProfile();
if (connectionProfile == null)
return;
// 実際にRSSフィードをダウンロードしてデータを更新する処理は、
// 最初にロードする処理と共通にする
await LoadAsync(feedsData);
}
……省略……
internal static async Task<DataModel.FeedsData> LoadAsync(DataModel.FeedsData feedsData)
{
feedsData.IsLoaded = false;
_lastLoaded = DateTimeOffset.Now;
……省略……
Logicフォルダの「DataLoader.cs」ファイルに、太字の部分を追加する。
実際にRSSフィードをダウンロードしてデータを更新する処理として、前回と同じLoadAsyncメソッドを呼び出す。前回のままではリロードに対応していないのだが、それは次で改修する。
静的変数_lastLoadedは、アプリ起動時に最小値(=西暦1年1月1日)に初期化され、LoadAsyncメソッドの開始時にその時刻がセットされる。
コメントの「★1」については、後述。
保持しているデータを書き換える
最後に、Webサービスからダウンロードしたデータが、以前のものと変わっているときだけ保持しているデータを書き換えるようにしよう(次の3つのコード)。
internal static class FeedsDataExtension
{
// 【第7回】IsLoadedフラグに関係なく、フィードのタイトルからFeedオブジェクトを得る
internal static Feed Find(
this System.Collections.ObjectModel.ObservableCollection<Feed> feeds,
string feedTitle)
{
foreach (var feed in feeds)
if (feed.Title == feedTitle)
return feed;
return null;
}
}
DataModelフォルダの「FeedsData.cs」ファイルに、このクラスを追加する。
using AtmarkItReader.DataModel; // 【第7回】拡張メソッドの利用に必要
……省略……
internal static AtmarkItReader.DataModel.FeedsData Add(
Windows.Web.Syndication.SyndicationFeed syndicationFeed,
DataModel.FeedsData feedsData)
{
// 【第7回】まず変数名を変更する:newFeed→targetFeed
// var newFeed = new DataModel.Feed(syndicationFeed.Title.Text);
// feedsData.Feeds.Add(newFeed);
// ↓
// 【第7回】繰り返し呼び出されたときは、既存のFeedオブジェクトを使う
DataModel.Feed targetFeed = feedsData.Feeds.Find(syndicationFeed.Title.Text);
if (targetFeed == null)
{
// まだ存在していないときは、新しく生成する(既存のコード)
targetFeed = new DataModel.Feed(syndicationFeed.Title.Text);
feedsData.Feeds.Add(targetFeed);
}
else
{
// すでに存在していたら、データに変更があるかどうかをチェックする
if (targetFeed.Items.Count > 0 && syndicationFeed.Items.Count > 0
&& targetFeed.Items[0].Title == syndicationFeed.Items[0].Title.Text)
{
// 簡易的に、最初の記事タイトルが同じならデータに変更はないと判定してみた。
// データに変更が無ければ、何もせずリターン
return feedsData; // ★2
}
// データに変更があった時は、既存のデータを消す
targetFeed.Items.Clear();
}
foreach (var syndicationItem in syndicationFeed.Items)
{
……省略(newFeed→targetFeedの変更はある)……
Logicフォルダの「FeedProcessor.cs」ファイルに、太字の部分を追加する。追記する前に、変数名newFeedをtargetFeedにリネームしておくこと。
コメントの「★2」については、後述。
なお、ここのAddメソッドという名前も、「AddOrUpdate」などに変えた方が適切であろうが、今回はそのままとした。
using AtmarkItReader.DataModel; // 【第7回】拡張メソッドの利用に必要
private static void AddEmptyFavorite(DataModel.FeedsData feedsData)
{
// 【第7回】繰り返し呼び出されたときは、新たに追加せず、既存データを消す
var currentData = feedsData.Feeds.Find("お気に入り");
if (currentData != null)
{
currentData.Items.Clear();
return;
}
feedsData.Feeds.Add(new DataModel.Feed("お気に入り"));
}
Logicフォルダの「DataLoader.cs」ファイルのAddEmptyFavoriteメソッドに、太字の部分を追加する。
この「お気に入り」データを操作しているコードは、仮のもの。ちゃんとした実装は、次回で行う予定だ。
リロード機能をテストする
以上で完成だ。実際に操作して試してみてほしい。
ただし、そのままテストしようとすると、15分以上の間隔を空けないとWebアクセスしてくれないし、1日に数回あるかないかのWebサービスのデータ更新を待つのも大変だ。テストのためには、(1)リロードを無視する期間(「★1」のコメントを付けた箇所)を例えば0.1分(=6秒)などと短くする、(2)データが変わっていないときは書き換えない(「★2」のコメント)としているところをコメント・アウトする、という一時的な変更をするとよい。そうすると、数秒待つだけでWebアクセスするし、Webサービス側でデータが更新されていなくても画面が書き換わる。テストが終わったら、以上の2カ所は元に戻しておこう。
まとめ
今回は、ほかのアプリにデータを送る方法を考え、3通りの機能を追加した。また、自動的にWebサービスからリロードする機能も実装した。
- ほかのアプリにデータを送る方法として、Windowsストア・アプリでは共有コントラクトを活用したい
- そのほか、従来からのコピー&ペーストや、ファイル名拡張子/プロトコルに対する関連付けなども利用できる
- 画面にアプリ・バーを実装した。コマンド・ボタンだけのアプリ・バーの実装は簡単だ
- 自動的にリロードする仕組みを考えて実装した。エンド・ユーザーの迷惑にならないようにリロードするタイミングを決めることが大切だ
- リロードする処理では、データを書き換えるだけで、データ・バインドによって画面も書き換わった
次回は「入力されたデータを保存する」。ここまでダミーのままだった「お気に入り」の機能を実装する予定だ。
Copyright© Digital Advantage Corp. All Rights Reserved.