Using the query builder pattern in Hilo (Windows Store apps using JavaScript and 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]

From: Developing an end-to-end Windows Store app using JavaScript: Hilo

Previous page | Next page

Hilo implements a combination of the builder pattern and the query object pattern to construct query objects for data access to the Windows file system.

Download

After you download the code, see Getting started with Hilo for instructions.

You will learn

  • How to implement the builder pattern for a Windows Store app built using JavaScript.
  • How to implement the query object pattern for a Windows Store app built using JavaScript.
  • How to prefetch properties to optimize performance.
  • How to create and execute a file system query.

Applies to

  • Windows Runtime for Windows 8
  • Windows Library for JavaScript
  • JavaScript

The builder pattern

The builder pattern is an object creation pattern in which you provide input to construct a complex object in multiple steps. Depending on input, the object may vary in its representation. In Hilo, we construct a query builder object whose only function is to build a query object, which encapsulates a query. All of the pages in Hilo need access to the local picture folder to display images, so the query builder settings are particular to a Windows file system query.

Hilo\Hilo\month\month.js

this.queryBuilder = new Hilo.ImageQueryBuilder();

When we create the query builder, default builder options specific to a file system query are set in the constructor. After you create a query builder, you can call its particular members to set options that are specific to the page and query. The following code specifies that the query builder object will create a query object that

  • Is bindable to the UI
  • Includes a prefetch call on a file property
  • Is limited to six items in the query result

Hilo\Hilo\hub\hubPresenter.js

this.queryBuilder
    .bindable(true)
    .prefetchOptions(["System.ItemDate"])
    .count(maxImageCount);

The chained functions in the preceding code demonstrate the fluent coding style, which is supported because the builder pattern used in Hilo implements the fluent interface. The fluent interface enables a coding style that flows easily, partly through the implementation of carefully named methods. In the query builder object, each function in the chain returns itself (the query builder object). This enables the fluent coding style. For information on the fluent interface, see FluentInterface.

Once you have set desired options on the query builder, we use the settings to create a Windows.Storage.Search.QueryOptions object. We then build the query by passing the query options object as an argument to createFileQueryWithOptions. Finally, to execute the query we call getFilesAsync. We will show an example of building and executing a query in the Code Walkthrough.

For general information on the builder pattern, see Builder pattern.

[Top]

The query object pattern

In the query object pattern, you define a query by using a bunch of properties, and then execute the query. You can change options on the query builder and then use the builder to create a new query object. The following code shows the constructor function for the internally-defined query object. The passed-in settings are used to build a file query (_buildQueryOptions and _buildFileQuery).

Hilo\Hilo\imageQueryBuilder.js

function QueryObjectConstructor(settings) {
    // Duplicate and the settings by copying them
    // from the original, to a new object. This is
    // a shallow copy only.
    //
    // This prevents the original queryBuilder object
    // from modifying the settings that have been
    // sent to this query object.
    var dupSettings = {};
    Object.keys(settings).forEach(function (key) {
        dupSettings[key] = settings[key];
    });

    this.settings = dupSettings;

    // Build the query options.
    var queryOptions = this._buildQueryOptions(this.settings);
    this._queryOptionsString = queryOptions.saveToString();

    if (!this.settings.folder.createFileQueryWithOptions) {
        var folder = supportedFolders[this.settings.folderKey];
        if (!folder) {
            // This is primarily to help any developer who has to extend Hilo.
            // If they add support for a new folder, but forget to register it
            // at the head of this module then this error should help them
            // identify the problem quickly.
            throw new Error("Attempted to deserialize a query for an unknown folder: " + settings.folderKey);
        }
        this.settings.folder = folder;
    }

    this.fileQuery = this._buildFileQuery(queryOptions);
},

The query object pattern also makes it easy to serialize and deserialize the query. You do this by using methods on the query object, typically with corresponding names. For general info on the query object pattern, see Query Object.

In developing Hilo, we changed our initial implementation of the repository pattern to the query object pattern. The main difference between the repository pattern and the query object pattern is that in the repository pattern you create an abstraction of your data source and call methods such as getImagesByType. Whereas, with the query object pattern, you set properties on the query builder, and then simply build a query based on the current state of the builder.

