第10回 検索ボックスを装備する:連載:Windowsストア・アプリ開発入門(2/5 ページ)
Windowsストアアプリの利便性を高めるために、検索ボックスを実装してみよう。
検索機能を作る
それでは、検索ボックスを利用して検索機能をアプリに組み込んでみよう。いつものように、どんなものを作ればよいかというスペックを決めてから、実装へと進んでいく。
検索機能のスペックを考える
検索機能は、エンドユーザーに対して何を/どのように検索させて、どんなふうに見せるか、検討すべきことが多岐にわたる。
今回の検索対象は、現在ダウンロードしてあるRSSフィードの記事タイトルとしよう。他に、過去にダウンロードしたRSSフィードを蓄積しておいて検索対象とすることや、Bingなどの検索エンジンを利用することなども考えられる。
検索の方法は、大文字小文字無視で部分一致のAND検索としよう。エンドユーザーが空白かカンマで区切って入力した文字列は、全てがマッチした場合だけ結果に含める(=AND検索)。入力した文字列が記事タイトルのどこかに含まれていさえすればマッチしているとする(=部分一致)。文字列のマッチングを見るときには、半角英字の大文字小文字は区別しない(=大文字小文字無視)。なお、空白かカンマは文字列の区切りとするので、それらを検索文字列にはできない。
検索結果の見せ方は、記事一覧画面と同様にすればよいだろう。現在作っているアプリでは何百件も結果が出てくることはないので(全て表示しても100件未満)、分類や並べ替えの必要はないと判断する。
検索ボックスのUI
検索ボックスには、3種類の候補(検索履歴/結果候補/クエリ候補)を出す(次の画像)*4。
ただし、結果候補とクエリ候補は入力中の文字列が2文字以上のときだけ出すことにしよう。1文字では候補が多くなりすぎそうだからである。
結果候補に付けるアイコンは、全て上の画像に見える緑色のものとする。このアイコンは40×40ピクセルのものを用意しなければならない。他の方法としては、記事が入っていたRSSフィードごとにデザインを変えるとか、それぞれのWebページから画像を取得してきて使うとかいった方法も考えられる。
検索ボックスの主な動作は、次のようにする。
- 検索ボックスにフォーカスがないときのキー入力: 検索ボックスにフォーカスを移し、キー入力を検索文字列の入力にする*5
- 検索文字列の入力中に[Enter]キー、または右側のボタンをタップ: 検索結果画面へ遷移して検索を実行
- 検索履歴/クエリ候補を選択: 検索結果画面へ遷移して検索を実行
- 結果候補を選択: 記事表示画面へ遷移してその記事を表示
この検索ボックスは全ての画面に配置する*6。
*4 本稿執筆時点では、3種類の候補のうち複数種類を表示した場合に、SearchBoxコントロールに不具合があるようで、検索文字列の編集(キー入力と[Backspace]による削除の繰り返し)を長時間行っているとWin32 APIでの例外がしばしば発生する。この例外をトラップしてもアプリは終了させられてしまう。当面の間は、3種類の候補のうちどれか1つだけに限定して利用するのが賢明だろう。なお、デフォルトで表示される検索履歴を止めるには、SearchBoxコントロールのSearchHistoryEnabledプロパティをfalseにする。
*5 SearchBoxコントロールのFocusOnKeyboardInputプロパティがtrueになっている(=デフォルト)と、そのような動作になる。ただし、本稿執筆時点ではSearchBoxコントロールに不具合があるようで、他の画面へ遷移した後で戻ってきたときなど、しばしばこの機能が効かなくなることがある。
*6 別途公開しているサンプルコードでは、検索結果画面の実装を変えている。そちらの検索結果画面では、検索ボックスに候補を出さずに検索結果を直接変更するようにしているので、参考にしてほしい。
検索結果画面
検索した結果は、次の画像のような検索結果画面に表示しよう。
記事一覧画面と同様に、それぞれのタイルをタップすることで記事表示画面に遷移する。
なお、続けて検索しやすいように、実行した検索の検索文字列を検索ボックスにセットしておく。こうしておくことで、検索文字列を追加して絞り込むときの手間が省ける。
検索ロジックを実装する
検索を実行するロジック部分から実装していこう。検索文字列を受け取って検索結果を返せばよいのだが、どのような形で結果を返せばよいだろう? ロジックの都合からは、RSSフィードの1件ごとの記事データのコレクション(=IEnumerable<FeedItem>)を返すのが楽だ。検索結果画面の都合からは、記事一覧画面と同様にFeedクラスのオブジェクトを返してもらえると楽だ。今回は、小規模なアプリであることを考慮して、画面の都合を優先させてしまおう(大規模なアプリでは、最初に手間が掛かるものの、ロジックの都合を優先させた方がよい結果になる)。
次に、検索ロジックをどこに置くかだ。ダウンロードしたRSSフィードの全データはFeedsDataクラス(プロジェクトのDataModelフォルダー内)のオブジェクトが持っている。そこで、このFeedsDataクラスが検索ロジックを置く第一候補となる。あるいは、「検索は『検索の専門家』(「Searcherクラス」といった名前で実装することになるだろう)が行うべきだ」という発想に立てば、別のクラスを作ってそこに検索ロジックを配置することになるだろう。今回は、FeedsDataクラスに検索ロジックを置くことにしよう。
検索を実行するメソッドをSearchAsyncと名付けると、SearchAsyncメソッドは引数としてエンドユーザーが入力した検索文字列を受け取り、検索結果を格納したFeedオブジェクトを返してくれるものであればよい。それは、FeedsDataクラスの中に次のコードのようにして実装できる。
public async Task<Feed> SearchAsync(string queryText)
{
// フィードの読み込み完了を待機する(【第6回】参照)
while (!IsLoaded)
await Task.Delay(50);
// 小文字を大文字に変換し、単語ごとに区切る
string[] queries
= (queryText??string.Empty) // nullのときは空文字にする
.ToUpperInvariant() // 半角英字を大文字に変換
.Split(" , ".ToCharArray(), // 空白とカンマを区切りとして分割
StringSplitOptions.RemoveEmptyEntries);
// タイトルに使う文字列を作り、返り値となるFeedオブジェクトを生成する
var title = string.Join(" ", queries); // 単語を半角スペースで連結
var result = new Feed(title);
// クエリ文字列がないときは、0件を返す
if (queries.Length == 0)
return result;
// 全てのFeedからFeedItem(=記事データ)を取り出して、ワーク変数itemsに格納する
IEnumerable<FeedItem> items = this.Feeds.SelectMany(f => f.Items);
// ワーク変数itemsをクエリ文字列の1つで絞り込み、
// その結果をまたワーク変数itemsに格納する。
// それをクエリ文字列があるだけ繰り返せば、AND検索になる
foreach (string q in queries)
items = items.Where(item => item.Title.ToUpperInvariant().Contains(q));
// 検索結果から重複を排除し、PubDateの逆順にソートする
items = items.Distinct().OrderByDescending(item => item.PubDate);
// 注:Distinctメソッドを使うためには、FeedItemクラスにIEquatable<T>の実装が必要
// 検索結果を、返り値であるFeedオブジェクトのItemsプロパティに格納する
foreach (var item in items)
result.Items.Add(item);
return result;
}
このメソッドは、プロジェクトのDataModelフォルダーにある「FeedsData.cs」ファイルのFeedsDataクラスに記述する。
コメント中に出てくるIEquatable<T>インターフェースの実装は、すぐ次に掲載する。
上のSearchAsyncメソッドの中でDistinct拡張メソッド(System.Linq名前空間のEnumerableクラスに実装されている)を使っている。この拡張メソッドを使うには、その適用対象となるFeedItemクラスがIEquatable<T>インターフェース(System名前空間)を実装していなければならない。次のコードのようにFeedItemクラスに実装を追加する。
[System.Runtime.Serialization.DataContract]
public class FeedItem : IEquatable<FeedItem>
{
……省略……
// 等値比較を可能にするため、IEquatable<FeedItem>インターフェースを実装
public bool Equals(FeedItem other)
{
if (Object.ReferenceEquals(other, null))
return false;
if (Object.ReferenceEquals(this, other))
return true;
return string.Equals(this.Title, other.Title)
&& string.Equals(this.Link, other.Link)
&& string.Equals(this.PubDate, other.PubDate);
}
public override int GetHashCode()
{
int hashTitle = this.Title == null ? 0 : this.Title.GetHashCode();
int hashLink = this.Link == null ? 0 : this.Link.GetHashCode();
int hashPubDate = this.PubDate == null ? 0 : this.PubDate.GetHashCode();
return hashTitle ^ hashLink ^ hashPubDate;
}
}
太字の部分を、プロジェクトのDataModelフォルダーにある「FeedsData.cs」ファイルのFeedItemクラスに追加する。
以上で検索ロジックの実装は完了だ。それなりに複雑な検索処理なのだが、LINQを使うことで簡潔なコードになっている。LINQに慣れていないと戸惑われるかもしれないが、ここは1つMSDNのドキュメントに当たりながらじっくりと読み解いてほしい。
Copyright© Digital Advantage Corp. All Rights Reserved.