Pre-Process your images with promises | John V. Petersen

:

For this post, I’m using Windows 8 WinJS as the vehicle. However, this approach is applicable by any method that implements an XMLHttpRequest.

Here’s the scenario: you have an application with several images and those images are stored on a remote server. The sources of each image is something like http://someserver.com/myimage.png.

Looking at a Windows 8 store app as an example, the good news is that your page will display immediately. Your images may display immediately. Or more likely, they will begin to display after the page has loaded. You may see the images incrementally appearing. This does not make for a good user experience.

The good news is that WinJS implements the Promise Pattern and XMLHttpRequest via the WinJS.xhr object. This means we can pre-fetch images and resolve them to blob URL’s and then bind our UI to the blob URL instead of the raw image url.

Using a stock WinJS Navigation Project, the following code is placed in the app.addEventListener(“activated”, function (args) {} handler:

var promises = [];

//Load the blob for the default image in the event a specified image URL does not work.
WinJS.xhr({ url: "images/no-available-image.png", responseType: "blob" }).done(function 
(data) {
 var noImageBlob = URL.createObjectURL(data.response);
  //The imageResults.json file is a static result from the Bing Images API.
  WinJS.xhr({ url: "js/imageResults.json" }).done(function (data) {
     var results = JSON.parse(data.responseText);
     //Loop through the results to create the promise array.
     results.d.results[0].Image.forEach(function (element, index, array) {
     promises[index] = WinJS.xhr({ url: element.MediaUrl, responseType: "blob" })
        .then(function (e) {
           //When the promise executes, an imageBlob property is created
           //holding the image blob URL
           results.d.results[0].Image[index].imageBlob = URL.createObjectURL(e.response);
        }, function error(e) {
           //If an error occurs when trying to retrieve the image
           //the No Image BLob URL created on line 28 is used instead
           results.d.results[0].Image[index].imageBlob = noImageBlob;
        });
     });
     //The join method kicks off the promises handed to it.
     //The promises can run in any order and finish at any time. 
     //The done event for the join method fires after all of the promises
     //in the promise array have been fulfilled.
     WinJS.Promise.join(promises).done(function (e) {
        //Need to create a global namespace to hold a reference
        //to the images array. To facilitate data binding, 
        //the WinJS.BindingList method is invoked, passing the 
        //image array.
        WinJS.Namespace.define("ImageList",
           {
              Images: new WinJS.Binding.List(results.d.results[0].Image)
           });;
           //Now that all of the pre-processing has occurred, we can now
           //navigate to the home page that displays the images.
           if (app.sessionState.history) {
              nav.history = app.sessionState.history;
           }
           args.setPromise(WinJS.UI.processAll().then(function () {
              if (nav.location) {
                 nav.history.current.initialPlaceholder = true;
               return nav.navigate(nav.location, nav.state);
           }
           else {
                return nav.navigate(Application.navigator.home);
           }
        }));
     });
  });
);

JavasScript, being a dynamic language, we can simply augment the object from the parsed JSON. In this example, I used a static file from the Bing Image Search API. You will have to conform the object references to the specific API you are working with. In the previous code, we wait until all of the images have been fetch. In an async world, this can be a challenge. We don’t know what order the requests will be processed. In some cases, a server status 500 could result. This approach affords us an opportunity to handle that error with a place holder image. If on the other hand, you deal with the raw URL’s directly, you have no such opportunity to intercept errors.

On the HTML markup side, I have this code to bind an image tag’s src to the newly created imageBlob property. For this example, the raw URL that is returned in the response is MediaUrl.

<section aria-label="Main content" role="main">
   <div id="ImagesTemplate" data-win-control="WinJS.Binding.Template">
      <div style="width: 150px; height: 100px;">
          <img src="#" style="width: 60px; height: 60px;" 
              data-win-bind="alt: MediaUrl; src: imageBlob" />
      <div>
       <h4 data-win-bind="innerText: MediaUrl"></h4>
      </div>
   </div>
</section>
<div id="ImageListView" 
   data-win-control="WinJS.UI.ListView"  
   data-win-options="{itemDataSource : ImageList.Images.dataSource, 
                      itemTemplate: select('#ImagesTemplate')}">
</div>

In the markup, I have a WinJS ListView control that uses the ImageList.Images.dataSource as its datasource. The ListView also references a simple template to display the output.

Here is the result (looks a bit skewed due to this page’s dimensions):

images

When the page displays, because the images have already been pre-fetched, they will immediately display as well. This makes for a more pleasant user experience.

Looking at the first image, it has a raw URL of:

http://www.israbox.com/uploads/posts/2011-04/1303738133_john-coltrane.jpg

When pre-fetched, the blob URL is:

blob:354BAAC6-B8D8-4172-A283-4DF2F037FECD