How to create a custom data source (HTML)

[This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation]

Windows Library for JavaScript provides several ready-to-use data source objects that you can use to populate a ListView or FlipView with different types of data. There's the WinJS.Binding.List object for accessing arrays and JSON data, there's the StorageDataSource for accessing information about the file system.

You aren't limited to just these data sources. You can create your own custom data source that accesses any other type of data, such as an XML file or a web service. This topic shows you how to implement a custom data source that accesses a web service. It uses XHR to connect to the Bing image search service and displays the result in a ListView.

(Because the Bing service requires that each app has its own app ID key, you need to obtain an key before you can use this code. For more information on how to obtain an app ID key, see the bing developer center.)

To create a custom data source, you need objects that implement the IListDataAdapter and IListDataSource interfaces. The WinJS provides a VirtualizedDataSource object that implements IListDataSource—all you need to do is inherit from it and pass the base constructor an IListDataAdapter. You need to create your own object that implements the IListDataAdapter interface.

The IListDataAdapter interacts directly with the data source to retrieve or update items. The IListDataSource connects to a control and manipulates the IListDataAdapter.

Prerequisites

Instructions

Step 1: Create a JavaScript file for your custom data source

  1. Use Microsoft Visual Studio to add a JavaScript file to your project. In the Solution Explorer, right click your project's js folder and select Add > New Item. The Add New Item dialog appears.
  2. Select JavaScript File. Give it the name "bingImageSearchDataSource.js". Click Add to create the file. Visual Studio creates a blank JavaScript file named bingImageSearchDataSource.js.

Step 2: Create an IListDataAdapter

The next step is to create an object that implements the IListDataAdapter interface. An IListDataAdapter retrieves data from a data source and provides it to an IListDataSource.

The IListDataAdapter interface supports read and write access and change notifications. However, you don't need to implement the entire interface: you can create a simple, read-only IListDataAdapter by implementing just the itemsFromIndex and getCount methods.

  1. Open bingImageSearchDataSource.js, the JavaScript file that you created in the previous step.

  2. Create an anonymous function and turn on strict mode.

    As described in Coding basic apps, it's a good idea to encapsulate your JavaScript code by wrapping it in an anonymous function, and it's a good idea to use strict mode.

    (function () {
        "use strict"; 
    
  3. Use the WinJS.Class.define function to create your implementation of IListDataAdapter. The first parameter that the WinJS.Class.define function takes is the class constructor.

    This IListDataAdapter will connect to the bing search service. The Bing API search query expects certain data. We'll store this data, as well as some additional data, in the IListDataAdapter as class members:

    • _minPageSize: The minimum number of items per page.
    • _maxPageSize: The maximum number of items per page.
    • _maxCount: The maximum number of items to return.
    • _devKey: The app ID. The Bing API requires a AppID key to identify the application.
    • _query: The search string.

    Create a constructor that takes an AppID for the Bing API and a search query and provides values for the other members.

    
        // 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;
            },
    
  4. The next parameter the WinJS.Class.define function expects is an object that contains the class's instance members. You use this object to implement the itemsFromIndex and getCount methods.

    Create the opening brace for this object.

            // IListDataDapter methods
            // These methods define the contract between the IListDataSource and the IListDataAdapter.
            {
    
    1. Implement the itemsFromIndex method. The itemsFromIndex method connects to the data source and returns the requested data as an IFetchResult. The itemsFromIndex method takes three parameters: the index of an item to retrieve, the number of items before that item to retrieve, and the number of items after that item to retrieve.

                  itemsFromIndex: function (requestIndex, countBefore, countAfter) {
                      var that = this;
      
    2. Verify that the requested item (requestIndex) is less than the maximum number of items to retrieve. If it's not, return an error.

                      if (requestIndex >= that._maxCount) {
                          return Promise.wrapError(new WinJS.ErrorFromName(UI.FetchError.doesNotExist));
                      }
      
    3. Use requestIndex, countBefore, and countAfter to calculate the index of the first item and the size of the request. The countBefore and countAfter parameters are recommendations for how much data to retrieve: you are not required to retrieve all the items that you are asked for. In this example, bing has a maximum request size of 50 items, so we want to limit our request size to that.

      Typically a request will ask for one or two items before or after the requested item, and a larger number from the opposite side, so we want to take that into account when figuring out what we ask the server.

                      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;
                      }
      
    4. Create the request string.

                      // 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";
      
    5. Use WinJS.xhr to submit the request. The WinJS.xhr returns a Promise that contains the result. You can process the result by calling the Promise object's then method.

                      return WinJS.xhr({ url: requestStr }).then(
      
    6. Create a callback for a successful WinJS.xhr operation. This function processes the results and returns them as an IFetchResult item. The IFetchResult contains three properties:

      • items: An array of IItem objects that represent the query results.
      • offset: The index of the request item in the items array.
      • totalCount: the total number of items in the items array.

      Each IItem must have a key property that contains an identifier for that item and a data property that contains the item's data:

      { key: key1, data : { field1: value, field2: value, ... }}

      Here's what the array of the IItem objects would look like:

      [{ 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;
                              }
                          },
      
    7. Create a callback for an unsuccessful WinJS.xhr operation.

                          // Called if the WinJS.xhr funtion returned an error. 
                          function (request) {
                              return WinJS.UI.FetchError.noResponse;
                          });
      
    8. Close the itemsFromIndex method. You'll define another method next, so add a comma after you close itemsFromIndex.

                  },
      
  5. Implement the getCount method.

    1. The getCount method doesn't take any parameters and returns a Promise for the number of items in the IListDataAdapter object's results.

                  // Gets the number of items in the result list. 
                  // The count can be updated in itemsFromIndex.
                  getCount: function () {
                      var that = this;
      
    2. Create the request string. Because Bing doesn’t have an explicit way to ask for the count, we request one record and use it to obtain the count.

      
                      // 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";
      
    3. Use WinJS.xhr to submit the request. Process the results and return the count.

                      // 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));
                          });
                  }
      
  6. That's the last instance member, so close the object that you created to contain them. There are other IListDataAdapter methods that you could implement, but you don't need them to create a read only data source.

                // setNotificationHandler: not implemented
                // itemsFromStart: not implemented
                // itemsFromEnd: not implemented
                // itemsFromKey: not implemented
                // itemsFromDescription: not implemented
            }
    
  7. Close the call to WinJS.Class.define.

            );
    

    You created a class named bingImageSarchDataAdapter that implements the IListDataAdapter interface. Next, you'll create an IListDataSource.

