第10回 検索ボックスを装備する:連載:Windowsストア・アプリ開発入門(4/5 ページ)
Windowsストアアプリの利便性を高めるために、検索ボックスを実装してみよう。
検索コントロールを実装する
さて、これからSearchBoxコントロールを画面に配置して検索結果画面を呼び出すという実装をしたいのだが、このままだと複数の画面に同じ処理を何度も記述することになってしまう。その重複を避けて処理を1つにまとめるには、次のような選択肢が考えられる。
- ヘルパークラスを作ってそこに処理を置く
- ユーザーコントロールを使い、そこにSearchBoxコントロールと処理を置く
- SearchBoxコントロールを継承したコントロールを作り、そこに処理を置く
今回は、SearchBoxコントロールを継承してみよう。その新しく作る「検索コントロール」では、次の表に示す4つのイベントを処理する。
イベント | タイミング | 処理 |
---|---|---|
Loaded | 画面に表示されるとき | Appクラスが保持している最新のFeedsDataオブジェクトを取得する。 これは結果候補を検索するために必要 |
SuggestionsRequested | エンドユーザーがキー入力中に検索候補が必要になったとき | 結果候補とクエリ候補を算出して返す。 候補を出すためには、検索を実行する必要がある |
ResultSuggestionChosen | エンドユーザーが結果候補を選んだとき | 記事表示画面に遷移する |
QuerySubmitted | エンドユーザーが[Enter]キーを押下または検索ボタンをタップしたとき。 あるいは、エンドユーザーが検索履歴かクエリ候補を選んだとき |
検索結果画面に遷移する |
検索コントロールで処理すべきイベント |
表にも書いたが、結果候補とクエリ候補というものは実は検索を実行してその結果を表示するのである。ということは、長くても数百ミリ秒程度で検索結果が得られなければならない。今回の結果候補はメモリ上にあるせいぜい100個程度のオブジェクトを対象に検索するので、検索時間の心配は全くない(クエリ候補の実装はサンプルということで固定値を返す)。検索をクラウドで行う場合は、しっかり実験と検討を重ねて検索候補を出すべきかどうかを決定してほしい。
Loadedイベント
それでは実装していこう。プロジェクトに「Search」フォルダーを作り、新しくクラスを作り、「SearchControl」という名前にする。そうしたら、SearchBoxコントロールを継承させ、LoadedイベントでFeedsDataを取得してメンバー変数に保持しておくようにする(次のコード)。
using System;
using System.Threading.Tasks;
namespace AtmarkItReader.Search
{
public class SearchControl : Windows.UI.Xaml.Controls.SearchBox
{
private DataModel.FeedsData _feedsData;
public SearchControl()
{
this.Loaded += SearchControl_Loaded;
}
void SearchControl_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
_feedsData = App.Current.Resources["feedsDataSource"] as DataModel.FeedsData;
}
}
}
これで、検索コントロールが画面に表示されているときはいつでもFeedsDataオブジェクトが利用できるようになった。
SuggestionsRequestedイベント
このイベントハンドラーで検索候補を出すのだが、その実装の前に画像を用意しておく。結果候補には、「検索ボックスのUI」で述べたように40×40ピクセルのアイコンを付けなければならないのだ。プロジェクトのAssetsフォルダーに、「SuggestLogo.scale-100.png」というファイル名で次の画像を用意した。
SuggestionsRequestedイベントハンドラーで結果候補とクエリ候補をセットするコードは次のようになる。Asyncメソッドの呼び出しがある(SearchAsyncメソッドが該当)ので、GetDeferral〜deferral.Completeして、呼び出し元に非同期処理の完了を通知する必要がある。
public SearchControl()
{
this.Loaded += SearchControl_Loaded;
this.SuggestionsRequested += SearchControl_SuggestionsRequested;
}
async void SearchControl_SuggestionsRequested(
Windows.UI.Xaml.Controls.SearchBox sender,
Windows.UI.Xaml.Controls.SearchBoxSuggestionsRequestedEventArgs args
)
{
var queryText = args.QueryText.Trim();
if (queryText.Length < 2) // 1文字以下のときは候補を出さない
return;
var deferral = args.Request.GetDeferral();
var suggestions = args.Request.SearchSuggestionCollection;
// 結果候補
var searchResult = await _feedsData.SearchAsync(queryText); // 検索を実行する
if (searchResult.Items.Count > 0)
{
suggestions.AppendSearchSeparator("該当記事"); // 見出し行を挿入
var uri = new Uri("ms-appx:///Assets/SuggestLogo.png");
var thumbnail
= Windows.Storage.Streams.RandomAccessStreamReference.CreateFromUri(uri);
int count = 5; // 結果候補は最大5件まで表示することとした
foreach (var item in searchResult.Items)
{
// 結果候補をセット(引数は全て必須で、どれもnullにはできない)
suggestions.AppendResultSuggestion(
item.Title, item.PubDate, item.Title, thumbnail, item.Title);
if (--count == 0)
break;
}
}
// クエリ候補
suggestions.AppendSearchSeparator("お勧めの検索"); // 見出し行を挿入
suggestions.AppendQuerySuggestions(
new string[] { "Win 8.1", "ストア", }
);
deferral.Complete();
}
太字の部分を追加する。サンプルということで、クエリ候補の方は固定値を返しているが、実際には何らかの検索処理を行うことになる。
上のコードでは、検索文字列としてargs.QueryTextプロパティだけを使っている。これは、エンドユーザーが確定させた文字列である。IMEで変換中のときは、args.LinguisticDetailsプロパティ(Windows.ApplicationModel.Search名前空間のSearchQueryLinguisticDetailsクラス)から複数の変換候補を得られるので、それも利用することでよりユーザーフレンドリーにできるだろう。
今回はクエリ候補を固定値としたが、実際にはどうすべきか考察しておこう。クエリ候補は、何らかの辞書を先頭一致で検索することになるだろう(辞書としては欲をいえばシソーラスを使いたい)。けれども、自前で辞書を用意するのは手に余るし、かといってそのような日本語の無償のWebサービスも見当たらないようだ。別のアプローチとしては、検索対象(今回でいえば記事タイトル)から単語を抜き出して辞書を自動生成することも考えられる。これは単語を分かち書きする言語(英語など)では割と容易に実装できるので有効な手段になり得るが、ベタ書きの日本語では形態素解析や構文解析をしなければならず、それには辞書や膨大なトレーニングデータが必要になるという堂々巡りに陥る。過去の検索語句から辞書を作成することも考えられるが、それはデフォルトの検索履歴とほとんど同じものになってしまうだろう。結局のところ、日本語でクエリ候補を出すのは難しい。
ResultSuggestionChosenイベント
このイベントは、エンドユーザーが結果候補を選んだときに発生する。結果候補のTagプロパティには、SuggestionsRequestedイベントハンドラーで記事タイトルを格納してある。このイベントハンドラーでは、Tagプロパティ(=記事タイトル)で検索して該当記事のFeedItemオブジェクトを取得し、記事表示画面に渡す引数を作成して、記事表示画面に遷移させる(次のコード)。
public SearchControl()
{
this.Loaded += SearchControl_Loaded;
this.SuggestionsRequested += SearchControl_SuggestionsRequested;
this.ResultSuggestionChosen += SearchControl_ResultSuggestionChosen;
}
async void SearchControl_ResultSuggestionChosen(
Windows.UI.Xaml.Controls.SearchBox sender,
Windows.UI.Xaml.Controls.SearchBoxResultSuggestionChosenEventArgs args
)
{
// 該当する記事のFeedItemオブジェクトを取得する
var item = (await _feedsData.SearchAsync(args.Tag)).Items[0];
// FeedItemオブジェクトから、記事表示画面が必要とする引数を作成する(【第5回】参照)
string param = string.Format("{0}¥t{1}¥t{2}", item.Title, item.Link, item.PubDate);
Navigate(typeof(ViewPage), param); // 記事表示画面へ遷移
}
太字の部分を追加する。
QuerySubmittedイベント
このイベントが発生するのは、エンドユーザーが[Enter]キーを押したり検索ボタンをタップしたり、あるいは、検索履歴/クエリ候補を選んだときである。イベントハンドラーでは、検索結果画面に遷移させる(次のコード)。
public SearchControl()
{
this.Loaded += SearchControl_Loaded;
this.SuggestionsRequested += SearchControl_SuggestionsRequested;
this.ResultSuggestionChosen += SearchControl_ResultSuggestionChosen;
this.QuerySubmitted += SearchControl_QuerySubmitted;
}
void SearchControl_QuerySubmitted(
Windows.UI.Xaml.Controls.SearchBox sender,
Windows.UI.Xaml.Controls.SearchBoxQuerySubmittedEventArgs args
)
{
if (string.IsNullOrWhiteSpace(args.QueryText))
return;
Navigate(typeof(SearchResultPage), args.QueryText); // 検索結果画面へ遷移
}
void Navigate(Type page, string title)
{
var thisFrame
= Windows.UI.Xaml.Window.Current.Content as Windows.UI.Xaml.Controls.Frame;
if (thisFrame != null)
thisFrame.Navigate(page, title.Trim());
}
太字の部分を追加する。
これで検索コントロールは完成だ。
Copyright© Digital Advantage Corp. All Rights Reserved.