banner ad
We are excited to announce that the ASP.NET Forums are moving to the new Microsoft Q&A experience. Learn more >

BigShelf Code(Preview)

Exploring the Code

The rest of this document walks you through various implementation details.

Feature: Log In / Log Out / Authentication

This feature is implemented as an ASP.NET MVC controller, \Controllers\AccountController.cs, using ASP.NET’s standard FormsAuthentication API. We can skip over the details of this one because it uses purely traditional ASP.NET MVC techniques, it does not use Upshot, and is not a SPA in any way. Although it would have been possible to implement a “login” screen as a SPA, there is not much interactivity possible on such a screen, so it wouldn’t have significantly improved the user experience.

Feature: Catalog Browsing

When a user requests the application root URL, this is mapped via ASP.NET MVC to HomeController’s Index action. This renders its default view, \Views\Home\Index.cshtml, which includes:

  • _SortAndFilter.cshtml – a partial containing markup for the sort/filter UI
  • _Book.cshtml– a partial containing markup to be rendered dynamically for each book
  • _Paging.cshtml– a partial containing markup for the page controls UI
  • <script> tags referencing each of the client-side view model classes, including CatalogViewModel.js.
  • An upshot.metadata call, which emits a JSON representation of the server-side entity classes, so that Upshot knows how to interpret raw data from the server
  • A ko.applyBindings call, which makes Knockout attach a client-side viewmodel to the HTML document, and thereafter keep the two synchronized dynamically

To be clear, the initial HTML rendered by the server does not contain the actual book data. The initial HTML merely defines placeholder markup that will be used by the JavaScript code that runs when the page is loaded. The JavaScript code will make subsequent requests to the WebAPI endpoint to fetch the book data and render it dynamically. This is very typical for Single Page Applications. You’ll find the majority of the catalog browsing JavaScript logic in \Scripts\App\CatalogViewModel.js.

Rendering books The basic mechanism for fetching and rendering a page of books is:
  1. CatalogViewModel declares booksDataSource (an Upshot data source configured to retrieve Book entity data from the WebAPI endpoint, \Controllers\BigShelfController.cs):
  2. var booksDataSource = new upshot.RemoteDataSource({
        providerParameters: { ... URL etc here ... },
        ... other configuration here ...
    });
  3. CatalogViewModel also declares books, a Knockout observable array representing the output from booksDataSource queries.
  4. self.books = booksDataSource.getEntities();
  5. The main view, \Views\Home\Index.cshtml, uses a Knockout binding to indicate that the “book” markup should be rendered in a loop, once for each entry in the books observable array:
  6.     @Html.Partial("_Book")
    
  7. The partial, \Views\Home\_Book.cshtml, defines HTML markup to be bound to each entry in that array, e.g.:
  8.     ... other markup here ...
        <div class="details">
            <h3 class="title" data-bind="text: Title">
            ... other markup here ...
        
  9. The binding text: Title causes the book’s Title property to appear as the text content in the associated DOM element.

These four pieces are enough to query the WebAPI service for books and display the results on the screen. All Knockout bindings remain live for as long as the associated DOM elements remain in the document, so if a subsequent query reveals different data, any affected part of the UI will be updated immediately to match.

Client-side navigation

BigShelf uses client-side navigation to associate a URL with each UI state, so that the user can use the browser’s back/forward buttons to retrace their steps (without actually performing a full page load in the browser), or can bookmark and share locations. This is built on the History.js library, which internally uses the HTML5 pushState API for browsers that support it, and falls back on the HTML4 location.hash API for browsers that don’t.

To make all this work, CatalogViewModel defines a NavHistory object to represent the user’s navigation state:

self.nav = new NavHistory({
    params: { page: 1, filter: "all", ... etc ... },
    onNavigate: function (navEntry) {
        // Respond to the incoming sort/page/filter parameters
        // by updating booksDataSource and re-querying the server
    }
});

As you can see, NavHistory allows you to define which set of parameters may appear in the URL, along with default values for those parameters. It also lets you give an onNavigate callback function that will be invoked whenever the user moves to a new URL (e.g., by clicking “back” or “forwards”).

BigShelf responds to user navigation actions by updating the booksDataSource query parameters to match the requested page/sort/filter criteria, and then re-runs the query. This automatically updates the books observable array, which in turn automatically re-renders any affected part of the UI.

