Build Single Page Apps - Part 8 - JavaScript Data Services

I love to write code, but that doesn’t mean I want to write the same code over and over again. And I certainly don’t want to have to hunt down all that redundant code when I want to refactor a few pieces of it. That’s why I am a huge advocate for the Single Responsibility Principle (SRP) … or in simplest terms, I prefer to make smaller sets of code that have a primary focus. In JavaScript, that means modules like the ones below.

You’ll see examples of modules such as Models, a Data Service and a Data Context in my upcoming Pluralsight course titled “Building Single Page Apps (SPA) with HTML5, ASP.NET Web API, Knockout and jQuery”. (due to go live on Pluralsight in a week!).

You can catch up on the previous posts in this series here:

More on the Code Camper SPA

Part 1 - The Story Begins (What is the Code Camper SPA?)

Part 2 - Client Technologies

Part 3 - Server Technologies (the Data Layer)

Part 4 - Serving JSON with ASP.NET Web API

Part 5 - HTML 5 and ASP.NET Web Optimization

Part 6 - JavaScript Modules

Part 7 - MVVM and KnockoutJS

Part 8 - Data Services on the Client

Part 9 - Navigation, Transitions, Storage and Messaging

Part 10 - Saving, Change Tracking, and Commanding

Part 11 - Responsive Design and Mobility

Data Context

In the previous post (part 7) I discussed how I prefer the data binding techniques of KnockoutJS and the MVVM pattern in JavaScript. You can go check out an example of a ViewModel in that post. The ViewModel has a method named getSpeaker which defers its data retrieval to a datacontext module. It goes something like this:

datacontext.persons.getFullPersonById( currentSpeakerId() );

 

All view models in the application need data. When they need to get it or save it, they turn to the datacontext module who is responsible for getting or saving the data. Sound simple? Well, that’s the point. This data context module is like a client side repository.

The view model doesn’t care how the data is retrieved, it doesn’t care if its going to require an asynchronous call or not, it just wants the data. The datacontext hides all of the logic to retrieve (or save) the data so the view models just interact with the datacontext’s API.

Smart Caching

The datacontext in my app is smart enough to hang onto data once it gets it. When a request comes in for data, the datacontext checks if it has the data already. If so, it returns it. If not, it goes and gets it via an Ajax call (actually it defers the retrieval to a Data Service module … but more on that in a minute).

The datacontext’s getData method looks something like this (I shortened it just to get the main points across)

getData = function(...) {
return $.Deferred(function(def) {
// If the internal items object doesnt exist, 
// or we force a refresh
if (forceRefresh || !items) {
getFunction({
success: function(dtoList) {
mapToContext(...);
def.resolve(results);
},
error: function (response) {
logger.error(config.toasts.errorGettingData);
def.reject();
}
}, param);
} else {
itemsToArray(...);
def.resolve(results);
}
}).promise();
}

Consistent Expectations

Pay particular attention to the $.Deferred in this code. This function always returns a promise to the caller (the view model in my example). This provides a consistent expectations to the caller whether the data was retrieved from cache (which doesn’t require an async call) or an async Ajax call to the Web API on the server. This makes it easy to call the code and test it. (Yup, there’s unit tests in my app.)

Data Service

I mentioned that the data context handles getting and saving the data. That’s true, but more specifically it handles the job of figuring out how whether it has the data or not, and then if it does not have the data it asks the dataservice module to go get it.

The Data Service module (aka dataservice) makes the request for the data from whatever web service it needs to talk to. It has a simple interface so the datacontext can simply make a request like this:

dataservice.person.getPersons()

Remember that getFunction call up in the datacontext code we just looked at? If not, go look. I’ll wait.

That getFunction call is a delegate for the dataservice.person.getPersons (for example). So if we call it directly we could do it like this:

dataservice.person.getSpeakers({
success: function() { ... },
error: function() { ... }
});

The Payoff

This abstraction of data services and data contexts makes it simple to make calls for the same data from multiple places (including view models). So I reap the benefits of:

  1. Code re-use and less repeating myself (DRY)
  2. Testable modules
  3. Easier maintenance (when refactoring)

In the course I go deeper into these topics and how the data service promotes the use of switching between sample (or mock) data and live data. There are also modules for model factories for the observables (if you so choose to create them).

Make it Your Own

I’m very excited to see how people refactor the sample app I include in the course (for Plus subscribers) to add their own ideas. There are so many other angles I would love to have hit, but I made the decision early on to choose one path and explain it.Otherwise there was too much risk of getting myself derailed. The course follows one option for building a SPA but I also can see myself expanding on it for a follow up later. Sigh … but first, I’ll finish this series (next up, navigation) … then I need a vacation

 

UPDATE:

I wrote myself a reminder (and promptly forgot) to mention 2 libraries that could replace my datacontext module. I am looking forward to when they go to their final release stage: Upshot.js and BreezeJS. Upshot (by the Microsoft ASP.NET team) has been around for a while in pre-release form, but it’s not at a point where I feel comfortable using it just yet. However it has a lot of potential and I am all over it once it is released. The other is BreezeJS, by ideablade. These guys have been in this space before with WinForms, Silverlight, WPF and are BreezeJS is there new JavaScript offerring. Breeze is not out yet but will be here soon, I am told. Neither is something I would use today, but both are intriguing enough where it could dramatically simplify (and reduce or eliminate) my datacontext module with the same and even more functionality. I’m all for “better”!