Todo Sample Dataservice
The dataservice only has five public methods:
|getAllTodos||Query for Todos, either all Todos or just the active (non-archived) Todos.|
|createTodo||Create a new Todo and add it to the manager.|
|saveChanges||Save the Todos in cache with pending changes (the Todos to be added, modified, and deleted).|
|purge||Delete all Todos in the database and clear the cache; purely for the demo.|
|reset||Reset the database to its initial set of Todos and clear the cache; purely for the demo.|
These are the five methods called by the application ViewModel (or Controller) that manages the screen. We'll drill a little deeper into them in a moment after we've covered the setup code at the top of the file.
A Breeze application can be configured to work with a variety of remote services, to use a custom Ajax helper, and to create entities shaped to support a variety of model binding libraries such as Knockout, Backbone, and Angular.
Out of the box, Breeze is ready to talk to an ASP.NET Web API service, using jQuery's Ajax component, and it will create entities for us with Knockout which means that the entity properties will be Knockout observables.
The most basic Todo sample uses all three defaults; its dataservice needs no special configuration.
The Todo-Angular sample differs only it its choice of model library (AngularJS) so it needs one extra line of configuration:
breeze.config.initializeAdapterInstance("modelLibrary", "backingStore", true);
The "backingStore" library is Breeze's native model library which happens to be well suited to support Angular applications.
Every Breeze application needs an instance of the Breeze EntityManager class to access the remote service and to manage entities in cache. Here we create one called manager targeting the Todo Web API service whose endpoint is "api/todos".
You'll see an alternative endpoint that is commented out:
// Cross origin service example //var serviceName = 'http://todo.breezejs.com/api/todos';
That's the endpoint of a compatible service run by IdeaBlade on its own servers. Leave that be for now; it comes into play in a demonstration of cross-origin resource sharing (CORS) [FORTHCOMING].
We'll examine the five public methods in a bit more detail.
The ViewModel acquires a fresh set of Todos from the database by calling the getAllTodos method. It creates a Breeze query object targeting the Web API controller's "Todos" action method. The query is embellished with an orderBy clause that sorts the Todos by creation date. Note that the sort takes place on the database, not on the client.
The caller passes in a boolean indicating if the query results should include archived Todos. Usually you don't want the archived Todos so the value is false. The Todo application binds this value to the "Show archived" checkbox which is unchecked (false).
If includeArchived is false, the getAllTodos method modifies the query object with a "where" clause that filters the Todos - on the database, not the client - so that only active Todos (those whose IsArchived flag is false) will be retrieved.
Finally, the manager executes the query asynchronously and immediately returns a promise. It's a promise to deliver either the query results or an error when the server eventually replies.
See how the ViewModel (or Controller) which calls this method handles that promise.
The manager quietly retrieved the metadata from the Web API controller just before performing its first query. The manager holds that metadata in its MetadataStore.
The createTodo method relies on the manager's createEntity method to
- instantiate a new TodoItem
- extend it with properties defined in metadata
- set some of those data with initial values (if provided),
- add the new TodoItem to the manager.
The new TodoItem instance is shaped to suit the application's model library. If this is a Knockout application, the properties of the Todo are Knockout observables. If it's an Angular application, it has ECMAScript 5 properties with getters and setters.
The Todo also has a Breeze EntityAspect. This is a doorway to important features of every Breeze entity. The ViewModel calls upon two of these features: the propertyChanged event and the setDeleted method. The propertyChanged event is raised when a data property changes; the ViewModel listens to that event to learn when it should save those changes. If the user clicks the "X" next to the Todo description, the ViewModel will calls the setDeleted method to put the Todo in a "Deleted" state and then saves this change; on the server, entities marked for delete are removed from the database.
You'll find a few more specifics of this sample's implementation in the "Add a new entity" step of the Basic Breeze walk through.
After user input, the ViewModel could call the dataservice saveChanges method. If there is nothing to save, the method bypasses the manager and logs a "Nothing to save" message (unless told to skip that step).
Usually the manager's cache holds some kind of change: a new Todo, one or more modified Todos, or a Todo marked for deletion. Ultimately this method will call the EntityManager's saveChanges method.
But there is a wrinkle. Saving changes is an asynchronous operation. It takes time to complete. While waiting for the server to respond, the use could make another change and call the dataservice's saveChanges method again.
That could be a problem, especially if the previous save involved a new Todo. Until the first save completes, that new Todo is still sitting in cache. If we allowed the manager to save these changes again, it would save the new Todo a second time. We'd have duplicate Todo items in our database.
Breeze typically guards against this by throwing an exception if you call saveChanges while it is waiting for a response to an earlier save. If you overide that guard (as you can), you expose the application to the risk of a duplicate Todo.
This method tries an alternative gambit. It remembers if a save is in progress (the _isSaving flag). If a second save comes in before the first completes, it postpones that second save for 50 milliseconds (using setTimeout). The postponed saves won't be processed until there are no saves waiting a server response.
Note that the dataservice saveChanges method does not return a promise to the calling ViewModel. From the ViewModel's perspective, saveChanges is "call-and-forget".
Internally the service does wait for the promise.If the save succeeded, it logs the happy news. If the save failed, there's some tricky processing. Whether the save succeeds or fails, the _isSaving flag is reset so the next save request can be processed.
This private method analyzes the save failures. If the save failed validation it calls handleSaveValidationError which composes and displays an error message. The user can fix the problem (e.g., a description that is too long) and re-save.
If the server returned a concurrency exception, there's a good chance that some other user deleted the Todo or perhaps reset the database. A more sophisticated application would do something more clever. This sample simply rejects all the pending changes (manager.rejectChanges) and leaves it up to the user to decide what to do next.
The application is unable to cope with any other form of error. Maybe the server is down or the network is down. Who knows. It throws up its hands and suggests that the user reboot.
purge and reset
These are demo support methods. You wouldn't purge or reset your database in a real application.
They are interesting in one respect: they use jQuery AJAX to post directly to the corresponding Web API controller methods; they aren't using Breeze for this communication.
Why a dedicated dataservice?
Of course you could merge this data access component into the ViewModel (or Controller) that binds to the HTML widgets on screen. The ViewModel could create a Breeze EntityManager and call its executeQuery and saveChanges methods directly.
But we highly recommend keeping the data access and the Breeze particulars in a separate module of its own. ViewModels are much easier to maintain when they have less to do. In a multi-screen app, several ViewModels will surely share a common dataservice and its cache of entities.