In fact, BigShelf only ever issues booksDataSource queries in response to user navigation. When the user, say, clicks on a different page number, CatalogViewModel does not directly re-query the database. Instead, it handles user navigation gestures by instructing self.nav to move to the corresponding new URL, which in turn causes the onNavigate callback to fire, which in turn requeries the data service. It’s extremely useful to maintain this one-way flow of commands, whereby data querying only ever happens in response to navigation events, because:

  • It means you don’t have to duplicate data querying code in both UI event handlers and onNavigation callbacks – there is only one place for data querying
  • You can be certain that the UI state always matches the URL (there is no way for these two to get out of sync), and so if the user bookmarks a URL or uses back/forwards, they are sure to end up in the place that they expect

Paging, sorting, and filtering

As you’ve just learned, booksDataSource is queried for new data only in response to client-side URL changes. So, how do the paging, sorting, and filtering controls make this happen?

To define which “sort” options exist, CatalogViewModel defines sortOptions:

self.sortOptions = [
    { id: "Title", text: "Title" }, 
    { id: "Author", text: "Author" }, 
    { id: "Rating", text: "Rating" }, 
    { id: "MightRead", text: "Might Read"}
];

Then, to present these options in the UI, \Views\Home\_SortAndFilter.cshtml renders them using a foreach binding:

<label>Sort by:
<ul> data-bind="foreach: sortOptions">
    <li> data-bind="text: text, 
                   click: $parent.selectSortOption,
                   css: { selected: $parent.nav.params().sort == id }">

As you may be able to guess,

  • The text binding tells Knockout to display the text property from each sortedOptions entry
  • The css binding tells Knockout to apply the CSS class selected only if the item’s id property matches the id parameter in the current URL (to highlight the current sort option)
  • The click binding tells Knockout to invoke a function called selectSortOption on CatalogViewModel whenever a sort option is clicked

The selectSortOption does not have to do any actual querying. Its responsibility is merely to use client-side navigation to update the sort parameter in the URL (and ensure the user is on the first page of results):

self.selectSortOption = function (sortOption) {
    self.nav.navigate({ sort: sortOption.id, page: 1 });
}

Now, as you learned earlier, onNavigate will fire and the rest of the querying/UI updating logic will run. This pattern keeps the URL in sync with the sort order shown in the UI, so the user can click “back” to return to their previous sort order.

The implementation of paging and filtering is much the same as sorting. The only difference for paging is that, to display page numbers as groups (e.g., items: 1-6, 7-12, 13-18, etc., instead of page: 1, 2, 3, etc.), CatalogViewModel makes use of a separate model class, GroupedPagingViewModel, which encapsulates the ability to return these grouped page numbers.

Flagging and Rating Books

The server-side Book class does not have Rating or IsFlagged properties. That’s because each user profile may have different rating/flag values for each book. In other words, there is a many-to-many relation between users and books defined by the FlaggedBook entity: each FlaggedBook associates a user profile with a book, and gives that user’s rating/flag values for that book.

This is relevant on the client too. To display the current user’s rating/flag values for each book, we have to fetch the FlaggedBook entities for the current user, and effectively join them to the book data at the point of display.

CatalogViewModel instantiates a FlaggedBooksModel, which in turn queries for all the current user’s flagged books, independently of which books are being shown on screen:

var flaggedBooksDataSource = new upshot.RemoteDataSource({
    ... service URL etc. here ...
}).refresh();
var allFlaggedBooks = flaggedBooksDataSource.getEntities();

Then, FlaggedBooksModel exposes getFlaggedBookProperty/setFlaggedBookProperty functions that allow a caller to retrieve or update the user’s flag/rating values for a given book. These functions hide the fact that a user may not yet have flagged or rated a given book, and simply return default values for unflagged/unrated books.

The client-side Book class, defined in \Scripts\App\Entities\Book.js, exposes Rating, IsFlaggedToRead and similar properties that internally make calls to getFlaggedBookProperty/setFlaggedBookProperty. This allows the UI markup to bind to Rating, IsFlaggedToRead, etc., without knowing or caring that the data comes from an entirely separate data source, e.g.:

<div class="details">
    <h3 class="title" data-bind="text: Title">by 

Because the properties are implemented as Knockout ko.computed properties, it does not matter that the data queries are all asynchronous: initially, the properties may hold no data, but as soon as the query for flagged books completes, the property values will be updated automatically and any affected UI will be redrawn.

