カスタム データ ソースを作成する方法 (HTML)
[ この記事は、Windows ランタイム アプリを作成する Windows 8.x および Windows Phone 8.x 開発者を対象としています。Windows 10 向けの開発を行っている場合は、「最新のドキュメント」をご覧ください]
JavaScript 用 Windows ライブラリには、すぐに使用できるデータ ソース オブジェクトがいくつか用意されています。これらを使用して、ListView や FlipView にさまざまな種類のデータを設定できます。たとえば、配列や JSON データにアクセスするための WinJS.Binding.List オブジェクトや、ファイル システムに関する情報にアクセスするための StorageDataSource などがあります。
使用できるのはこれらのデータ ソースだけではありません。XML ファイルや Web サービスなど、その他の種類のデータにアクセスするカスタム データ ソースを独自に作成することができます。このトピックでは、Web サービスにアクセスするカスタム データ ソースを実装する方法を説明します。このカスタム データ ソースは、XHR を使って Bing の画像検索サービスに接続し、結果を ListView に表示します。
(Bing サービスでは各アプリに固有のアプリ ID キーが必要になるため、このコードを使用する前にキーを入手する必要があります。アプリ ID キーの入手方法についての詳しい情報は、 bing デベロッパー センターをご覧ください。)
カスタム データ ソースを作成するには、IListDataAdapter インターフェイスと IListDataSource インターフェイスを実装するオブジェクトが必要です。WinJS には、IListDataSource を実装する VirtualizedDataSource オブジェクトが用意されています。このオブジェクトを継承して、基本コンストラクターに IListDataAdapter を渡すだけで済みます。IListDataAdapter インターフェイスを実装するオブジェクトは独自に作成する必要があります。
IListDataAdapter は、データ ソースと直接やり取りして、項目を取得したり更新したりします。IListDataSource は、コントロールに接続し、IListDataAdapter を操作します。
必要条件
- WinJS のオブジェクトとコントロールの使用について理解している必要があります。詳しくは、「クイックスタート: JavaScript 用 Windows ライブラリのコントロールとスタイルの追加」をご覧ください。
- ListView または FlipView を作成してデータ ソースに接続する方法を理解している必要があります。ListView を使用する場合は、「クイックスタート: ListView の追加」をご覧ください。FlipView を使用する場合は、「クイックスタート: FlipView の追加」をご覧ください。
- Promise オブジェクトについて理解している必要があります。詳しくは、「JavaScript での非同期プログラミング」をご覧ください。
手順
ステップ 1: カスタム データ ソースのための JavaScript ファイルを作成する
- Microsoft Visual Studio を使って、プロジェクトに JavaScript ファイルを追加します。ソリューション エクスプローラーで、プロジェクトの
js
フォルダーを右クリックし、[追加]、[新しい項目] の順にクリックします。[新しい項目の追加] ダイアログが表示されます。 - [JavaScript ファイル] をクリックします。"bingImageSearchDataSource.js" という名前を付けます。 [追加] をクリックしてファイルを作成します。bingImageSearchDataSource.js という名前の空の JavaScript ファイルが作成されます。
ステップ 2: IListDataAdapter を作成する
次に、IListDataAdapter インターフェイスを実装するオブジェクトを作成します。IListDataAdapter は、データ ソースからデータを取得して IListDataSource に渡します。
IListDataAdapter インターフェイスでは、読み取り/書き込みアクセスと変更通知がサポートされています。ただし、インターフェイス全体を実装する必要はありません。itemsFromIndex メソッドと getCount メソッドのみを実装して、読み取り専用の単純な IListDataAdapter を作成します。
bingImageSearchDataSource.js (前の手順で作成した JavaScript ファイル) を開きます。
匿名関数を作成し、strict モードを有効にします。
「基本的なアプリのコーディング」で説明したように、JavaScript コードを匿名関数でラップしてカプセル化し、strict モードを使用することをお勧めします。
(function () { "use strict";
WinJS.Class.define 関数を使って、IListDataAdapter の実装を作ります。WinJS.Class.define 関数が受け取る最初のパラメーターは、クラス コンストラクターです。
この IListDataAdapter は bing 検索サービスに接続します。Bing API の検索クエリでは特定のデータが必要です。そのデータと、いくつかの追加データを、IListDataAdapter にクラス メンバーとして格納します。
- _minPageSize: 各ページの項目の最小数。
- _maxPageSize: 各ページの項目の最大数。
- _maxCount: 返される項目の最大数。
- _devKey: アプリ ID。 Bing API では、アプリケーションを識別するための AppID キーが必要です。
- _query: 検索文字列。
Bing API の AppID と検索クエリを受け取ってその他のメンバーの値を指定するコンストラクターを作ります。
// Definition of the data adapter var bingImageSearchDataAdapter = WinJS.Class.define( function (devkey, query) { // Constructor this._minPageSize = 10; // based on the default of 10 this._maxPageSize = 50; // max request size for bing images this._maxCount = 1000; // limit on the bing API this._devkey = devkey; this._query = query; },
WinJS.Class.define 関数の次のパラメーターは、クラスのインスタンス メンバーを含むオブジェクトです。このオブジェクトを使って、itemsFromIndex メソッドと getCount メソッドを実装します。
このオブジェクトのための左中かっこを入力します。
// IListDataDapter methods // These methods define the contract between the IListDataSource and the IListDataAdapter. {
-
itemsFromIndex メソッドを実装します。itemsFromIndex メソッドは、データ ソースに接続し、要求されたデータを IFetchResult として返します。 itemsFromIndex メソッドは 3 つのパラメーターを受け取ります。これらのパラメーターで、取得する項目のインデックスと、その項目の前後の項目をそれぞれいくつ取得するかを指定します。
itemsFromIndex: function (requestIndex, countBefore, countAfter) { var that = this;
要求された項目 (requestIndex) が、取得する項目の最大数より少ないことを確認します。そうでない場合はエラーを返します。
if (requestIndex >= that._maxCount) { return Promise.wrapError(new WinJS.ErrorFromName(UI.FetchError.doesNotExist)); }
requestIndex、countBefore、countAfter を使用して、最初の項目のインデックスと要求のサイズを計算します。countBefore パラメーターと countAfter パラメーターは、取得するデータ量の推奨値です。要求されたすべての項目を取得する必要はありません。この例では、bing の最大要求サイズが 50 項目なので、要求のサイズをこの値で制限します。
通常は、要求された項目の前または後の項目が 1 つか 2 つ要求され、その反対側の項目がそれより多く要求されます。したがって、サーバーに要求する内容を決定する際にはこのことを考慮に入れます。
var fetchSize, fetchIndex; // See which side of the requestIndex is the overlap. if (countBefore > countAfter) { // Limit the overlap countAfter = Math.min(countAfter, 10); // Bound the request size based on the minimum and maximum sizes. var fetchBefore = Math.max( Math.min(countBefore, that._maxPageSize - (countAfter + 1)), that._minPageSize - (countAfter + 1) ); fetchSize = fetchBefore + countAfter + 1; fetchIndex = requestIndex - fetchBefore; } else { countBefore = Math.min(countBefore, 10); var fetchAfter = Math.max(Math.min(countAfter, that._maxPageSize - (countBefore + 1)), that._minPageSize - (countBefore + 1)); fetchSize = countBefore + fetchAfter + 1; fetchIndex = requestIndex - countBefore; }
要求文字列を作成します。
// Create the request string. var requestStr = "http://api.bing.net/json.aspx?" + "AppId=" + that._devkey + "&Query=" + that._query + "&Sources=Image" + "&Version=2.0" + "&Market=en-us" + "&Adult=Strict" + "&Filters=Aspect:Wide" + "&Image.Count=" + fetchSize + "&Image.Offset=" + fetchIndex + "&JsonType=raw";
WinJS.xhr を使用して要求を送信します。WinJS.xhr は、結果を含む Promise を返します。結果を処理するには、Promise オブジェクトの then メソッドを呼び出します。
return WinJS.xhr({ url: requestStr }).then(
WinJS.xhr の操作が成功した場合のコールバックを作ります。この関数は、結果を処理して IFetchResult 項目として返します。IFetchResult には次の 3 つのプロパティが含まれています。
- items: クエリの結果を表す IItem オブジェクトの配列。
- offset: items 配列内の要求項目のインデックス。
- totalCount: items 配列内の項目の合計数。
各 IItem には、その項目の識別子を含む key プロパティと、項目のデータを含む data プロパティが必要です。
{ key: key1, data : { field1: value, field2: value, ... }}
したがって、IItem オブジェクトの配列は次のようになります。
[{ key: key1, data : { field1: value, field2: value, ... }}, { key: key2, data : {...}}, ...];
function (request) { var results = [], count; // Use the JSON parser on the results (it's safer than using eval). var obj = JSON.parse(request.responseText); // Verify that the service returned images. if (obj.SearchResponse.Image !== undefined) { var items = obj.SearchResponse.Image.Results; // Create an array of IItem objects: // results =[{ key: key1, data : { field1: value, field2: value, ... }}, { key: key2, data : {...}}, ...]; for (var i = 0, itemsLength = items.length; i < itemsLength; i++) { var dataItem = items[i]; results.push({ key: (fetchIndex + i).toString(), data: { title: dataItem.Title, thumbnail: dataItem.Thumbnail.Url, width: dataItem.Width, height: dataItem.Height, linkurl: dataItem.Url } }); } // Get the count. count = obj.SearchResponse.Image.Total; return { items: results, // The array of items. offset: requestIndex - fetchIndex, // The index of the requested item in the items array. totalCount: Math.min(count, that._maxCount), // The total number of records. Bing will only return 1000, so we cap the value. }; } else { return WinJS.UI.FetchError.doesNotExist; } },
WinJS.xhr の操作が失敗した場合のコールバックを作成します。
// Called if the WinJS.xhr funtion returned an error. function (request) { return WinJS.UI.FetchError.noResponse; });
itemsFromIndex メソッドを閉じます。続けて別のメソッドを定義するため、itemsFromIndex を閉じた後にコンマを追加します。
},
getCount メソッドを実装します。
getCount メソッドはパラメーターを受け取らず、IListDataAdapter オブジェクトの結果の項目の数として Promise を返します。
// Gets the number of items in the result list. // The count can be updated in itemsFromIndex. getCount: function () { var that = this;
要求文字列を作成します。Bing には数を要求する方法は特に用意されていないため、レコードを 1 つ要求し、そのレコードを使って数を取得します。
// Create up a request for 1 item so we can get the count var requestStr = "http://api.bing.net/json.aspx?"; // Common request fields (required) requestStr += "AppId=" + that._devkey + "&Query=" + that._query + "&Sources=Image"; // Common request fields (optional) requestStr += "&Version=2.0" + "&Market=en-us" + "&Adult=Strict" + "&Filters=Aspect:Wide"; // Image-specific request fields (optional) requestStr += "&Image.Count=1" + "&Image.Offset=0" + "&JsonType=raw";
WinJS.xhr を使って要求を送信します。結果を処理して数を返します。
// Make an XMLHttpRequest to the server and use it to get the count. return WinJS.xhr({ url: requestStr }).then( // The callback for a successful operation. function (request) { var data = JSON.parse(request.responseText); // Bing may return a large count of items, /// but you can only fetch the first 1000. return Math.min(data.SearchResponse.Image.Total, that._maxCount); }, function (request) { return WinJS.Promise.wrapError(new WinJS.ErrorFromName(WinJS.UI.FetchError.doesNotExist)); }); }
これが最後のインスタンス メンバーなので、インスタンス メンバーを格納するために作成したオブジェクトを閉じます。IListDataAdapter には、実装できるメソッドがほかにもありますが、読み取り専用のデータ ソースには必要ありません。
// setNotificationHandler: not implemented // itemsFromStart: not implemented // itemsFromEnd: not implemented // itemsFromKey: not implemented // itemsFromDescription: not implemented }
WinJS.Class.define の呼び出しを閉じます。
);
以上で、IListDataAdapter インターフェイスを実装する
bingImageSarchDataAdapter
というクラスを作成できました。次に、IListDataSource を作成します。
ステップ 3: IListDataSource を作成する
IListDataSource は、ListView などのコントロールを IListDataAdapter に接続します。IListDataSource が IListDataAdapter を操作し、IListDataAdapter が実際のデータの操作や取得を行います。この手順では、IListDataSource を実装します。
WinJS には、VirtualizedDataSource オブジェクトという IListDataSource インターフェイスの実装が用意されています。このオブジェクトを、独自の IListDataSource を実装するために使用できます。これから見ていくように、必要な作業はあまりありません。
WinJS.Class.derive 関数を使用して、VirtualizedDataSource を継承するクラスを作成します。この関数の 2 番目のパラメーターとして、Bing のアプリ ID とクエリ文字列を受け取るコンストラクターを定義します。そのコンストラクターで基底クラスのコンストラクターを呼び出して、新しい
bingImageSarchDataAdapter
(前の手順で定義したオブジェクト) を渡します。var bingImageSearchDataSource = WinJS.Class.derive(WinJS.UI.VirtualizedDataSource, function (devkey, query) { this._baseDataSourceConstructor(new bingImageSearchDataAdapter(devkey, query)); });
WinJS.Namespace.define 関数を使用して名前空間を定義し、クラスをパブリックにアクセス可能にします。WinJS.Namespace.define 関数は 2 つのパラメーターを受け取ります。作成する名前空間の名前と、1 つ以上のプロパティ/値ペアを含むオブジェクトです。各プロパティはメンバーのパブリック名で、各値は、プライベート コードの基になる変数、プロパティ、または関数です。指定した変数、プロパティ、関数が公開されます。
WinJS.Namespace.define("DataExamples", { bingImageSearchDataSource: bingImageSearchDataSource });
以上で IListDataAdapter と IListDataSource を実装できました。bingImageSearchDataSource.js の作業が完了したので、外側の匿名関数を閉じます。
})();
作成したカスタム データ ソースを使うには、
bingImageSearchDataSource
クラスの新しいインスタンスを作ります。コンストラクターにアプリの Bing アプリ ID と検索クエリを渡します。var myDataSrc = new DataExamples.bingImageSearchDataSource(devKey, searchTerm);
これで、IListDataSource を受け取るコントロール (ListView コントロールなど) で
bingImageSearchDataSource
を使うことができます。
完全な例
bingImageSearchDataSource.js の完全なコードを以下に示します。完全なサンプルについては、データ ソースの操作のサンプルをご覧ください。
// Bing image search data source example
//
// This code implements a datasource that will fetch images from Bing's image search feature
// Because the Bing service requires a developer key, and each app needs its own key, you must
// register as a developer and obtain an App ID to use as a key.
// For more info about how to obtain a key and use the Bing API, see
// https://bing.com/developers and https://msdn.microsoft.com/en-us/library/dd251056.aspx
(function () {
// Define the IListDataAdapter.
var bingImageSearchDataAdapter = WinJS.Class.define(
function (devkey, query) {
// Constructor
this._minPageSize = 10; // based on the default of 10
this._maxPageSize = 50; // max request size for bing images
this._maxCount = 1000; // limit on the bing API
this._devkey = devkey;
this._query = query;
},
// IListDataDapter methods
// These methods define the contract between the IListDataSource and the IListDataAdapter.
// These methods will be called by vIListDataSource to fetch items,
// get the number of items, and so on.
{
// This example only implements the itemsFromIndex and count methods
// The itemsFromIndex method is called by the IListDataSource
// to retrieve items.
// It will request a specific item and hints for a number of items before and after the
// requested item.
// The implementation should return the requested item. You can choose how many
// additional items to send back. It can be more or less than those requested.
//
// This funtion must return an object that implements IFetchResult.
itemsFromIndex: function (requestIndex, countBefore, countAfter) {
var that = this;
if (requestIndex >= that._maxCount) {
return Promise.wrapError(new WinJS.ErrorFromName(UI.FetchError.doesNotExist));
}
var fetchSize, fetchIndex;
// See which side of the requestIndex is the overlap.
if (countBefore > countAfter) {
// Limit the overlap
countAfter = Math.min(countAfter, 10);
// Bound the request size based on the minimum and maximum sizes.
var fetchBefore = Math.max(
Math.min(countBefore, that._maxPageSize - (countAfter + 1)),
that._minPageSize - (countAfter + 1)
);
fetchSize = fetchBefore + countAfter + 1;
fetchIndex = requestIndex - fetchBefore;
} else {
countBefore = Math.min(countBefore, 10);
var fetchAfter = Math.max(Math.min(countAfter, that._maxPageSize - (countBefore + 1)), that._minPageSize - (countBefore + 1));
fetchSize = countBefore + fetchAfter + 1;
fetchIndex = requestIndex - countBefore;
}
// Create the request string.
var requestStr = "http://api.bing.net/json.aspx?"
+ "AppId=" + that._devkey
+ "&Query=" + that._query
+ "&Sources=Image"
+ "&Version=2.0"
+ "&Market=en-us"
+ "&Adult=Strict"
+ "&Filters=Aspect:Wide"
+ "&Image.Count=" + fetchSize
+ "&Image.Offset=" + fetchIndex
+ "&JsonType=raw";
// Return the promise from making an XMLHttpRequest to the server.
return WinJS.xhr({ url: requestStr }).then(
// The callback for a successful operation.
function (request) {
var results = [], count;
// Use the JSON parser on the results (it's safer than using eval).
var obj = JSON.parse(request.responseText);
// Verify that the service returned images.
if (obj.SearchResponse.Image !== undefined) {
var items = obj.SearchResponse.Image.Results;
// Create an array of IItem objects:
// results =[{ key: key1, data : { field1: value, field2: value, ... }}, { key: key2, data : {...}}, ...];
for (var i = 0, itemsLength = items.length; i < itemsLength; i++) {
var dataItem = items[i];
results.push({
key: (fetchIndex + i).toString(),
data: {
title: dataItem.Title,
thumbnail: dataItem.Thumbnail.Url,
width: dataItem.Width,
height: dataItem.Height,
linkurl: dataItem.Url
}
});
}
// Get the count.
count = obj.SearchResponse.Image.Total;
return {
items: results, // The array of items.
offset: requestIndex - fetchIndex, // The index of the requested item in the items array.
totalCount: Math.min(count, that._maxCount), // The total number of records. Bing will only return 1000, so we cap the value.
};
} else {
return WinJS.UI.FetchError.doesNotExist;
}
},
// Called if the WinJS.xhr funtion returned an error.
function (request) {
return WinJS.UI.FetchError.noResponse;
});
},
// Gets the number of items in the result list.
// The count can be updated in itemsFromIndex.
getCount: function () {
var that = this;
// Create up a request for 1 item so we can get the count
var requestStr = "http://api.bing.net/json.aspx?";
// Common request fields (required)
requestStr += "AppId=" + that._devkey
+ "&Query=" + that._query
+ "&Sources=Image";
// Common request fields (optional)
requestStr += "&Version=2.0"
+ "&Market=en-us"
+ "&Adult=Strict"
+ "&Filters=Aspect:Wide";
// Image-specific request fields (optional)
requestStr += "&Image.Count=1"
+ "&Image.Offset=0"
+ "&JsonType=raw";
// Make an XMLHttpRequest to the server and use it to get the count.
return WinJS.xhr({ url: requestStr }).then(
// The callback for a successful operation.
function (request) {
var data = JSON.parse(request.responseText);
// Bing may return a large count of items,
/// but you can only fetch the first 1000.
return Math.min(data.SearchResponse.Image.Total, that._maxCount);
},
function (request) {
return WinJS.Promise.wrapError(new WinJS.ErrorFromName(WinJS.UI.FetchError.doesNotExist));
});
}
// setNotificationHandler: not implemented
// itemsFromStart: not implemented
// itemsFromEnd: not implemented
// itemsFromKey: not implemented
// itemsFromDescription: not implemented
}
);
var bingImageSearchDataSource = WinJS.Class.derive(WinJS.UI.VirtualizedDataSource, function (devkey, query) {
this._baseDataSourceConstructor(new bingImageSearchDataAdapter(devkey, query));
});
WinJS.Namespace.define("DataExamples", { bingImageSearchDataSource: bingImageSearchDataSource });
})();