Monday, February 20, 2012

Building Single Page Apps with ASP.NET MVC4 - Part 1 - Basic Desktop Application

This blog entry is mainly based on the presentation given by Steven Sanderson on TechDays 2012 called “Building Single Page Apps for desktop, mobile and tablet with ASP.NET MVC 4”. Steven’s talk consisted of a large demo that immediately got me interested and made me want to try it out.
There was one big disappointment however. With the latest beta version of ASP.NET MVC 4 and information from the official pages, it was not possible to recreate Steven’s demo. In the following post, I’ll show what I had to do to get things to work. This is the first post of a series
  1. Single Page Applications - Part 1 - Basic Desktop application
  2. Single Page Applications - Part 2 - Advanced Desktop Application
  3. Single Page Applications - Part 3 - Basic Mobile Application
  4. Single Page Applications - Part 4 - Sorting, Filtering and Manipulation data with upshot.js
  5. Single Page Applications - Part 5 - With MVC4 RC

New Project Template

The ASP.NET MVC 4 beta adds a new project template in Visual Studio 2010 called “The “Single Page Application" (SPA). When creating a new project and starting it up, you’ll get some guidance about completing a first demo application for adding ToDoItems using a new TasksController. I will not do this but instead I will follow the same sequence as Steve did in his presentation and create a new SPA application called “DeliveryTracker”

Server side Domain Model

Since this demo application is a tool to track deliveries we will be using the following datamodel. The Customer class shows how to define a one-to-many relationship.
namespace DeliveryTracker.Models
{
    public class Customer
    {
        public int CustomerId { get; set; }

        public string Name { get; set; }
        public string Address { get; set; }
    }

    public class Delivery
    {
        public int DeliveryId { get; set; }
        public virtual int CustomerId { get; set; }
        public virtual Customer Customer { get; set; }

        public string Description { get; set; }
        public bool IsDelivered { get; set; }
    }
}
In order to store delivery information in the database, we are using the Entity Framework Code First approach
namespace DeliveryTracker.Models
{
    public class AppDbContext : DbContext
    {
        public DbSet<Customer> Customers { get; set; }
        public DbSet<Delivery> Deliveries { get; set; }
    }
}

Web API Data Service

The data for the application is loaded from a DbDataController which was introduced with the new Web API feature. In the solution explorer, right-click the controller folder and add a new controller called “DataServiceController” based on the “Empty Controller” template. The purpose here is to show what is happening behind the scenes and not rely on scaffolding too much. Replace the parent Controller class by DbDataController using the AppDbContext as generic parameter.
namespace DeliveryTracker.Controllers
{
    public class DataServiceController : DbDataController<AppDbContext>
    {

        public IQueryable<Delivery> GetDeliveriesForToday() 
        {
            return DbContext.Deliveries.Include("Customer")
                            .OrderBy(x => x.DeliveryId);
        }

        public void InsertDelivery(Delivery delivery) 
                  { InsertEntity(delivery); }
        public void UpdateDelivery(Delivery delivery) 
                  { UpdateEntity(delivery); }
        public void DeleteDelivery(Delivery delivery) 
                  { DeleteEntity(delivery); }
    }
}
The GetDeliveriesForToday will be used by the page to load the data. The IQueryable type allows sorting, paging, filtering and passing in custom parameters. The other methods are necessary for inserting, updating and deleting entities. If you build and run now, you could run OData queries against the service on http://localhost/api/DataService/GetDeliveriesForToday

Client Side ViewModel -  upshot.js & knockout.js

In order to use this data, the page will use the upshot.js javascript library that knows how to talk to a DbDataController. It will want to map your server side C# model in to a client-side javascript model. Steven has hidden this step behind his mysterious and undocumented Html.UpshotContext class.

Add a new “App” subfolder inside the “Scripts” folder and create a new script there called “DeliveryViewModel.js”. In the constructor of the DeliveriesViewModel, we are using upshot to make a connection to the GetDeliveriesForToday method in the DbDataController class. The results from the query are stored in the self.deliveries property.

Notice that the properties of the Customer and Delivery classes are knockout observables. Knockout keeps the data on your views and viewmodel synchronized. Upshot synchronizes with the data service. The bufferChanges option is set to false, meaning that any data that is changed on the web page will also be submitted to the server and saved directly in the database.
/// <reference path="_references.js" />

