0
votes

I can't quite get my head around how to do this. I've started with Julie Lerman's "BreezyDevices" solution to learn with and I have used her javascript viewmodel as a base.

I have:

//properties and methods to expose via this class
var vm = {
    game: ko.observableArray([]),
    save: function () {
        dataservice.saveChanges();
    },
    reset: function () { dataservice.reset(getAllGames) },

};

at the top of the viewmodel and this will return each of my games in an array. All works fine. "Games" has related data which returns an array called "Sets" containing "ourscore" and "theirscore" as properties.

On my html page I want to bind the concrete database properties returned as part of the "game" entity but I also want to create a "result" property for each game which is computed based on a javascript function that loops through each set score and returns values accordingly.

I tried using the layout in the breeze "Todo" solution and set this up immediately under the code above:

initVm();

function initVm() {
    addComputeds();
}

function addComputeds() {
    vm.result = ko.computed(function () {
        var ourSets = getResult().ourSets;
        var theirSets = getResult().theirSets;

        if (ourSets == 0 && theirSets == 0) {
            return "No Result";
        }
        return (ourSets > theirSets ? "Won " : "Lost ") + "<b>" + ourSets.toString + "</b>-" + theirSets.ToString;
    });
}


function getResult() {
    var ourSets = 0;
    var theirSets = 0;

    vm.game().forEach(function (game) {
        for (var gs in game.Sets) {
            if (gs.ourScore > gs.theirScore) {
                ourSets +=1;
            }
            else {
                theirSets +=1;
            }               
        }
    });

    return {
        ourSets: ourSets,
        theirSets: theirSets
    };
}

but that seems to me as though it'll add a "result" to the viewmodel (vm) and not each game entity? Also when I run the code it doesn't error, but it doesn't create a "result" property anywhere I can see and just doesn't appear to be working.

Looking at it again as I add it in here I can see it's wrong as it needs to be dealing with each specific game entity to work out each result and not the array of games (so I need something in vm.games.result and not vm.result) but I am too new at this to understand how to address each individual game entity. My .net coding brain would have me pass each game entity in a loop to a function to return the result for that game but I don't know if that's how it works with breeze/knockout too.

I've googled everywhere but I just can't seem to find the relevant examples for my requirement so would really appreciate some pointers please!


@BeaverProj

I have a main.js file in which this happens:

(function (root) {
    var app = root.app;

    app.logger.info('Please wait... data loading');

    ko.applyBindings(app.gameViewModel, $("content").get(0));

    $(".view").css({ display: 'block' });
}(window));

Have now edited top section to this:

var vm = {
    game: ko.observableArray([]),
    save: function () {
        dataservice.saveChanges();
    },
    reset: function () { dataservice.reset(getAllGames) },
    result: ko.computed(function () {
        var gameRes = getResult();
        var ourSets = gameRes.ourSets;
        var theirSets = gameRes.theirSets;

        if (ourSets == 0 && theirSets == 0) {
            return "No Result";
        }
        return (ourSets > theirSets ? "Won " : "Lost ") + "<b>" + ourSets + "</b>-" + theirSets;
        })
};

"getResult" now references "app.gameViewModel.game().forEach(function (Game) {" instead of "vm..."

As before - no errors, but no results either. I still get the array of "game" but nothing else. The above viewmodel still seems wrong to me... The "result" should be attached to the game entity (vm.game) and not the vm - currently this will give vm.result and there is a result per game (so vm.game.result), not per array of games. This is why I'm wondering if I need to be extending the entity via breeze. I could do this in normal javascript but it seems that breeze or knockout should be able to do this far easier?

4

4 Answers

0
votes

A few things:

  • I'm fairly certain you will want your result computed function declared inside the view model definition. Just put it under the reset function.

  • In the code you have shared you haven't initialized your view model or shared it with knockout.js. You need a call somewhere like this:

    var gameViewModel = new vm(); ko.applyBindings(gameViewModel);

  • After that change, in your getResult() method you would reference gameViewModel (the instance) instead of the vm variable (class definition).

  • One last thing which shouldn't affect whether its working too much is that you should only make one call to getResult() in your result computed and assign the return value to a variable. Otherwise you are computing it twice which is a waste of resources and technically could change between calls.

0
votes

OK, so after much fiddling about I've made some progress. Sorry Ward (if you read this) - your documentation may be great for experienced coders but it's not good for noobs!)

Top of dataservice.js is now this:

var manager = new breeze.EntityManager(serviceName);
var store = manager.metadataStore;

var Game = function () {
    //this.result = "Won 3-2";

};

var gameInitializer = function (game) {
    //game.result = "Won 3-2";
    game.result = function () {
        return getResult(game);
    }();
 };

