Domanda

This code returns a config object, and eventually populates it with knockout observables.

define("config", [], function () {
  var config = {};
  $.ajax({
    url: "api/Config",
    success: function (result) {
      for (pname in result)
        config[pname] = ko.observable(result[pname]);
    },
  });
  return config;
});

It works in terms of populating the config object with settings, but it doesn't finish until after one of my views has applied bindings, and this causes problems at runtime.

How would you set this up to wait on the result of the ajax query before returning the config object? My first thought was using a promise object, but I can't see how to apply it.

I'm not looking for a workaround, I already have one:

define("config", [], function () {
  var config = {
    IsReady: ko.observable()
  };
  $.ajax({
    url: "api/Config",
    success: function (result) {
      jquery.extend(config, result);
      config.IsReady(true);
    },
  });
  return config;
});

With this setup I bind like this

<img data-bind="attr: { src: config.IsReady() ? config.Logo : '' }" />

But I don't want to pollute my views with that rubbish, so I want to know how to wait on an async operation before returning from a factory method.

Trying to apply advice received so far, I turned things inside out. Because result is now in scope and ready to use when define is called, I simply pass it directly to define.

$.ajax({
    url: "api/Config",
    success: function (result) {
      define("config", [], result);
    }
});

//the following code should not execute until config has been defined.

define(['durandal/system', 'durandal/app', 'durandal/viewLocator'], 
       function (system, app, viewLocator) {
  //>>excludeStart("build", true);
  system.debug(true);
  //>>excludeEnd("build");

  app.title = 'Jumbo File Transfer';

  app.configurePlugins({
    router: true,
    dialog: true,
    widget: true
  });

  app.start().then(function () {
    //Replace 'viewmodels' in the moduleId with 'views' to locate the view.
    //Look for partial views in a 'views' folder in the root.
    viewLocator.useConvention();

    //Show the app by setting the root view model for our app with a transition.
    app.setRoot('viewmodels/shell', 'entrance');
  });
});

This runs but leaves the question of how to make the rest of the application wait till this has happened. So I put the rest of main.js inside the success function, like this.

$.ajax({
  url: "api/Config",
  success: function (result) {
    define("config", [], result);
    define(['durandal/system', 'durandal/app', 'durandal/viewLocator'], 
           function (system, app, viewLocator) {
      //>>excludeStart("build", true);
      system.debug(true);
      //>>excludeEnd("build");

      app.title = 'Jumbo File Transfer';

      app.configurePlugins({
        router: true,
        dialog: true,
        widget: true
      });

      app.start().then(function () {
        //Replace 'viewmodels' in the moduleId with 'views' to locate the view.
        //Look for partial views in a 'views' folder in the root.
        viewLocator.useConvention();

        //Show the app by setting the root view model for our app with a transition.
        app.setRoot('viewmodels/shell', 'entrance');
      });
    });
  }
});

This actually does execute in the right order - I stepped through it. But the app fails to start. If I had to guess why, I would say define wants this to be the global context.

Nathan's answer below doesn't stop Durandal from starting properly but the behaviour of the config define isn't quite right. I need config defined as an object full of settings properties, not an object with a factory method. But we're nearly there. It just needs to look like this:

define('configFactory', ['plugins/http'], function (http) {
  "use strict";
  var getConfig = function () { 
    return http.get("api/Config").then(function (data) { return data; });
  };
  return { getConfig: getConfig };
});
define(['durandal/system', 'durandal/app', 'durandal/viewLocator', 'configFactory'], 
       function (system, app, viewLocator, configFactory) {

  //>>excludeStart("build", true);
  system.debug(true);
  //>>excludeEnd("build");

  app.title = 'Jumbo File Transfer';

  app.configurePlugins({
    router: true,
    dialog: true,
    widget: true
  });

  configFactory.getConfig().then(function (config) {
    define('config', [], config);
    app.start();
  }).then(function () {
    //Replace 'viewmodels' in the moduleId with 'views' to locate the view.
    //Look for partial views in a 'views' folder in the root.
    viewLocator.useConvention();

    //Show the app by setting the root view model for our app with a transition.
    app.setRoot('viewmodels/shell', 'entrance');
  });
});

