A Single Page Application (SPA) is a different way of building HTML 5 applications from traditional Web page development. Instead of spreading the functionality of your Web applications across a collection of separate Web pages with hyperlinks between them, you instead define a single root page that the user lands on and never leaves as long as they are working with your application. You define client-side logic that switches out the data and chunks of content within that page to allow the user to navigate from logical screen to logical screen without ever leaving the page. This means that the user never sees a full page refresh while using your application. They see portions of the screen change based on their interaction, and those changes can be done in a more fluid way with transitions to enhance the user experience. You can also support using the application while offline by storing data client-side, based on some of the newer APIs of HTML 5. Taking this approach makes an SPA feel very much like a desktop application to the end user.
A good example is the Gmail Web client. You land on the main page and you can view your inbox, switch to other folders (“Labels” in Google-speak), compose emails, view your contacts and tasks - all without leaving the page you landed on. To do all that, you need not only the mechanisms that do the navigation between logical chunks of content within your main page, but you also need some good client-side support for making service calls, getting data to the client side and caching it there, allowing the page to present it, change it, validate those changes, and submit changes back to the server side.
This article focuses on that data support, which is what the Upshot library that is part of the ASP.NET SPA stack provides. I’ll also touch on some of the other libraries you might use to support the navigation and view definition parts, as well as the server-side support in the ASP.NET SPA stack.
Single Page Application Concepts
You need to know few things about the architecture of SPAs and the capabilities of Upshot before you can dig into some code. The general architecture of an SPA in Figure 1 shows where Upshot fits in the client-side architecture, and the data services in that diagram are supplied by the DataController services or by WCF RIA Services DomainServices.
A Shift of Responsibility
If you build a traditional ASP.NET Web application, you rely on post-backs and server code to figure out what to do when the user navigates and interacts with your application. You handle all the data manipulation and interaction logic on the server side. When you build an SPA, you put all of that on the client side in JavaScript that runs in the browser. For .NET programmers, that may sound like crazy talk.
The idea of writing piles of JavaScript code to handle all the stuff that ASP.NET makes easy for you, plus all the interaction and data manipulation logic that you are used to writing in C# or Visual Basic, probably sounds like it is a great way to extend your project timeline and get your project cancelled. But the HTML world has evolved quite a bit from the traditional model of Web development. You can build HTML 5 applications with the support of a number of client-side JavaScript libraries that provide functionality, making it much easier to write portions of the functionality on the client side to enable a much richer and responsive user experience. These same approaches are also enablers for structuring your application as an SPA.
Data Management in SPAs
You also need to change the way you manage data when building SPAs. In a traditional ASP.NET Web application, you do your data retrieval and modification in the page-handling code on the server, then render out a new page containing the data or the controls to enter changes to the data. When you build a traditional application like this and you are constantly navigating from page to page, the browser sets a new execution context on the client side with each page switch. As a result, you have no opportunity to retain state in memory in order to have an ongoing interaction with the same set of data across multiple user navigation steps (e.g., show data listing > edit details > drill down to child details > edit > save > return to listing). In an SPA, you can retain references to data in JavaScript on the client side because the user never leaves the main page while running your application. With HTML 5, you also have the opportunity to persist that data on the client side across multiple user sessions and even run the application and access the data while offline.
You get your data in an SPA via service calls made from the client-side JavaScript. The initial root page rendering from the server contains the root structure of the page, as well as possibly an initial data set. But the first thing the application usually does is make an asynchronous (AJAX) service call from a script to get the latest data to support the current child views presented by the root page. Then, as the user navigates or interacts with the page (e.g. switching from inbox view to contacts view in a mail client), subsequent service calls are made to refresh the client-side cache of the appropriate data, and the UI updates without a full page refresh.
This is where Upshot comes in - it’s a client-side library that implements a Unit of Work pattern (http://martinfowler.com/eaaCatalog/unitOfWork.html) for working with data resources on the server side. Upshot allows you to pull data to the client side, cache it, lets your client script make changes to the data, validates those changes, and makes the calls to pass the modified data back to the server side.
What’s in the Box?
ASP.NET Single Page Application is the official name of the capabilities as of the beta release of ASP.NET MVC 4. The stack includes both the client-side support (the Upshot JavaScript library) and some server-side support (to define the services that Upshot can consume). Upshot is not limited to building SPAs with HTML 5. Even a page-navigation-oriented Web application could use it to retrieve and update data within different pages in the application. In fact, the demo code in this article does not involve any complicated navigation within a single page; it covers the basic usage patterns of Upshot and the services it consumes, which are more focused on data manipulation on the client side than how the application is really structured at a UI level. Upshot is part of the ASP.NET SPA stack because you need the kind of data support it provides for SPAs.
The services that Upshot can consume include WCF RIA Services DomainService classes as well as services defined on the new ASP.NET Web API stack. The WCF RIA Services stack has been available for a while and you can read all about it in my 10-part series at http://www.silverlightshow.net/items/WCF-RIA-Services-Part-1-Getting-Started.aspx.
WCF RIA Services was originally designed primarily for Silverlight clients. Upshot extends the reach of RIA Services to include HTML 5 clients as well. The sample code in this article doesn’t use WCF RIA Services, but instead uses a new way of achieving the same thing by defining services derived from the DataController class that is part of the SPA stack on the server side.
So the SPA stack includes three things: the Upshot library for the client side, the server-side DataController and metadata support classes, and tooling (project and item templates) to write the code faster.
The SPA stack includes three things: the Upshot library for the client side, the server-side DataController and metadata support classes, and tooling.
Getting Started with ASP.NET Single Page Applications
Let’s start on the server side to see how to build ASP.NET SPAs. You need some services for the client side to consume before you can see how to use Upshot. The first thing you need is some data to work with, because the core capabilities of Upshot and DataController services is on implementing a Unit of Work pattern around data retrieval and update (CRUD).
A DataController is a class built on top of the ASP.NET Web API for exposing HTTP Web Services. They support handling URI-based routing and parameters, HTTP verb handling (GET, PUT, POST, DELETE), content negotiation (determining the format of the message based on Content-Type, Accept HTTP headers, and media types), and automatic deserialization into strongly typed model objects. The DataController class can be directly derived from if you need full control of the data access approach you are going to use, or you can use the DbDataController or LinqToEntitiesDataController derived classes as the base class for the controller you define. DbDataController works with Code-First Entity Framework models and LinqToEntitiesDataController works with EDMX data model classes. These controller classes use a common infrastructure with MVC page-oriented controllers, but are used to define something that will be exposed to the client side as an HTTP Web API - a non-SOAP formatted Web service.
Creating the Project
Create a new project using the ASP.NET MVC 4 Web Application project template. In the dialog that is presented containing multiple kinds of MVC 4 projects, select an Empty project. I am not using any SPA project templates for this article because their status is in flux at the time of writing.
The Empty project does not have the assemblies and scripts you need to build an SPA, so you need to get a NuGet package. You can either do this from the NuGet Package Manager Console (View > Other Windows > Package Manager Console menu in Visual Studio) by issuing the command Install-Package SinglePageApplication or using the Manage NuGet Packages UI from the Solution Explorer context menu and finding the package for SPAs. If you don’t see these available in your environment, you need to install the NuGet Package Manager extension to Visual Studio 2010 from the Extensions Manager.
Defining a Data Model
DataController services are easiest to define if you use Entity Framework as your underlying model because there are derived types that encapsulate some of the code required when working with an Entity Framework model. Upshot includes support for working with Database-First or Model-First EDMX Entity Framework models, or Code-First DbContext Entity Framework models. I use Code-First in the sample code.
Upshot includes support for working with Database-First or Model-First EDMX Entity Framework models, or Code-First DbContext Entity Framework models.
The SPA101 solution in the sample code uses the Northwind database as the data source. There’s a simple entity class for a Customer shown in the following snippet that I added to the Models folder of the project.
public class Customer
{
[Key]
public string CustomerID { get; set; }
public string CompanyName { get; set; }
public string Phone { get; set; }
}
Notice the [Key] attribute on the CustomerID property. The DataController class recognizes the DataAnnotations namespace for that attribute, and others are defined and can use them to help manage the round-trip transfer of the data as well as to support client-side validation of data changes.
To do the data access for customers, define a DbContext derived class that exposes a Customers collection as a DbSet.
public class NorthwindDbContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
}
You’ll also need a connection string in the web.config file named NorthwindDbContext that points to the Northwind database. The sample code includes a SQL script file to create that database so you can run the sample. See this article http://www.code-magazine.com/Article.aspx?quickid=1108051 for a deeper introduction into Entity Framework Code-First.
Real-world data models can get a lot more complicated than the demo code model. DataControllers and Upshot can handle one-to-many, one-to-one, and many-to-many relationships between objects. You probably won’t want to get too complicated with the data models that you expose to the client side because transferring large chunks of data to a browser-based application is not good; you might run into object model relationships that DataControllers, Upshot, or JSON can’t handle well, such as circular object graph references.
Define the DataController
A simple controller class for exposing the Customers collection to the client looks like this:
public class CustomersDataController :
DbDataController<NorthwindDbContext>
{
public IQueryable<Customer> GetCustomers()
{
return DbContext.Customers.OrderBy(
c => c.CompanyName);
}
public void InsertCustomer(Customer c)
{ base.InsertEntity(c); }
public void UpdateCustomer(Customer c)
{ base.UpdateEntity(c); }
public void DeleteCustomer(Customer c)
{ base.DeleteEntity(c); }
}
You can see that the base class exposes the wrapped DbContext class passed as a generic type parameter so that you can get to it through the DbContext property on the base class. The base class takes care of the instancing of that class and the basic query patterns.
Data retrieval calls (HTTP GETs) will be routed to the query method using an ASP.NET Routing Service action parameter. When an update comes from the client, it comes in as a POST request, and gets routed to a method on the base class called Submit that takes in a ChangeSet representing the Unit of Work from the client. The ChangeSet can contain multiple inserts, updates, and deletes of items in the underlying resource collection. The base class then iterates through those changes one at a time and invokes your controller class’ respective methods, giving your code a chance to handle any business logic and possibly handle the data access yourself. However, if you are using the DbDataController class, you can simply call the base class methods and it will take care of the Entity Framework calls required to persist the changes.
Add an Area Registration
You will need to provide specific routing information for the DataController service because the default routes for an MVC project do not use the same URI conventions as Upshot does for routing the calls to the service. Area registrations are automatically discovered in an MVC application based on a call made in the Global.asax class.
The following code shows the necessary area registration for the CustomersDataController class.
public class CustomersDataRouteRegistration :
AreaRegistration
{
public override string AreaName
{
get { return "CustomersData"; }
}
public override void RegisterArea(
AreaRegistrationContext context)
{
RouteTable.Routes.MapHttpRoute(
"CustomersData", // Route name
"api/CustomersData/{action}", // URL
new { controller = "CustomersData" }
// Parameter defaults
);
}
}
This routing allows URLs like the following:
- Call the query method: http://mysite/api/CustomersData/GetCustomers
- Call the Submit method: http://mysite/api/CustomersData/Submit
Add a Web Page
You’ll need a place for the client HTML and JavaScript to live, and you can either create that as a flat HTML file or you can use ASP.NET to render the basic structure of the page. In the sample code, you’ll see both for comparison, but I’ll mainly show the flat HTML file code. There is no MVC magic required for rendering a page that uses Upshot, just good old HTML and JavaScript.
There is no MVC magic required for rendering a page that uses Upshot, just good old HTML and JavaScript.
The first thing you’ll need is to include the following scripts in your page:
<script src="Scripts/<a href="http://jquery-1.6.4.js">jquery-1.6.4.js</a>"
type="text/javascript"></script>
<script src="Scripts/knockout.js"
type="text/javascript"></script>
<script src="Scripts/upshot.js"
type="text/javascript"></script>
<script src="Scripts/<a href="http://upshot.compat.knockout.js">upshot.compat.knockout.js</a>"
type="text/javascript"></script>
Upshot leverages JQuery and you will want to in your client-side JavaScript as well, so that is the first dependency. Knockout is a separate JavaScript library that allows you to data bind HTML elements to JavaScript objects and properties in a fashion very similar to XAML data binding, and also supports the Model-View-ViewModel (MVVM) pattern in client JavaScript. I won’t be going into any detail on that library because I won’t be using any Knockout-specific code. It needs to be there because Upshot requires the support of a library that has an implementation of an observable object. An observable is a JavaScript object that can raise change notifications to subscribed objects when the object properties change, much like the INotifyPropertyChanged interface pattern in .NET code. Knockout provides this capability, but you could substitute a different JavaScript library that provides similar capabilities. In the current release, Knockout is the only supported library that works with Upshot.
Next, open a JQuery ready handler and start filling in the body of that function (the ellipses) with the Upshot code.
<script type="text/javascript">
$(function () { ... });
</script>
Start Writing Some Upshot Code
The first part of your JQuery-ready handler defines the JavaScript objects that you use as the data objects on the client side. In the demo scenario, you need a Customer object to hold the customer data. The following code shows what that declaration looks like.
window.Customer = function (initialData) {
var self = this;
upshot.map(initialData,
upshot.type(Customer), self);
}
Objects in JavaScript are functions themselves and you can pass in initialization data like a parameterized constructor in .NET. The customer object uses Upshot to map the initialization data into fields in the object based on metadata.
The next chunk of Upshot code initializes the Upshot dataSources collection and declares the metadata about the type(s) you will return from the service and works with the client side, as shown in Listing 1. You can see the manual declaration of the Upshot initialization code and get a sense of the structure. The good news is that if you use an ASP.NET page instead of a flat HTML file, there is an HtmlHelper class that can render a good chunk of the code. I’ll show that code after the resulting JavaScript that makes it all happen on the client side.
The code in Listing 1 first initializes the dataSources collection of the Upshot library. This either creates an empty object or uses an existing collection. Next, you can see the declaration of metadata for the type(s) that are returned from the service to describe the data it can work with. For the metadata, first there is the declared JavaScript type of the object(s) (Customer:#SPA101Models in this sample). This uses the server class name and namespace to form a string that is unique to that type and is used by Upshot to locate the rest of the metadata for that type. Next, it defines which field is the key field, followed by the declarations of the other fields in the object, along with their type strings.
You can also see in Listing 1 that the metadata also contains validation information in the rules section. It can generate these from DataAnnotation attributes on the properties of your types. For example, because the CustomerID property was marked as a [Key**],** it treats it as a required field. Entity Framework can infer some things like maximum string length from the size of the underlying database column and those can get added to the metadata as well.
The next statement in Listing 1 defines the Customers collection that will be one of the dataSources exposed by Upshot in this application (in this sample, it’s the only collection). It is initialized with the RemoteDataSource method, which takes a settings object as a parameter. The providerParameters argument identifies the base URI (relative to the site of the Web page, but can be absolute) of the service, as well as the query method name to invoke to return the collection, which becomes an action parameter on the URI when the request is made. The entityType identifies which of the types defined in metadata will be the items in this data source collection. The bufferChanges flag indicates whether you want changes cached on the client side so that you can issue a batch submission to the server at some point in the future, or whether you want each change that is made to an entity flushed with a service call at the time the change is made. The dataContext parameter relates to capabilities of Knockout beyond the scope of this article. The mapping parameter supplies the factory method that is used as each entity is read out of the stream to construct an object for it on the client side.
The last statement in Listing 1 might seem somewhat redundant because it relates a metadata type to the client-side object type. This is used by the upshot.map function that was called in the Customer initialization method.
Simplify Upshot Initialization with MVC
There is a lot going on in Listing 1, but it is mostly boilerplate code that begs for code generation. And you can get exactly that if you use ASP.NET to render the page instead of doing it by hand. You can do the full initialization of Upshot with one line of Razor code in an ASP.NET MVC page:
@(Html.UpshotContext(bufferChanges: true)
.DataSource<CustomersDataController>(
x => x.GetCustomers())
.ClientMapping<Customer>("Customer"))
That one line of code reflects on the entity type and the controller and generates the metadata, and it emits the standard initialization code for Upshot in Listing 1.
You can do the full initialization of Upshot with one line of Razor code in an ASP.NET MVC page.
Make the Call
Now that Upshot is initialized and knows what the data is that it will be working with and where to call to get it, you need to invoke the first call to retrieve data when appropriate. You do this with a refresh call on the data source collection. In the sample application, this is part of the ready handler.
var custDataSource =upshot.dataSources.Customers;
var dataSource = custDataSource.refresh(
undefined, function (customers) {
for (i = 0; i < customers.length; i++) {
$("#CustomersList").append("<li>" +
customers[i].CompanyName() +
"</li>");
}
});
When you call refresh, an HTTP GET request is sent to the query method that you specified in setting up the RemoteDataSource using the base address and the query method name as an action parameter (/api/CustomersData/GetCustomers). The callback function passed to the refresh call in the snippet gets the resulting entity collection as an argument and iterates over it, using a JQuery selector to get a reference to an unordered list (ul) element with id=CustomersList and appending list items for each customer.
One important thing to point out is that CompanyName is called as a function, not accessed as a named property. This is because Upshot creates all of the objects and their properties as observables, mentioned earlier. The support for observables is provided by Knockout, not Upshot. To access the value of an observable property, call it as a function. To set the value, pass the new value to the function as a single argument.
Add New Items
The HTML markup also contains input fields for defining a new customer and a button to add it to the Customers collection cached on the client side, as well as making the call to the server to persist the new customer. The handling code for the button click is shown in Listing 2. The first thing it does is get a reference to the Customers collection of entities from the data source that was returned from the refresh call shown earlier. This gets populated asynchronously when the GET response comes back in, so trying to look at that collection too soon after calling for a refresh can result in an empty collection. The code uses a button click handler for simplicity so that you can click it to use the results, which will have been retrieved by the time you have a chance to click the button. For production code you should wire up a callback to enable the button at an appropriate time, such as once the asynchronous call is complete.
Listing 2 shows the code to construct a new Customer object, populate its properties with the values of the input fields using the JQuery selectors and val function. The code then pushes the new object into the entities collection and calls commitChanges on the Upshot data source. The commitChanges method is what triggers an HTTP POST request to be sent to the base address, with Submit appended as a URI parameter (/api/CustomersData/Submit). Upshot packages all the changes made and cached on the client side and sends them as a ChangeSet payload, which in turn gets passed to the DataController base class Submit method on the server side. It then invokes the appropriate Insert/Update/Delete methods on your derived DataController class for each entity in the change set.
Upshot packages all the changes made and cached on the client side and sends them as a ChangeSet payload which gets passed to the DataController.
The commitChanges function in Listing 2 takes two callback functions, one for success and one for failure. The one for success clears the input fields in the add customer form, and the failure one pops up an alert dialog box with the error information.
To edit an item, you get a reference to the appropriate entity from the collection and start changing the properties of that object. Because those properties are observables, Upshot is notified of the changes and knows that it should be sent to the server when commitChanges is called. If you want to delete an object, you call the deleteEntity method on the data source object. Upshot marks it for deletion and removes it from the exposed collection of entities, but does not permanently remove it until after commitChanges has been called and a success response is returned.
Define the UI Structure
The UI markup couldn’t be much simpler for this sample; it’s just a few inputs for the add customer form and a single empty unordered list element that gets populated dynamically when the customers are retrieved.
<p>CustomerID:
<input name="CustomerID" id="CustomerID" />
</p>
<p>CompanyName:
<input name="CompanyName" id="CompanyName"/>
</p>
<p>Phone:
<input name="Phone" id="Phone" />
</p>
<p>
<button id="AddCustomer">Add</button>
</p>
<br />
<ul id="CustomersList" />
ASP.NET Single Page Applications Timeline
Even though Microsoft provided a version of the SPA functionality as part of the ASP.NET MVC 4 beta release, the Release Preview of MVC 4 does not include it. Microsoft has decided to release it separately as its own package, and it will release after the final release of MVC 4. Microsoft has made the MVC 4 and SPA code base available as open source at http://aspnetwebstack.codeplex.com. The best place to get the latest official releases is at http://www.asp.net/Single-Page-Application.
Wrapping Up
Now you have seen the basic structure and code for a minimal HTML application using Upshot and the ASP.NET DataController services. In a real application, you are probably going to want to do things a little more cleanly in terms of the UI, and you are certain to have a much more involved UI than this example shows. Get familiar with JQuery as well as KnockoutJS (http://www.knockoutjs.com). Other libraries that are part of the MVC 4 project scripts by default include nav.js and History.js, which are the pieces that facilitate swapping out sub-views and having discrete addresses associated with that sub-view layout so that users can navigate forward and back with the browser through those views, as well as saving shortcuts to those locations in the application.
The sample code for the article includes a separate sample solution that has a slightly more complete version of the same functionality. It uses Knockout and data binding to render the UI, uses the SPA.css styles that are included when you install the SPA NuGet package for a little nicer styling, and it uses the validation metadata (rules) to provide client-side indications when you fail validation. It runs validation any time you change one of the objects or define a new one on the client side, but it can also show validation errors generated server side when committing changes.
It is pretty early in the lifecycle of Upshot as a library and the ASP.NET Single Page Application stack as a whole. Keep your eyes peeled on the ASP.NET team blog and the www.asp.net site for future releases, samples, and documentation. You can also check out my blog as I continue to do a lot of writing and speaking about these technologies.
Listing 1: Upshot initialization code
upshot.dataSources = upshot.dataSources || {};
upshot.metadata({ "Customer:#SPA101.Models": {
"key": ["CustomerID"],
"fields": {
"CompanyName": { "type": "String:#System" },
"CustomerID": { "type": "String:#System" },
"Phone": { "type": "String:#System" }
},
"rules": {
"CustomerID": {
"required": true, "maxlength": 128
}
},
"messages": {
}
}
});
upshot.dataSources.Customers = upshot.RemoteDataSource({
providerParameters: { url: "/api/CustomersData/",
operationName: "GetCustomers" },
entityType: "Customer:#SPA101.Models",
bufferChanges: true,
dataContext: undefined,
mapping: { "Customer:#SPA101.Models":
function (data) { return new Customer(data) } }
});
upshot.registerType("Customer:#SPA101.Models", function () {
return Customer });
Listing 2: Submit changes with a button click
$("#AddCustomer").click(function () {
var customers = dataSource.getEntities();
var newCust = new Customer({
CustomerID: $("#CustomerID").val(),
CompanyName: $("#CompanyName").val(),
Phone: $("#Phone").val()
});
customers.push(newCust);
dataSource.commitChanges(function () {
$("#CustomerID").val("");
$("#CompanyName").val("");
$("#Phone").val(""); },
function (status, errorText) {
alert("Commit failed. Status code: "
+ status + ", error message: "
+ errorText);
});
});