We changed to the query object pattern mainly for two reasons. First, we found we were building different query objects with a lot of the same code and default values in different pages. Second, we were adding repository functions that were no longer generic. The following code shows an example of one specialized repository function that we removed from Hilo.

   getQueryForMonthAndYear: function(monthAndYear){   
       var options = this.getQueryOptions();   
       options.applicationSearchFilter = 'taken: ' + monthAndYear;   
       return options.saveToString();   
   },

We felt that adding page-specific functions to the repository made the code too brittle, so we chose instead to streamline the implementation by using a query builder.

[Top]

Code walkthrough

The code defines the query builder class in imageQueryBuilder.js. The code also exposes the query builder object, Hilo.ImageQueryBuilder, for use in the app by using WinJS.Namespace.define.

Hilo\Hilo\imageQueryBuilder.js

WinJS.Namespace.define("Hilo", {
    ImageQueryBuilder: ImageQueryBuilder
});

In Hilo, a particular page can request a query builder by first creating a new ImageQueryBuilder object. Typically, we create the query builder in each page's ready function.

Hilo\Hilo\month\month.js

this.queryBuilder = new Hilo.ImageQueryBuilder();

Once we create the query builder, we pass it to the page's presenter. For more info on the Model-View-Presenter pattern we used to create different views, see Using a separated presentation pattern.

Hilo\Hilo\month\month.js

this.presenter = new Hilo.month.MonthPresenter(loadingIndicator, this.semanticZoom, this.zoomInListView, zoomOutListView, hiloAppBar, this.queryBuilder);

When you create the query builder object, the constructor for the query builder calls reset to set the default values.

Hilo\Hilo\imageQueryBuilder.js

function ImageQueryBuilderConstructor() {
    this.reset();
},

In the reset function of the query builder object, we set all the default values, such as the source file folder and the supported files types.

Hilo\Hilo\imageQueryBuilder.js

reset: function () {
    this._settings = {};
    this._set("fileTypes", [".jpg", ".jpeg", ".tiff", ".png", ".bmp", ".gif"]);
    this._set("prefetchOption", fileProperties.PropertyPrefetchOptions.imageProperties);

    this._set("thumbnailOptions", fileProperties.ThumbnailOptions.useCurrentScale);
    this._set("thumbnailMode", fileProperties.ThumbnailMode.picturesView);
    this._set("thumbnailSize", 256);

    this._set("sortOrder", commonFileQuery.orderByDate);
    this._set("indexerOption", search.IndexerOption.useIndexerWhenAvailable);
    this._set("startingIndex", 0);
    this._set("bindable", false);

    return this;
},

The values are added to the array in the _set method.

Hilo\Hilo\imageQueryBuilder.js

_set: function (key, value) {
    this._settings[key] = value;
}

After you create a query builder, you can set additional options on the builder to match the desired query. For example, in the hub page, we set options in the following code. These options specify the type of builder as bindable, set a prefetch option for the item date (System.ItemDate), and set the count to six. The count value indicates that the hub page will display only six images total.

Hilo\Hilo\hub\hubPresenter.js

this.queryBuilder
    .bindable(true)
    .prefetchOptions(["System.ItemDate"])
    .count(maxImageCount);

When we call bindable, we simply add a new "bindable" setting to the array of the query builder’s settings. We will use this property later to create bindable objects that wrap the returned file system objects. For more info, see Making the file system objects observable.

Hilo\Hilo\imageQueryBuilder.js

bindable: function (bindable) {
    // `!!` is a JavaScript coersion trick to convert any value
    // in to a true boolean value. 
    //
    // When checking equality and boolean values, JavaScript 
    // coerces `undefined`, `null`, `0`, `""`, and `false` into 
    // a boolean value of `false`. All other values are coerced 
    // into a boolean value of `true`.
    //
    // The first ! then, negates the coerced value. For example,
    // a value of "" (empty string) will be coerced in to `false`.
    // Therefore `!""` will return `true`. 
    //
    // The second ! then inverts the negated value to the
    // correct boolean form, as a true boolean value. For example,
    // `!!""` returns `false` and `!!"something"` returns true.
    this._set("bindable", !!bindable);
    return this;
},