function DeliveriesViewModel() {
    // Private
    var self = this;
    var dataSourceOptions = {
        providerParameters: { 
          url: "/api/DataService", 
          operationName: "GetDeliveriesForToday"
        },
        entityType: "Delivery:#DeliveryTracker.Models",
        bufferChanges: false,
        mapping : Delivery
    };

    // Public Properties
    self.dataSource = new upshot.RemoteDataSource(dataSourceOptions)
                                .refresh();
    self.deliveries = self.dataSource.getEntities();   
}

function Customer (data) {
    var self = this;

    self.CustomerId = ko.observable(data.CustomerId);
    self.Name = ko.observable(data.Name);
    self.Address = ko.observable(data.Address);
    upshot.addEntityProperties(self, "Customer:#DeliveryTracker.Models");
}

function Delivery (data) {
    var self = this;

    self.DeliveryId = ko.observable(data.DeliveryId);
    self.CustomerId = ko.observable(data.CustomerId);
    self.Customer = ko.observable(data.Customer);
    self.Description = ko.observable(data.Description);
    self.IsDelivered = ko.observable(data.IsDelivered);
    upshot.addEntityProperties(self, "Delivery:#DeliveryTracker.Models");
}

Index View

In order to trigger the upshot call to the service when the index.cshtml page is loaded, add this code below the title section. All the rest of the code is not needed and can be removed. The Html.MetaData function provides the structure of the types to upshot. Knockout applies the bindings to the DeliveriesViewModel
<script src="~/Scripts/upshot.js" 
          type="text/javascript"></script>
<script src="~/Scripts/knockout-2.0.0.js" 
          type="text/javascript"></script>
<script src="~/Scripts/nav.js" 
          type="text/javascript"></script>
<script src="~/Scripts/native.history.js" 
          type="text/javascript"></script>
<script src="~/Scripts/upshot.compat.knockout.js"
          type="text/javascript"></script>
<script src="~/Scripts/upshot.knockout.extensions.js" 
          type="text/javascript"></script>
<script src="~/Scripts/App/DeliveriesViewModel.js" 
          type="text/javascript"></script>

<script type="text/javascript">
    $(function () {
        upshot.metadata(@(Html.Metadata
            <DeliveryTracker.Controllers.DataServiceController>()));

        ko.applyBindings( new DeliveriesViewModel( ));
    });
</script>
We can now use the data that is loaded and bound to the DeliveriesViewModel in the HTML. Add this code below the javascript call
<section>
<div>
    <h3>Deliveries</h3>
    <ol data-bind="foreach: deliveries">
        <li data-bind="css: { highlight: IsDelivered}">
            <strong data-bind="text: Description"></strong>
            is for <em data-bind="text: Customer().Name"></em>
            <label>
               <input data-bind="checked: IsDelivered" 
                           type="checkbox"/>
               Delivered</label>
        </li>
    </ol>
</div>
</section>

Routing

If you run this code, you will noticed that loading data from the database and displaying it on page works fine but submitting changes to the services fails. Investigating with Fiddler shows that the service responds to the submit request with the error “The service does not support the POST verb”

What Steven Sanderson also failed to mention is that you need to correct the 'MapHttpRoute' route in the routing table in global.asax.cs like this:
            routes.MapHttpRoute(
                "DataService", // Route name
                "api/DataService/{action}", // URL with parameters
                new { controller = "DataService" } // Parameter defaults
            );
So with the code above you can list the deliveries in the database and update the "IsDelivered" properties in real-time from a single page. All communication consists of AJAX calls handled by the upshot framework.

Download the source code

You can download the code (Visual Studio 2010 project) from my Skydrive

19 comments:

Anonymous said...

Started working following steve presentation but Html.Upshot was missing. Following your great post it's all working now without a problem.
One little question.
I can't seem to show data if i'm passing ViewModel from dataservice not my full entity. Can you help?

Bart Jolling said...

I'm afraid you'll need to elaborate on that questions because I'm not sure I understand what you mean. To what class do your refer when you mention 'ViewModel'? The only ViewModel here is the client-side DeliveriesViewModel which cannot be passed from the server-side DataService.

What I can tell is that the purpose of DbDataController is to expose full entities the way they are defined in the Entity Framework model. If you want to expose different objects, you might want to take a look at one of its parent classes like DataController or ApiController. But I can't tell yet if this would still work with upshot or if you'd be better off looking at the native WebAPI samples.

Anonymous said...

I was interested to expose different objects (DTO-s) not full entities and use upshot, but as you say we'll wait until proper documentation is out for upshot. Thanks for suggestion i'll look into DataController & ApiController. As for WebApi i have already used it with no problem to expose my DTO-s, it's really nice how it all works together. And WebApiTestClient is much better to use than fiddler.