Step 3: Create an IListDataSource

A IListDataSource connects a control (such as a ListView) to an IListDataAdapter. The IListDataSource manipulates the IListDataAdapter, which does the work of actually manipulating and retrieving data. In this step, you implement an IListDataSource.

The WinJS provides one implementation of the IListDataSource interface for you: the VirtualizedDataSource object. You can use this object to help implement your IListDataSource. As you'll see in a moment, there isn't much work to do.

  1. Use the WinJS.Class.derive function to create a class that inherits from VirtualizedDataSource. For the function's second parameter, define a constructor that takes a Bing App ID and a query string. Have the constructor call the base class constructor and pass it a new bingImageSarchDataAdapter (the object you defined in the previous step).

        var bingImageSearchDataSource = WinJS.Class.derive(WinJS.UI.VirtualizedDataSource, function (devkey, query) {
            this._baseDataSourceConstructor(new bingImageSearchDataAdapter(devkey, query));
        });
    
  2. Use the WinJS.Namespace.define function to define a namespace and make the class publicly accessible. The WinJS.Namespace.define function takes two parameters: the name of the namespace to create, and an object that contains one or more property/value pairs. Each property is the public name of the member, and each value is underlying variable, property, or function in your private code that you want to expose.

        WinJS.Namespace.define("DataExamples", { bingImageSearchDataSource: bingImageSearchDataSource });  
    
  3. You've now implemented an IListDataAdapter and an IListDataSource. You're done with bingImageSearchDataSource.js, so you can close the outer anonymous function.

    })();
    

    To use your custom data source, create a new instance of the bingImageSearchDataSource class. Pass the constructor the Bing App ID for your app and a search query:

    var myDataSrc = new DataExamples.bingImageSearchDataSource(devKey, searchTerm);
    

    You can now use bingImageSearchDataSource with controls that take an IListDataSource, such as the ListView control.

Complete example

Here's the complete code for bingImageSearchDataSource.js. For the complete sample, see the Working with data sources sample.

// 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 }); 

})();