When you perform file operations with the Windows Runtime, it can be helpful to instruct Windows Runtime to optimize the retrieval of specific file properties. You do this by setting prefetch options on a query. Here, we take the passed in System.ItemDate parameter and set the query builder’s prefetchOption property to the same value. Initially, we just set the prefetch properties on the query builder, as shown here. We will use this value later when we build the query.

Hilo\Hilo\imageQueryBuilder.js

prefetchOptions: function (attributeArray) {
    this._set("prefetchOption", fileProperties.PropertyPrefetchOptions.none);
    this._set("prefetchAttributes", attributeArray);
    return this;
},

Once the query builder’s options are set, we call build to build the actual query. We pass in a Windows.Storage.KnownFolders object to specify the file system folder set.

Hilo\Hilo\hub\hubPresenter.js

var query = this.queryBuilder.build(this.folder);

The build method creates a new internally defined query object. In the query object’s constructor function, QueryObjectConstructor, we attach the query builder’s settings.

Hilo\Hilo\imageQueryBuilder.js

function QueryObjectConstructor(settings) {
    // Duplicate and the settings by copying them
    // from the original, to a new object. This is
    // a shallow copy only.
    //
    // This prevents the original queryBuilder object
    // from modifying the settings that have been
    // sent to this query object.
    var dupSettings = {};
    Object.keys(settings).forEach(function (key) {
        dupSettings[key] = settings[key];
    });

    this.settings = dupSettings;

    // Build the query options.
    var queryOptions = this._buildQueryOptions(this.settings);
    this._queryOptionsString = queryOptions.saveToString();

    if (!this.settings.folder.createFileQueryWithOptions) {
        var folder = supportedFolders[this.settings.folderKey];
        if (!folder) {
            // This is primarily to help any developer who has to extend Hilo.
            // If they add support for a new folder, but forget to register it
            // at the head of this module then this error should help them
            // identify the problem quickly.
            throw new Error("Attempted to deserialize a query for an unknown folder: " + settings.folderKey);
        }
        this.settings.folder = folder;
    }

    this.fileQuery = this._buildFileQuery(queryOptions);
},

From the constructor, the query object calls _buildQueryOptions. This function turns the query builder settings into a valid Windows.Storage.Search.QueryOptions object. It is here that we set prefetch options to optimize performance. For the Hub page, we pass the ItemDate property as the second parameter in setPropertyPrefetch.

Hilo\Hilo\hub\hub.js

_buildQueryOptions: function (settings) {
    var queryOptions = new search.QueryOptions(settings.sortOrder, settings.fileTypes);
    queryOptions.indexerOption = settings.indexerOption;

    queryOptions.setPropertyPrefetch(settings.prefetchOption, settings.prefetchAttributes);

    if (this.settings.monthAndYear) {
        queryOptions.applicationSearchFilter = translateToAQSFilter(settings.monthAndYear);
    }

    return queryOptions;
},

The prefetched item date is not used directly in the hub view. The hub view doesn’t care about dates, and just displays the first six images. But in hub.js we will use the item date to set the correct index value on an image before passing to other pages, such as the details page. Windows Runtime handles retrieval of the item date on demand in a separate asynchronous operation. This is a relatively slow operation that is performed one file at a time. By prefetching the item dates before we get the actual images, we could improve performance.

For the month page, we also set a prefetch option on the thumbnails (setThumbnailPrefetch). For more info on prefetching thumbnails, see Improving performance.

When creating your own apps, you will want to test prefetch options when you interact with the file system, to see whether you can improve performance.

After calling _buildQueryOptions, the query object constructor then calls _buildFileQuery. This function converts the QueryOptions object to a Windows Runtime file query by using Windows.Storage.StorageFolder.createFileQueryWithOptions

Hilo\Hilo\imageQueryBuilder.js

_buildFileQuery: function (queryOptions) {
    return this.settings.folder.createFileQueryWithOptions(queryOptions);
},

We then execute the query. In the Hub page, the code to execute the query looks like this.

Hilo\Hilo\hub\hubPresenter.js

return query.execute()
    .then(function (items) {
        if (items.length === 0) {
            self.displayLibraryEmpty();
        } else {
            self.bindImages(items);
            self.animateEnterPage();
        }
    });

