6
votes

The question: How can I structure my code so that the knockout bindings are not applied until all of the queries for the ViewModel have been executed?

Update: After some further research and experimentation I think that using something along the lines of a Deferred function may work. I have tried a few implementations, however it only defers until the query is called, rather than until all query results have been processed. I'm obviously doing something wrong here but my javascript foo is weak.

Technologies used: Entity Framework 5 w/ Oracle, .Net 4 Web API, Knockout 2.2, Breeze 0.71.3

The situation: Breeze is being used to call a Web API method which retrieves an Enumerable of POCOs, populates a knockout observable array, and the array is bound to a select control in the View.

The problem: The breeze queries have not completed and the knockout observables have not been populated prior to applying the ViewModel bindings to the View. When the query results are returned, the UI is unresponsive for 5 - 7 secs while the ko observable is populated and thus the select control is updated. Based on the logging this appears to be the issue...

The cshtml file:

<select data-bind="options: $root.brokers, value: 'id', optionsText: 'name'">
<script data-main="/BrokerCommission/Scripts/app/main" src="/BrokerCommission/Scripts/require.js"></script>

main.js:

requirejs.config(
    {
        // well-know paths to selected scripts
        paths: {
            'breeze': '../breeze.debug', // debug version of breeze
            'text': '../text'// html loader plugin; see http://requirejs.org/docs/api.html#text
        }
    }
);

define(['logger', 'text', 'breeze'], function(logger) {

require(['vm.muni'],
function()
{
    logger.info('applying bindings');
    ko.applyBindings(my.vm);
});

vm.muni is my ViewModel javascript file. Here's a method being exposed to exec a query:

getAllBrokers = function () {
        dataservice.getBrokers()
            .then(processBrokerQueryResults)
            .fail(handleQueryErrors);
    },

    processBrokerQueryResults = function (data) {
        logger.info("Start loading Brokers " + Math.round(new Date().getTime() / 1000));
        my.vm.brokers([]);
        $.each(data.results, function (i, d) {
            brokers.push(new my.Broker()
                .id(d.id)
                .name(d.name)
            );
        });
        logger.info("End loading Brokers " + Math.round(new Date().getTime() / 1000));
    },

Here is the breeze query from dataservice.js file:

function getBrokers() {
    var query = new entityModel.EntityQuery()
            .from("GetBrokers")
            //.orderBy("name");
    return manager.executeQuery(query);
};
1

1 Answers

5
votes

A few thoughts occur to me:

  1. I rarely push one-by-one into the observableArray. Too many DOM updates. Create a temporary array instead and update the observableArray with that. I believe this is the answer to the performance problem you're asking about.

  2. Set up an initialize method that returns a promise when all initial async load methods complete.

  3. Delay ko.applyBindings until the initialize promise resolves successfully.

  4. Consider showing a splashscreen + spinner while waiting for initialization to finish.

  5. Try not to do too much during initialization.

I can't show you how to do every one of these things in this answer. I'll show you #1 here and refer you "Fetch on Launch" in the documentation for #2. Web search + experimentation are your friends for the rest.

observableArray
processBrokerQueryResults = function (data) {
        ...
        var tempArray = []
        $.each(data.results, function (i, d) {
            tempArray.push(dtoToBroker(d));
        });
        brokers(tempArray);
        ...

        // I prefer named fns to long lambdas
        function dtoToBroker(dto) {
           return new my.Broker()
                  .id(d.id)
                  .name(d.name);
        }

I like to use the ECMAScript 5 Array.map syntax which returns an array after applying a function to each element of a source array. So I'd write:

processBrokerQueryResults = function (data) {
        ...
        brokers(data.results.map(dtoToBroker));
        ...
}