Fernando Correia said...

Great job explaining all that. Can you make your demo source code available?

Russ Painter said...

I'm trying to add security to my data service. I've tried re-using my AuthorizeAttribute(s) that are working on my normal controllers, but this isn't doing it. I've created a Stack Overflow question here: http://stackoverflow.com/questions/9475068/authorize-attribute-not-filtering-on-dbdatacontroller

Thought you might be the man with the answer.

Bart Jolling said...

Sorry for the late reply but the past few days I've been ill. I've uploaded the code to my skydrive:

https://skydrive.live.com/redir.aspx?cid=92fec8e2df222664&resid=92FEC8E2DF222664!400&parid=92FEC8E2DF222664!399&authkey=!ACc_clpHQCxRexY

Eric's Edge said...

Thanks for this. Makes a world of difference having "all" of it explained!

Fernando Correia said...

The code worked well and it's very informative. Thank you for sharing.

I had to add assembly="EntityFramework, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" to Web.config to make it run, though.

Timm Krause said...

Can you tell me why I don´t get the Html.Metadata method?

Bart Jolling said...

Hi Tim, this function appears to be part of the "System.Web.Http.Data.Helpers" assembly so you would have to add following line to your web.config

<add assembly="System.Web.Http.Data.Helpers, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />

This was done automatically for me when creating a new project from the "Single Page Application" template in ASP.NET MVC4 beta. You can download my working VS2010 project so compare and see if you are missing something.

Sagy said...

Hi,
I tried for a few hours today to figure where the Html.Upshot was coming from and I couldn't... Great article.

What I'd like to know is, do I need to duplicate my model to the client in order to use Upshot, the way you did in the article?
If so, it doesn't seem reasonable. I have a model that contains around 30 objects with all their connections. That's a lot to duplicate.

Thanks,
Sagy

Bart Jolling said...

Steven Sanderson has released his version of this application.

https://github.com/SteveSanderson/DeliveryTracker

After browsing through his code, it seems that his Html.UpshotContext function is creating the Javascript viewmodels so you wouldn't have to do it manually.

In Stevens code the UpshotContext function can be found in the System.Web.Http.Data.Helpers2 project which is part of the AspNetWebApi.Data package.

Also the answer from Tallmaris to this StackOverflow question might help you:
http://stackoverflow.com/questions/9669179/binding-upshot-to-a-web-api-in-a-different-project

Sagy said...

Hi Bart,

Thanks for the reply - you're right.

I also discovered that there is a new release of Upshot - version 1.0.0.2 and when I installed it through NuGet (Install-Package SinglePageApplication.CSharp), it adds a reference to System.Web.Http.Data.Helpers2 and than UpshotContext is avaliable.

Sagy

James Hancock said...

Why oh why does Web API not support SELECT????

Can you say "Holly SELECT * AMATURE HOUR BATMAN?"

If you're listing info, you're going to want to do a projection and return just the results requested per the select.

We should be able to specify that it returns dynamic and the underlying structure should do the select for us automatically.

Anyone have any real work arounds that allow the client to do the select for us instead of having to create a zilion get methods with a zillion classes with the right things that should be returned?

Please say this is being put in for the next beta!!!!

Bart Jolling said...

That would definitely make your site vulnerable to SQL Injection attacks.

A quick web search gave me this interesting article that shows how you can solve your problem by using the Web API to expose your data according to the ODATA conventions to manipulate and filter your data.

And once your entities are exposed as OData, you can use AJAX to load them dynamically. Here's an example using the jQuery DataTables plug-in.

Eric Veal said...

I can't get version 4.0 of your Delivery tracker to run. Did it break with the MVC 4.0 release candidate? What's the latest and greatest on learning SPA?

Bart Jolling said...

Hi Eric,

By coincidence I've received a new laptop and was re-installing it with Visual Studio 2010 SP1, the MVC 4.0 RC and the latest SQL Server 2012. So I tried to get DeliveryTracker.4.0 working in this fresh environment.

I chose the ASP.NET Development Server to host the project. I noticed that the name of my connectionstring in the web.config was incorrect. After renaming it from "DefaultConnection" into "AppDbContext" and pointing it to my new SQL 2012 instance, the application worked again.

If it's a different issue, please let me know. I'll update the DeliveryTracker.4.0 package on my SkyDrive as soon as I'm back on my regular DEV station

Evelynn said...

'call it “DeliveryViewModel.js”' - really?

Bart Jolling said...

@Evelynn

fixed, thanks