In hindsight

You can make ajax block by setting the async option to false, like this.

var foo;

$.ajax({ 
  url:"whatever", 
  async: false
}).done(function(result){ 
  foo = result;
});

//code that needs result

However, introducing a bottleneck is seldom a good idea.

Here's the fundamental problem: the below will fail because config doesn't have a property foo until after the first rendering pass.

<span data-bind="text:config.foo"><span>

We could synchronously load config in main, but a better answer is to defer binding until foo is available.

<!-- ko if: config.foo -->
<span data-bind="text:config.foo"><span>
...
<!-- /ko -->

You don't need to be explicit like this in the template for a foreach binding, since the template is rendered for each instance. You need only pre-declare the observableArray.

È stato utile?

Soluzione

I would suggest using the http plugin in durandal to handle this.

define(['plugins/http', 'services/logger'], function (http, logger) {
    "use strict";
    var getConfig = function () {
        return http.get("api/Config").then(function (data) {
            return data;
        });
    };
    return {
        getConfig: getConfig
    };
});

It should then be a matter of doing

define(['durandal/system', 'durandal/app', 'durandal/viewLocator', 'config'], 
       function (system, app, viewLocator, config) {
  //>>excludeStart("build", true);
  system.debug(true);
  //>>excludeEnd("build");

  app.title = 'Jumbo File Transfer';

  app.configurePlugins({
    router: true,
    dialog: true,
    widget: true,
    http: true
  });

  config.getConfig().then(app.start()).then(function () {
    //Replace 'viewmodels' in the moduleId with 'views' to locate the view.
    //Look for partial views in a 'views' folder in the root.
    viewLocator.useConvention();

    //Show the app by setting the root view model for our app with a transition.
    app.setRoot('viewmodels/shell', 'entrance');
  });
});

Edit
I think you just need to add what you had in your done function of the ajax call to get what you need.

define('configFactory', ['plugins/http'], function (http) {
  "use strict";
  var getConfig = function () { 
    return http.get("api/Config").then(function (data) { 
        var config = {};

        for (pname in data){
             config[pname] = ko.observable(data[pname]);
        }

        return config; 
    });
  };
  return { getConfig: getConfig };
});

Altri suggerimenti

This is a usecase for promises. I'm not sure about javascript's cross browser support for this, but it seems to be like in other async languages. Full raw javascript example on this page

If you use jquery, it gets even easier:

$.ajax({
  url: "http://fiddle.jshell.net/favicon.png",
  beforeSend: function( xhr ) {
    xhr.overrideMimeType( "text/plain; charset=x-user-defined" );
  }
}).done(function( data ) {
  if(console && console.log ) {
    console.log( "Sample of data:", data.slice( 0, 100 ) );
  }
});

JQuery Docs about Ajax Promises

You can't return config like you are. You simply can't. The ajax call is asynchronous so it will return immediately and you will return an empty config object. The only way to handle this with asynchronous ajax is to design for asynchronous code. You can either use promises or you can call a function from your success handler and pass it the now finished config object. Either way, you're going to get the finished config object in a callback function.

define("config", [], function () {
  var config = {};
  $.ajax({
    url: "api/Config",
    success: function (result) {
      for (pname in result) {
        config[pname] = ko.observable(result[pname]);
      }
      // call your function now and pass it the completed config object
      callMyFunction(config);
    }

  });
});

You may think this is a work-around, but it is how you code for asynchronous operations.

There is a simpler answer.
If you make your config observable, then you can update it later:

define("config", [], function () {
  var config = ko.observable({}); // 1. Make config observable
  $.ajax({
    url: "api/Config",
    success: function (result) {
      //for (pname in result) {
      //    config[pname] = ko.observable(result[pname]);
      //}
      config(result); // 2. Replace the existing config object
    },
  });
  return config;
});

So your html would be updated to:

<img data-bind="attr: { src: config().Logo }" />
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top