In the execute call, we use getFilesAsync to actually retrieve the images. We then check whether the bindable property was set in the query. If bindable is set, this means we need to wrap returned StorageFile objects to make them usable for binding to the UI. For info on wrapping the file system object for binding to the UI, see Making the file system objects observable.

Hilo\Hilo\imageQueryBuilder.js

execute: function () {
    var start, count;
    var queryPromise;

    switch (arguments.length) {
        case (0):
            start = this.settings.startingIndex;
            count = this.settings.count;
            break;
        case (1):
            start = arguments[0];
            count = 1;
            break;
        case (2):
            start = arguments[0];
            count = arguments[1];
            break;
        default:
            throw new Error("Unsupported number of arguments passed to `query.execute`.");
    }

    if (count) {
        // Limit the query to a set number of files to be returned, which accounts
        // for both the `count(n)` and `imageAt(n)` settings from the query builder.
        queryPromise = this.fileQuery.getFilesAsync(start, count);
    } else {
        queryPromise = this.fileQuery.getFilesAsync();
    }

    if (this.settings.bindable) {
        // Create `Hilo.Picture` objects instead of returning `StorageFile` objects
        queryPromise = queryPromise.then(this._createViewModels);
    }

    return queryPromise;
},

When the query result is returned in the completed promise, we call bindImages. The main job of this function is to associate the ungrouped index value for each returned image to a group index value that is month-based. It then requests binding to the UI by using setDataSource.

Hilo\Hilo\hub\hubPresenter.js

bindImages: function (items) {
    this.dataSource = items;

    if (items.length > 0) {
        items[0].className = items[0].className + " first";
    }

    // We need to know the index of the image with respect to
    // to the group (month/year) so that we can select it
    // when we navigate to the detail page.
    var lastGroup = "";
    var indexInGroup = 0;
    items.forEach(function (item) {
        var group = item.itemDate.getMonth() + " " + item.itemDate.getFullYear();
        if (group !== lastGroup) {
            lastGroup = group;
            indexInGroup = 0;
        }

        item.groupIndex = indexInGroup;
        indexInGroup++;
    });

    this.listview.setDataSource(items);
},

The Hub page uses a WinJSBinding.List for its data source. For info on data sources used in Hilo, see Working with data sources.

[Top]

Making the file system objects observable

Objects returned from the file system, aren’t in a format that’s bindable (observable) to controls such as ListView and FlipView. This is because Windows Runtime is not mutable. There is a WinJS utility for making an object observable, but this utility tries to change the object by wrapping it in a proxy. When returned StorageFile objects need to be bound to the UI, they are set as "bindable" in the query builder. For bindable objects, the execute function in the query object calls _createViewModels. This maps the file system objects to Hilo.Picture objects.

Hilo\Hilo\imageQueryBuilder.js

_createViewModels: function (files) {
    return WinJS.Promise.as(files.map(Hilo.Picture.from));
}

The code here shows the constructor function for a Picture object. In this function, string values are added to the object as properties. After the properties are set, they can be used for binding to the UI. For example, the url property is set by calling the app’s URL cache (not shown). The cache calls URL.createObjectURL. createObjectURL takes as input a blob of binary data (a passed-in thumbnail) and returns a string URL.

Hilo\Hilo\Picture.js

function PictureConstructor(file) {
    var self = this;

    this.addUrl = this.addUrl.bind(this);

    this.storageFile = file;
    this.urlList = {};
    this.isDisposed = false;

    this._initObservable();
    this.addProperty("name", file.name);
    this.addProperty("isCorrupt", false);
    this.addProperty("url", "");
    this.addProperty("src", "");
    this.addProperty("itemDate", "");
    this.addProperty("className", "thumbnail");

    this.loadFileProperties();
    this.loadUrls();
},

Tip  Before navigating to a new page, you need to release objects that you created by using URL.createObjectURL. This avoids a potential memory leak. You can do this by using URL.revokeObjectURL, which in Hilo is called in urlCache.js (not shown). For info on finding memory leaks and other performance tips, see Improving performance and Analyzing memory usage data.

 

[Top]