In my previous post I explained my initial solution for my single page application using Knockout and Sammy on MVC3. This solution depended on calling ko.applyBindings whenever the view was swapped by Sammy. Calling ko.applyBindings multiple times on the same element is a bad idea because Knockout will create multiple duplicate bindings on the same DOM node, which we do not want.
Not happy with my previous solution, I set out to improve it. Some major changes to the previous solution:
- The shell page now has its own viewmodel that manages the viewmodels that are bound to the views.
- The views are no longer loaded dynamically (there was very little overhead in putting them in the shell page and the pagesize didn’t grow a lot)
- Sammy routing is moved entirely to the master viewmodel on the shell page. This allows us to control the viewmodels (set observables, call methods, etc.) based on the route taken.
Let’s look at some code. Here is the (simplified) shellpage viewmodel:
1: var app = function () {
2: var self = this;
3:
4: self.State = ko.observable('home');
5:
6: self.Home = ko.observable(new homepageVm());
7: self.User = ko.observable(new userInfoVm());
8:
9: $.sammy(function () {
10:
11: this.get('#/', function (context) {
12: self.State('home');
13: });
14:
15: this.get('#/info/:username', function (context) {
16: self.State('user');
17: self.User().UserName(context.params['username']);
18: self.User().LoadInfo();
19: });
20:
21: }).run();
22: };
It contains a State observable that will show and hide our views, and of course the viewmodels that bind to the views.
The following code will start the application, we create an instance of the viewmodel, which will start up Sammy and then bind the viewmodel to our container.
1: $(document).ready(function () {
2:
3: var app = new app();
4: ko.applyBindings(app, document.getElementById('container'));
5:
6: location.hash = '#/';
7:
8: });
To show and hide the views based on the state of the shell VM, we can now bind the views like this:
1: <div id="homeView" data-bind="with: Home, visible: State() === 'home'">
2: @Html.Partial("_Home")
3: </div>
4:
5: <div id="userView" data-bind="with: User, visible: State() === 'user'">
6: @Html.Partial("_Info")
7: </div>
The Knockout with: binding creates a new binding context for the views which allows us to bind directly to the subVMs observables without having to worry about the shell VM:
1: function homepageVm() {
2: var self = this;
3:
4: self.Name = ko.observable('InitialValue...');
5:
6: self.DoIt = function () {
7: self.Name('Changed!');
8: };
9: };
With this view:
1: <p data-bind="text: Name">
2: </p>
3: <button data-bind="click: DoIt">
4: Do test</button>
As you can see the view has no notion of the shell VM because the with: knockout binding creates the new binding context. Since the binding specifies with: Home the view is bound to the observables of the Home viewmodel, which is an observable of the shell VM.
So, no more calls to ko.applyBindings which is good and we gained more control over the viewmodels that are loaded by the shell.
Bonus feature of this approach:
You could bind a DOM element in the homeView to a property in the User viewmodel using a binding that looks like this:
1: data-bind="text: $root.User().UserName"
This binding uses the root object, in this case, the shell VM and takes the UserName observable from the User viewmodel. Not sure where this might come in handy, but its nice to have.