Todo Sample ViewModel Design

The Todo sample binds HTML widgets in a view to a JavaScript "ViewModel". The ViewModel could be the viewModel.js of our sample using KnockoutJS or the controller.js in our sample using AngularJS. Both work in essentially the same way and expose for binding virtually the same properties and methods. Both are located in the Scripts/app folder.

Query Todos

When the viewmodel is created, it calls its own getAllTodos method which delegates to the dataservice (the dataservice is a separate JavaScript component that handles all data operations using Breeze). When the query succeeds, the controller pours the retrieved TodoItems into its items array.

The model library (Knockout, Angular, ...) binds the items array to the unordered list in the view, using an <li> template. Suddenly todos appear on screen.

Checking the "Show archived" checkbox triggers a re-query of Todos.

If the checkbox is checked, the query retrieves every Todo, both the active and the archived. If the checkbox is clear, the query retrieves only the active Todos (those whose isArchived flag is false). Look at the dataservice getAllTodos method to see how this query condition is constructed.

Detecting and saving changes

This telling of the query story omitted an important step. Before adding a Todo to the items array, the controller calls extendItem on each one. The extendItem method adds an isEditing property to the Todo. The raw Todo entity doesn't have an isEditing property because there is no "isEditing" column in the database Todo table. We add this property here to help the UI toggle the presentation of the Todo between a readonly and an editable view:

The extendItem method also subscribes to the Breeze entityAspect.propertyChanged event which is raised when a Todo data property changes. The user can edit the Description or check the "Is Done" checkbox. Either change flows through to a Todo data property where it triggers a propertyChanged event and Breeze calls the subscription handler.

If you've finished editing the Todo, the subscription handler, after a brief delay (so the DOM can settle down), validates the modified entity and saves it. In other words, the Breeze propertyChanged handler saves the entity if any of its data properties change.

Adding a new Todo

As you enter the name of a new Todo in the big textbox at the top of the screen, that name is bound to the ViewModel's newTodo property. Leave that textbox and you trigger the ViewModel's addItem method which (a) asks the dataservice to create a new Todo entity, (b) fills in the new Todo's properties, (c) extends it with the isEditing property as just discussed, and (d) saves it.

Deleting a Todo

Clicking the "X" to the right of the Todo description triggers the ViewModel's removeItem method.

The removeItem method first removes the Todo from the items array, sets its Breeze EntityState to "Deleted", and tells the dataservice to save the Todo. Saving a Todo in a "Deleted" state deletes that Todo from the database.

"Mark all complete"

A change to the "Mark all as complete" checkbox triggers the ViewModel's markAllCompleted method which sets every displayed Todo's IsDone flag to true (checked) or false (unchecked). Normally, the propertyChanged subscription (mentioned above) would save each changed Todo individually. But this method intervenes by setting suspendItemSave before updating the isDone values. After updating all Todos, it tells the dataservice to save all of the modified Todos in a single batch.

Validation

Sometimes a changed entity fails validation. Breeze maintains validation rules for entity types. In this example, the Description is constrained by a minimum and maximum length rule, established by the [Required, StringLength(maximumLength: 30)] attribute decorating the Description property of the TodoItem class in the Model back on the server. Breeze.NET transmitted this rule to the client via metadata. The Breeze client picks it up and applies it here ... automatically.

This ViewModel wants to take charge of validation and the display of validation errors. So it validates the entity explicitly in several places and routes validation failures to its handleItemItemErrors method.

The handleItemItemErrors method logs the first error it finds which causes a red "toast" message to float up from the bottom right of the screen. Then it tells Breeze to "reject all changes" to the errant entity. So if you make the Todo Description too long, you'll see both a "toast" error and the Description revert to its previous state.

Archive completed

A message in the lower left tells you how many Todos are "left" to do. The moment any active Todos are completed, a button appears on the lower right telling you that you can archive these completed todos. That button is bound to the ViewModel's archiveCompletedItems method.

Like markAllCompleted, the ViewModel's archiveCompletedItems method suspends save while it sets the IsArchived flag of one or more Todos. Then it saves all updated Todos as a single batch.

The ViewModel's getState method handles the bookkeeping about how many active Todos are left and how many are archivable. This getState method is called by two message building methods: itemsLeftMessage and archiveCompleted.

In the Knockout-based viewModel.js, these methods are Knockout computeds that update the UI when Todos are added or removed from the items array or when any Todo's IsDone or IsArchived changes. It may seem like magic. But it's a consequence of the fact that these methods rely on getState() which evaluates the items array and every Todo's IsDone or IsArchived property ... all of which are Knockout observables. Knockout computeds respond to changes in every dependent Knockout observable, no matter how deep in the call chain.

In the Angular-based controller.js, Angular regularlly polls the controller's itemsLeftMessage and archiveCompletedMessage methods to freshen the messages on screen.

Purge and Reset

After playing with Todo for a while, you can accumulate a great many junk Todos. You can delete every Todo in the database with the "purge" feature or reset the database to it's initial condition with "reset".

The pertinent links below the Todo list are bound to the ViewModel's purge and reset methods which relay those requests to the dataservice after which the ViewModel re-queries the database and the session (in effect) begins anew.

Back to the main Todo Sample page