Whenever the Rating property is modified by UI code (for example, the starRating binding knows how to respond to clicks on the “star” icons by updating the associated model data), Upshot will notice that a FlaggedBook entity property has changed, and will therefore synchronize this change with the server immediately.

Whenever the user clicks on a “Flag to read” button, this invokes the flagToRead function on the corresponding Book instance:

self.flagToRead = function () { self.IsFlaggedToRead(1) }

This sets the underlying FlaggedBook entity’s IsFlaggedToRead property to the value 1 (the value used in the database to indicate “might read later”). As always, Upshot will observe this change happening, and will immediately synchronize it with the server via WebAPI.

Feature: User Profile Management

When the user clicks on their username near the top of the screen, or otherwise requests the URL /Profile, this is mapped via ASP.NET MVC’s routing configuration to \Controllers\ProfileController.cs. The profile editing screen is implemented as a SPA, so the initial server-rendered markup is merely a template and does not contain the actual profile data.

Showing and editing the Name and Email Address

The primary JavaScript viewmodel class for the profile screen, ProfileEditorViewModel, is defined in \Scripts\App\ProfileEditorViewModel.js. To load and save the user’s profile entity, it defines an Upshot data source called profileDataSource:

var profileDataSource = new upshot.RemoteDataSource({
    ... other configuration, e.g., service URL, is here ...
    bufferChanges: true
}).refresh();

Two key points to note:

  • Because bufferChanges is set to true, Upshot won’t synchronize changes with the server until we explicitly ask it to. This is good, because we want the user to control when data is saved by clicking the “save” button.
  • Because the profile screen does not use client-side navigation (as there isn’t really any data structure to navigate through), we simply call refresh() so that Upshot loads data immediately. Unlike the catalog browser, this UI doesn’t need to call refresh() from within an onNavigate handler.

Next, since we only want to get a single profile entity matching the logged-in user (not an array of profile entities), ProfileEditorViewModel uses the getFirstEntity() function to extract a single object called profile from profileDataSource:

self.profile = profileDataSource.getFirstEntity();

Todo: getFirstEntity isn’t a standard (yet). We may need to rephrase this when we finalize the API.

Upshot returns all data asynchronously. So, profile will not initially contain any value – it will obtain a value only when profileDataSource finishes its underlying Ajax request. That doesn’t cause any difficulty for us, though, because profile is a Knockout “observable” value: whenever its contents change, any associated UI will be updated automatically. We can refer to it in the UI even before it is loaded.

The Name and EmailAddress properties are displayed in the UI by means of HTML form elements bound to those properties, e.g.:

<div class="fieldgroup">
    <label>Name</label>
    <input class="revertible" data-bind="value: Name, css: { modified: Name.Modified }" />
    <div class="reverter" title="Revert" data-bind="visible: Name.Modified, click: Name.Revert"></div>
</div>

To highlight whether or not the property has been modified, we use the css binding to cause the CSS class modified to be attached conditionally depending on Name.Modified. Note that the Modified and Revert sub-properties are attached to the entity in \Scripts\App\Entities\Profile.js.

After the user has edited Name or EmailAddress or both, the affected field(s) will be highlighted as edited in the UI because of those css bindings. The user can then click the “Save changes” button, which is bound to the following function on ProfileEditorViewModel:

self.save = function () { profileDataSource.commitChanges() }

This simply instructs Upshot to synchronize any buffered changes with the server. As soon as synchronization is complete, Upshot will set the Modified status of each property to false, which in turn causes the “modified” highlights to disappear from the UI.

Showing and editing the list of friends

The profile entity has a property called Friends, which of course is an observable array of Friend objects associated with the user’s profile. This makes it easy to display a dynamic list of friends in the UI:

  • ... markup for each friend here ...

As you can see, the view again uses the css binding to highlight Friend entries that have been modified (added or deleted) on the client and not yet synchronized with the server.

Todo: IsAdded and IsDeleted aren’t standards (yet). We may need to rephrase this when we finalize the API.

It’s fairly easy to delete a friend. If you click on one of the friends in the list, this is bound to invoke the toggleFriend function on ProfileEditorViewModel:

self.toggleFriend = function (friend) {
    var friendsDataSource = upshot.EntitySource.as(self.profile().Friends);
    if (friend.EntityState() === upshot.EntityState.Unmodified)
        friendsDataSource.deleteEntity(friend);
    else
        friendsDataSource.revertChange(friend);
}

The function doesn’t only delete friends. It also undeletes them (i.e., reverts the deletion) if they were already deleted.

The Friends collection is part of the main profile entity that, as you saw earlier, gets synchronized with the server whenever the user clicks “Save changes”. So, we don’t have to write any extra code to let the user save changes to their list of friends.

Adding new friends is slightly trickier, because we have to present some UI for choosing a friend to add. The design we chose for BigShelf is to use an autocompleting textbox. We could instead have used a drop-down list, but that would not scale well if there were thousands or millions of profile records in the database.

To provide data for the “friends” autocompletion menu, ProfileEditorViewModel defines another Upshot data source, possibleFriendsDataSource:

var possibleFriendsDataSource = new upshot.RemoteDataSource({
    providerParameters: { url: options.serviceUrl, operationName: "GetProfiles" },
    entityType: BigShelf.Profile.Type
});

Separately, the UI contains a textbox bound as follows:


This uses a custom Knockout binding called autocomplete (defined in \Scripts\App\bindings.js) that connects the textbox with jQuery UI’s autocomplete widget. It configures the autocompleter to fetch its list of suggestions from the function findFriends on ProfileEditorViewModel, which in turn supplies the user’s typed value to possibleFriendsDataSource as a filter criteria. Once Upshot has asynchronously retrieved new results, they are passed back into the jQuery UI autocomplete widget and displayed on the screen.

Later, when the user actually chooses an entry in the dropdown list (by clicking on it, or by using the keyboard arrow keys and pressing enter), this event is bound to the function addFriend on ProfileEditorViewModel. This is a little tricky, because it needs to do three things based on the profile object chosen in the autocomplete:

  • Copy the chosen profile data into profileDataSource’s data context using Upshot’s merge function. This is necessary because the chosen profile object comes from possibleFriendsDataSource’s data context: an entirely separate silo of data. It wouldn’t be possible to create a new Friend object referring to the chosen profile entity without first making that profile entity available in the correct data context.
  • Check whether or not the user’s profile already contains that Friend entity instance referring to the chosen user profile. If so, skip the next step to avoid creating a duplicate record. This is just an aspect of BigShelf’s design; a different application might choose to allow duplicate Friend records.
  • Create a new Friend record referring to the chosen user profile, and append it to the current user’s Friends array.

Once you add a new item in the user’s Friends array, it will show up in the UI immediately, because the foreach binding keeps the UI in sync with the underlying array. Plus, it will be highlighted in green to mean “added”, because of the css binding you saw earlier.

Finally, we don’t need any extra code to allow the user to save or revert these changes. You’ve already seen that the save function calls Upshot’s commitChanges function, which synchronizes the entire profile object graph with the server. Once changes are committed, the IsAdded property on the new Friend entities will change value to false, so the green highlights will disappear from the UI automatically.

Review

You’ve seen that BigShelf is not a trivial application: it has plenty of functionality, and indeed goes further than most applications in providing a rich UI:

  • The catalog browser, being rendered dynamically on the client and supporting client-side navigation, gives a faster, more immersive experience than you’d find in a traditional server-oriented or Ajax-style web application.
  • The profile editor, with its autocompleting friends picker, and its ability to track, highlight, and finally save or revert all changes, is more ambitious and more enjoyable for the user than you might expect in a web application.

Single Page Application architecture provides a solid pattern for such functionality. ASP.NET MVC, WebAPI, Upshot, and Knockout realize these goals robustly and flexibly. However, making this all work requires a different way of thinking than many web developers are used to:

  • Developers need to be (or become) comfortable with writing object-oriented JavaScript
  • Developers need to think of the UI as stateful, having more in common with a desktop application UI than a traditional web page
  • Developers who want to benefit from the MVVM pattern will need to exercise discipline in modeling all state as viewmodel data, and having the UI generated from that state. This is different from traditional jQuery-style web coding, in which the developer directly catches DOM events and makes ad-hoc changes to the DOM without having any underlying data model to represent what is happening.

Web UIs continue to evolve, becoming richer, more interactive, and more fun to use. Learning how to apply SPA architecture and libraries will help you.

This article was originally created on November 14, 2012

Author Information