This post is part of a series about how some engineers at Red Gate spent a week building an end-to-end Nomad for Visual Studio sample. You can find the rest of the posts using the dtw-crm-sample tag.
Now that we have a login screen and a search page that both work in isolation, we need to tie them together and provide a way for the user to transition between them.
This is the part of the application that I found trickiest to do and I couldn’t really find a great way to structure things. At its simplest, JQM provides a really nice way to switch between pages in your application:
$.mobile.changePage(url);
The reason we couldn’t use this simple function is because the page that is going to be loaded needs to have some bindings applied using the Knockout.js function:
ko.applyBindings(viewModel);
but where should we make that call? We have a few options:
Destination page constructs view model and calls binding
In the HTML for the destination page, it could know how to construct the view model and register it with Knockout.js
This doesn’t work in our case though as I want to be able to construct the view model for the page that is being loaded with information that exists on the current page. For example, I want the search page that is being loaded to know what server it should talk to and that is information that must be passed through from the Login page.
We could use the query string to pass along parameters but it just seemed ugly, especially when you want to pass on complex objects.
Bind an event handler to fire after destination page has rendered
We could bind an event handler attached in the original page that says when then second page appears, call Knockout.js with a specific view model that has already been constructed. This looks like:
$(‘#SearchPage’).live(‘pageshow’, function (event, ui) {
ko.applyBindings(new SearchPageViewModel(server, user, password), document.getElementById(“SearchPage”));
});
$.mobile.changePage(‘searchPage.html’);
This is the approach I went with initially. In the LoginViewModel I attached an event handler that said when a <div> with the id ‘SearchPage’ appeared, then Knockout.js should be called to bind a SearchPageViewModel() to it.
This actually worked quite well but unfortunately, it starts to fall apart when you transition back and forth between pages. Consider the following flow:
Login Page – User presses the login button
Attach the event handler and call transition page
The Search Page is brought into view and the bindings are applied
Search Page – User presses the back button
App transitions back to the login page
Login Page – User presses the login button
Attach the event handler and call transition page
Now there are two event handler registered looking out for the search page <div>
The search page is brought into view and the bindings are applied twice as the event handlers’ fire.
This makes Knockout extremely unhappy.
A Solution: Introduce some global state
The solution I actually ended up going with was to introduce a global object that was responsible for managing the lifetime of view models and transitioning between pages. I would be really happy to get feedback on this approach as I couldn’t find much on the web about a nicer way to architect Cordova applications using Knockout.js and I would be keen to hear about alternative approaches to dealing with the issues.
Instead of calling $.mobile.changePage() directly, all my view models will call PageStateManger.changePage(url, viewModel) which will be responsible for changing to that page and applying the view model. You can see the full code on GitHub
It’s a pretty simple approach, basically:
When the global PageStateManager is created, it registers a single event handler which will listen for the JQM ‘pagechange’ event
When changePage is called by a source view model, it does two things:
it stashes away the view model that it has been given for the target page in an array – this is the global state that is maintained that I don’t see a way to get away without.
It then calls the built-in JQM changePage() function
When JQM has grabbed the page and inserted it into the DOM, it will call the ‘onpagechange’ event handler with a reference to that <div>. Each of the pages in the app are marked up with a data- attribute instructing the PageStateManager which view model to go and get from the global cache and apply. I could have done this using the URL of the page as the key, but I found that using the data-viewModel attribute was very comfortable coming from WPF where each UI element can define a <DataContext>
Now forward transitions are taken care of, and when transitions are initiated by something other than code, such as the back button, it will still work because the same ‘onpagechange’ event will fire and the PageStateManager can go and fetch the necessary ViewModel based on the data-viewModel attribute of the page.
All in all, this isn’t the most disgusting code I have written as I can’t see how to do without at least some global state. If there is anyone out there that has a nicer way deal with the issues, I would be really happy to hear from you.