store.registerEntityTypeCtor("Game", Game, gameInitializer);

and the getResult function:

function getResult(game) {
    var ourSets = 0;
    var theirSets = 0;

    game.Sets().forEach(function (gs) {
        if (gs.ourScore() > gs.theirScore()) {
            ourSets += 1;
        }
        else {
            theirSets += 1;
        }
    }
    );
    if (ourSets == 0 && theirSets == 0) {
                return "No Result";
    }
    else {
            return (ourSets > theirSets ? "Won " : "Lost ") + ourSets + "-" + theirSets;
    }
}

This, apparently, is known in breeze as a "post-construction initializer" and seems like a good solution for my needs in this case as I don't need to do anything with the result, it's a straightforward standalone result of a calculation and is only used for display purposes. Nothing can change on the page that will affect the result, it's already happened.

What I still have no idea about is whether this is the most (or even only) way of doing this, or whether I could achieve a similar result with knockout or even standard javascript instead. Doing it within breeze appears to handle the passing of the game entity that's needed to then expose the related "Sets" entity. Just had to be careful to refer to properties with the parentheses added or it all failed.

I don't like the idea of having the dataservice littered with model-specific constructors, initializers and functions, however. To my mind, most of what I've done belongs in the "vm.game.js" file and not the generic "dataservice.js" file as it currently is.

I'm now going to attempt to shift the code over. It may take me some time to work out the references!

0
votes

your post-construction initializer is very similar to the answer to this problem which was to use the ko mapping plugin doc'd @ http://knockoutjs.com/documentation/plugins-mapping.html.

you would use the mapping config to add a create handler to append the results computed observable you wanted. something like

var mapping = {
  game : {
    create : function(options) {
            var game = options.data;
            game.results = ko.computed( function(){ 
              //your result sets calc here
            }  );

            return game;
        }
  }
}

even better would be to define your own GameVM type and just put it there.

function GameVM( GameData )
{
    var self = this;
    this.Sets = ko.mapping.fromJS( GameData.Sets );
    this.Results = ko.computed( function(){
         self.Sets()...
    } );

}

and a mapping of

 var mapping = {
      game : {
        create : function(options) {
               return new GameVM( options.data );
      }
    }

this is cleaner, and it makes your game VM more testable as it's a smaller unit to test.

checkout the mapping documentation to really see how it can help with your reset() calls via the custom update callback.

0
votes

Don't sweat it. Keep exploring. We are all learning.

I don't know if it's better to put this in the ViewModel (VM) or in the entity. There is no intrinsic right answer.

Let's assume you want it in the entity (because that's more interesting from a Breeze perspective). Next question: do you need it to be observable?

You wrote "it's a straightforward standalone result of a calculation and is only used for display purposes. Nothing can change on the page that will affect the result, it's already happened." That suggests that it need not be observable so you won't need a KO computed. Your most recent solution isn't even presenting it as a property; I'm guessing something in your VM is calling getResult().

This leads me to think that you might prefer registering a custom constructor and putting getResult on the prototype of that constructor:

var Game = function () { }

Game.prototype.getResult = function () {
  var ours = 0;
  var theirs = 0;
  this.sets().forEach(function (s) {
    (s.ourScore() > s.theirScore()) ? ours += 1 : theirs += 1;
  });
  return (ours || theirs) ? 
      (ours > theirs ? "Won " : "Lost ") + ours + "-" + theirs :
      "No result";
}

store.registerEntityTypeCtor("Game", Game); // ctor but no initializer

On the other hand, if you thought the scores could change and you wanted the screen to update as they did, you'd probably want to move the logic into a results KO computed. The ourScore and theirScore observables should keep the results computed up-to-date. I'm thinking out loud here, not having tried it.

You would define that results computed in an initializer rather than the ctor. Why? Because KO observables must be attached to instances, not to the prototype. If you defined it within the ctor, Breeze might try to serialize and change track it. Better to tack it on to the entity in the initializer; Breeze ignores entity members added by an initializer.

On further thought, maybe I'd keep the getResult method where it is in the prototype and write an initializer that added a ko computed ... like so (warning: not tested):

function gameInitializer(game) {
   game.results = ko.computed(function() { return game.getResults();});
}

store.registerEntityTypeCtor("Game", Game, gameInitializer);

And now for the serious point: should this logic be in the dataservice?

I would not have it in my dataservice for the very reason you state yourself. This kind of logic expresses concerns intrinsic to the model and has nothing to do with managing data access. It's OK to mush it all together in the dataservice in a demo. In "real" code one would factor it out into a model.js component (I think of Game as a Model component, not a ViewModel component) and load that script between the dataservice.js and the viewmodel.js script tags.