ASP.NET MVC 5

A .NET Developer Primer for Single-Page Applications

Long Le

Download the Code Sample

A majority of Microsoft .NET Framework developers have spent most of their professional lives on the server side, coding with C# or Visual Basic .NET when building Web applications. Of course, JavaScript has been used for simple things such as modal windows, validation, AJAX calls and so on. However, JavaScript (client-side code for the most part) has been leveraged as a utility language, and applications were largely driven from the server side.

Lately there’s been a huge trend of Web application code migrating from the server side to the client side (browser) to meet users’ expectations for fluid and responsive UX. With this being the case, a lot of .NET developers (especially in the enterprise) are dealing with an extreme amount of anxiety about JavaScript best practices, architecture, unit testing, maintainability and the recent explosion of different kinds of JavaScript libraries. Part of the trend of moving to the client side is the increasing use of single-page applications (SPAs). To say that SPA development is the future is an extreme understatement. SPAs are how some of the best applications on the Web offer fluid UX and responsiveness, while minimizing payloads (traffic) and round-trips to the server.

In this article, I’ll address the anxieties you might experience when making the transition from the server side into the SPA realm. The best way to deal with these anxieties is to embrace JavaScript as a first-class language just like any .NET language, such as C#, Visual Basic .NET, Python and so on.

Following are some fundamental principles of .NET development that are sometimes ignored or forgotten when developing apps in JavaScript:

  • Your code base is manageable in .NET because you’re decisive with class boundaries and where classes actually live within your projects.
  • You separate concerns, so you don’t have classes that are responsible for hundreds of different things with overlapping responsibilities.
  • You have reusable repositories, queries, entities (models) and data sources.
  • You put some thought into naming your classes and files so they’re more meaningful.
  • You practice good use of design patterns, coding conventions and organization.

Because this article is for .NET developers who are being introduced to the SPA world, I’ll incorporate the least number of frameworks possible to build a manageable SPA with sound architecture.

Creating an SPA in Seven Key Steps

Following are seven key steps to convert a new ASP.NET Web Application that was created with the out-of-the-box Visual Studio 2013 ASP.NET MVC template into an SPA (with references to the appropriate project files that can be found in the accompanying code download).

  1. Download and install the NuGet packages RequireJS, RequireJS text plug-in and Kendo UI Web.
  2. Add a configuration module (Northwind.Web/Scripts/app/main.js).
  3. Add an app module (Northwind.Web/Scripts/app/app.js).
  4. Add a router module (Northwind.Web/Scripts/app/router.js).
  5. Add an action and view both named Spa (Northwind.Web/Controllers/HomeController.cs and Northwind.Web/Views/Home/Spa.cshtml).
  6. Modify the _ViewStart.cshtml file so MVC will load views without using the _Layout.cshtml file by default (Northwind.Web/Views/_ViewStart.cshtml).
  7. Update the layout navigation (menu) links to match the new SPA-friendly URLs (Northwind.Web/Views/Shared/_Layout.cshtml).

After these seven steps have been carried out, your Web application project structure should look something like Figure 1.

ASP.NET MVC Project Structure
Figure 1 ASP.NET MVC Project Structure

I’ll show how to build an awesome SPA in ASP.NET MVC with the following JavaScript libraries, available via NuGet:

  • RequireJS (requirejs.org): This is a Java­Script file and module loader. RequireJS will provide #include/import/require APIs and the ability to load nested dependencies with dependency injection (DI). The RequireJS design approach uses the Asynchronous Module Definition (AMD) API for JavaScript modules, which helps to encapsulate pieces of code into useful units. It also provides an intuitive way to refer to other units of code (modules). RequireJS modules also follow the module pattern (bit.ly/18byc2Q). A simplified implementation of this pattern uses JavaScript functions for encapsulation. You’ll see this pattern in action later as all JavaScript modules will be wrapped within a “define” or “require” function.
  • Those familiar with DI and Inversion of Control (IoC) concepts can think of this as a client-side DI framework. If that’s as clear as mud at the moment, no worries—I’ll soon get into some coded illustrations where all this will make sense.
  • Text plug-in for RequireJS (bit.ly/1cd8lTZ): This will be used to remotely load chunks of HTML (views) into the SPA.
  • Entity Framework (bit.ly/1bKiZ9I): This is pretty self-explanatory, and because the focus of this article is on SPA, I won’t get too much into Entity Framework. However, if you’re new to this, there’s plenty of documentation available.
  • Kendo UI Web (bit.ly/t4VkVp): This is a comprehensive JavaScript/­HTML5 framework that encompasses Web UI Widgets, DataSources, templates, the Model-View-ViewModel (MVVM) pattern, SPAs, styling, and so on to help deliver a responsive and adaptive application that will look great.

Setting up the SPA Infrastructure

To show how to set up the SPA infrastructure, first I’ll explain how to create the RequireJS (config) module (Northwind.Web/Scripts/app/main.js). This module will be the app start-up entry point. If you’ve created a console app, you can think of this as the Main entry point in Program.cs. It basically contains the first class and the method that’s called when the SPA starts up. The main.js file basically serves as the SPA’s manifest and is where you’ll define where all things in the SPA are and their dependencies, if any. The code for RequireJS configuration is shown in Figure 2.

Figure 2 RequireJS Configuration

require.config({
  paths: {
    // Packages
    'jquery': '/scripts/jquery-2.0.3.min',
    'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
    'text': '/scripts/text',
    'router': '/scripts/app/router'
  },
  shim : {
    'kendo' : ['jquery']
  },
  priority: ['text', 'router', 'app'],
  jquery: '2.0.3',
  waitSeconds: 30
});
require([
  'app'
], function (app) {
  app.initialize();
});

In Figure 2, the paths property contains a list of where all the modules are located and their names. Shim is the name of a module defined previously. The shim property includes any dependencies the module may have. In this case, you’re loading a module named kendo and it has a dependency on a module named jquery, so if a module requires the kendo module, go ahead and load jQuery first, because jQuery has been defined as a dependency for the kendo module.

In Figure 2, the code “require([], function(){})” will load in the next module, which is the module I named app. Note that I’ve deliberately given meaningful names to modules.

So, how does your SPA know to invoke this module first? You configure this on the first landing page in the SPA with the data-main attribute in the script reference tag for RequireJS. I’ve specified that it run the main module (main.js). RequireJS will handle all the heavy lifting involved in loading this module; you just have to tell it which module to load first.

You have two options for SPA views that will be loaded into the SPA: standard HTML (*.html) or ASP.NET MVC Razor (*.cshtml) pages. Because this article is intended for .NET developers—and a lot of enterprises have server-side libraries and frameworks they’d like to continue using in their views—I’ll go with the latter option of creating Razor views.

I’ll start off by adding a view and name it Spa.cshtml, as mentioned previously. This view will basically load up the shell or all the HTML for the layout of the SPA. From this view, I’ll load in the other views (for example, About.cshtml, Contact.cshtml, Index.cshtml and so on) as the user navigates through the SPA, by swapping the views that replace all the HTML in the “content” div.

Creating the SPA Landing Page (Layout) (Northwind.Web/Views/Spa.cshtml) Because the Spa.cshtml view is the SPA’s landing page where you’ll load in all your other views, there won’t be much markup here, other than referencing the required style sheets and RequireJS. Note the data-main attribute in the following code, which tells RequireJS which module to load first:

@{
  ViewBag.Title = "Spa";
  Layout = "~/Views/Shared/_Layout.cshtml";
}
<link href=
  "~/Content/kendo/2013.3.1119/kendo.common.min.css" 
  rel="stylesheet" />
<link href=
  "~/Content/kendo/2013.3.1119/kendo.bootstrap.min.css" 
  rel="stylesheet" />
<script src=
  "@Url.Content("~/scripts/require.js")"
  data-main="/scripts/app/main"></script>
<div id="app"></div>

Adding an Action for the SPA Layout (Northwind.Web/­Controllers/HomeController.cs) To create and load the Spa.cshtml view, add an action and view:

public ActionResult Spa()
{
  return View();
}

Create the Application Module (Northwind.Web/Scripts/app/app.js) Here’s the Application module, responsible for initializing and starting the Kendo UI Router:

define([
    'router'
  ], function (router) {
    var initialize = function() {
      router.start();
    };
    return {
      initialize: initialize
    };
  });

Create the Router Module (Northwind.Web/Scripts/app/router.js) This is called by app.js. If you’re already familiar with ASP.NET MVC routes, it’s the same notion here. These are the SPA routes for your views. I’ll define all the routes for all the SPA views so when the user navigates through the SPA, the Kendo UI router will know what views to load into the SPA. See Listing 1 in the accompanying download.

The Kendo UI Router class is responsible for tracking the application state and navigating between the application states. The router integrates into the browser history using the fragment part of the URL (#page), making the application states bookmarkable and linkable. When a routable URL is clicked, the router kicks in and tells the application to put itself back into the state that was encoded into the route. The route definition is a string representing a path used to identify the state of the application the user wants to see. When a route definition is matched from the browser’s URL hash fragment, the route handler is called (see Figure 3).

Figure 3 Registered Route Definitions and Corresponding URLs

Registered Route (Definition) Actual Full (Bookmarkable) URL
/ localhost:25061/home/spa/home/index
/home/index localhost:25061/home/spa/#/home/index/home/about
/home/about localhost:25061/home/spa/#/home/about/home/contact
/home/contact localhost:25061/home/spa/#/home/contact/customer/index
/customer/index localhost:25061/home/spa/#/customer/index

As for the Kendo UI layout widget, its name speaks for itself. You’re probably familiar with the ASP.NET Web Forms MasterPage or MVC layout included in the project when you create a new ASP.NET MVC Web Application. In this SPA project, it’s located at the path Northwind.Web/Views/Shared/_Layout.cshtml. There’s little difference between the Kendo UI layout and MVC layout, except the Kendo UI layout runs on the client side. Just as the layout worked on the server side, where the MVC runtime would swap out the content of the layout with other views, the Kendo UI layout works the same exact way. You swap out the view (content) of the Kendo UI layout using the showIn method. View contents (HTML) will be placed in the div with the ID “content,” which was passed into the Kendo UI layout when it was initialized. After initializing the layout, you then render it inside the div with the ID “app,” which is a div in the landing page (Northwind.Web/Views/Home/Spa.cshtml). I’ll review that shortly.

The loadView helper method takes in a view model, a view and—if needed—a callback to invoke once the view and view model binding takes place. Within the loadView method, you leverage the Kendo UI FX library to aesthetically enhance the UX by adding some simple animation to the view swapping process. This is done by sliding the current loaded view to the left, remotely loading in the new view and then sliding the new loaded view back to the center. Obviously, you can easily change this to a variety of different animations using the Kendo UI FX library. One of the key benefits of using the Kendo UI layout is shown when you invoke the showIn method to swap out views. It will ensure the view is unloaded, destroyed properly and removed from the browser’s DOM, thus ensuring the SPA can scale and is performant.

Edit the _ViewStart.cshtml View (Northwind.Web/Views/­_ViewStart.cshtml) Here’s how to configure all views to not use the ASP.NET MVC layout by default:

@{
  Layout = null;
}

At this point, the SPA should be working. When clicking on any of the navigation links on the menu, you see the current content is being swapped out via AJAX thanks to the Kendo UI router and RequireJS.

These seven steps needed to convert a fresh ASP.NET Web Application into an SPA aren’t too bad, are they?

Now that the SPA is up and running, I’ll go ahead and do what most developers will end up doing with an SPA, which is adding some create, read, update and delete (CRUD) functionality.

Adding CRUD Functionality to the SPA

Here are the key steps needed to add a Customer grid view to the SPA (and the related project code files):

  • Add a CustomerController MVC controller (Northwind.Web/Controllers/CustomerController.cs).
  • Add a REST OData Customer Web API controller (Northwind.Web/Api/CustomerController.cs).
  • Add a Customer grid view (Northwind.Web/Views/­Customer/Index.cshtml).
  • Add a CustomerModel module (Northwind.Web/Scripts/app/models/CustomerModel).
  • Add a customerDatasource module for the Customer grid (Northwind.Web/Scripts/app/datasources/customer­Datasource.js).
  • Add an indexViewModel module for the Customer grid view (Northwind.Web/Scripts/app/viewModels/­indexViewModel.js).

Setting Up the Solution Structure with Entity Framework Figure 4 shows the solution structure, high­lighting three projects: Northwind.Data (1), Northwind.Entity (2) and Northwind.Web (3). I’ll briefly discuss each, along with Entity Framework Power Tools.

  • Northwind.Data: This includes everything related to the Entity Framework Object-Relational Mapping  (ORM) tool, for persistence.
  • Northwind.Entity: This includes domain entities, composed of Plain Old CLR Object (POCO) classes. These are all the persistent-ignorant domain objects.
  • Northwind.Web: This includes the ASP.NET MVC 5 Web Application, the presentation layer, where you’ll build out the SPA with two previously mentioned libraries—Kendo UI and RequireJS—and the rest of the server-side stack: Entity Framework, Web API and OData.
  • Entity Framework Power Tools: To create all the POCO entities and mappings (database-first), I used the Entity Framework Power Tools from the Entity Framework team (bit.ly/1cdobhk). After the code generation, all I did here is simply copy the entities into a separate project (Northwind.Entity) to address separation concerns.

A Best-Practice Solution Structure
Figure 4 A Best-Practice Solution Structure

Note: Both the Northwind SQL install script and a backup of the database are included in the downloadable source code under the Northwind.Web/App_Data folder (bit.ly/1cph5qc).

Now that the solution is set up to access the database, I’ll go ahead and write the MVC CustomerController.cs class to serve up the index and edit views. Because the controller’s only responsibility is to serve up an HTML view for the SPA, the code here will be minimal.

Creating MVC Customer Controller (Northwind.Web/­Controllers/CustomerController.cs) Here’s how to create the Customer controller with the actions for the index and edit views:

public class CustomerController : Controller
{
  public ActionResult Index()
  {
    return View();
  }
  public ActionResult Edit()
  {
    return View();
  }
}

Creating the View with the Customers Grid (Northwind.Web/­Views/Customers/Index.cshtml)Figure 5 shows how to create the view with the Customers grid.

If the markup in Figure 5 isn’t familiar, don’t panic—it’s just the Kendo UI MVVM (HTML) markup. It simply configures an HTML element, in this case the div with an ID of “grid.” Later on when you bind this view to a view model with the Kendo UI MVVM framework, this markup will be converted to Kendo UI widgets. You can read more on this at bit.ly/1d2Bgfj.

Figure 5 Customer Grid View Markup with an MVVM Widget and Event Bindings

<div class="demo-section">
  <div class="k-content" style="width: 100%">
    <div id="grid"
      data-role="grid"
      data-sortable="true"
      data-pageable="true"
      data-filterable="true"
      data-editable="inline"
      data-selectable="true"
      data-toolbar='[ { template: kendo.template($("#toolbar").html()) } ]'
      data-columns='[
        { field: "CustomerID", title: "ID", width: "75px" },
        { field: "CompanyName", title: "Company"},
        { field: "ContactName", title: "Contact" },
        { field: "ContactTitle", title: "Title" },
        { field: "Address" },
        { field: "City" },
        { field: "PostalCode" },
        { field: "Country" },
        { field: "Phone" },
        { field: "Fax" } ]'
      data-bind="source: dataSource, events:
        { change: onChange, dataBound: onDataBound }">
    </div>
    <style scoped>
    #grid .k-toolbar {
      padding: 15px;
    }
    .toolbar {
      float: right;
    }
    </style>
  </div>
</div>
<script type="text/x-kendo-template" id="toolbar">
  <div>
    <div class="toolbar">
      <span data-role="button" data-bind="click: edit">
        <span class="k-icon k-i-tick"></span>Edit</span>
      <span data-role="button" data-bind="click: destroy">
        <span class="k-icon k-i-tick"></span>Delete</span>
      <span data-role="button" data-bind="click: details">
        <span class="k-icon k-i-tick"></span>Edit Details</span>
    </div>
    <div class="toolbar" style="display:none">
      <span data-role="button" data-bind="click: save">
        <span class="k-icon k-i-tick"></span>Save</span>
      <span data-role="button" data-bind="click: cancel">
        <span class="k-icon k-i-tick"></span>Cancel</span>
    </div>
  </div>
</script>

Creating MVC (OData) Web API Customer Controller (Northwind.Web/Api/CustomerController.cs) Now I’ll show how to create the MVC (OData) Web API Customer controller. OData is a data-access protocol for the Web that provides a uniform way to query and manipulate data sets through CRUD operations. Using ASP.NET Web API, it’s easy to create an OData endpoint. You can control which OData operations are exposed. You can host multiple OData endpoints alongside non-OData endpoints. You have full control over your data model, back-end business logic and data layer. Figure 6 shows the code for the Customer Web API OData controller.

The code in Figure 6 just creates an OData Web API controller to expose Customer data from the Northwind database. Once this is created, you can run the project, and with tools such as Fiddler (a free Web debugger at fiddler2.com) or LINQPad, you can actually query customer data.

Figure 6 Customer Web API OData Controller

public class CustomerController : EntitySetController<Customer, string>
{
  private readonly NorthwindContext _northwindContext;
  public CustomerController()
  {
    _northwindContext = new NorthwindContext();
  }
  public override IQueryable<Customer> Get()
  {
    return _northwindContext.Customers;
  }
  protected override Customer GetEntityByKey(string key)
  {
    return _northwindContext.Customers.Find(key);
  }
  protected override Customer UpdateEntity(string key, Customer update)
  {
    _northwindContext.Customers.AddOrUpdate(update);
    _northwindContext.SaveChanges();
    return update;
  }
  public override void Delete(string key)
  {
    var customer = _northwindContext.Customers.Find(key);
    _northwindContext.Customers.Remove(customer);
    _northwindContext.SaveChanges();
  }
}

Configuring and Exposing OData from the Customer Table for the Grid (Northwind.Web/App_Start/WebApiConfig.cs)Figure 7 configures and exposes OData from the Customer table for the grid.

Querying OData Web API with LINQPad If you haven’t used LINQPad (linqpad.net) yet, please add this tool to your developer toolkit; it’s a must-have and is available in a free version. Figure 8 shows LINQPad with a connection to the Web API OData (localhost:2501/odata), displaying the results of the LINQ query, “Customer.Take (100).”

Figure 7 Configuring ASP.NET MVC Web API Routes for OData

public static void Register(HttpConfiguration config)
{
  // Web API configuration and services
  ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
  var customerEntitySetConfiguration =
    modelBuilder.EntitySet<Customer>("Customer");
  customerEntitySetConfiguration.EntityType.Ignore(t => t.Orders);
  customerEntitySetConfiguration.EntityType.Ignore(t =>
     t.CustomerDemographics);
  var model = modelBuilder.GetEdmModel();
  config.Routes.MapODataRoute("ODataRoute", "odata", model);
  config.EnableQuerySupport();
  // Web API routes
  config.MapHttpAttributeRoutes();
  config.Routes.MapHttpRoute(
    "DefaultApi", "api/{controller}/{id}",
    new {id = RouteParameter.Optional});
}

Querying the Customer Controller Web API OData Via a LINQPad Query
Figure 8 Querying the Customer Controller Web API OData Via a LINQPad Query

Creating the (Observable) Customer Model (Northwind.Web/­Scripts/app/models/customerModel.js) Next is creating the (Kendo UI Observable) Customer model. You can think of this as a client-side Customer entity domain model. I created the Customer model so it can easily be reused by both the Customer grid view and the edit view. The code is shown in Figure 9.

Figure 9 Creating the Customer (Kendo UI Observable) Model

define(['kendo'],
  function (kendo) {
    var customerModel = kendo.data.Model.define({
      id: "CustomerID",
      fields: {
        CustomerID: { type: "string", editable: false, nullable: false },
        CompanyName: { title: "Company", type: "string" },
        ContactName: { title: "Contact", type: "string" },
        ContactTitle: { title: "Title", type: "string" },
        Address: { type: "string" },
        City: { type: "string" },
        PostalCode: { type: "string" },
        Country: { type: "string" },
        Phone: { type: "string" },
        Fax: { type: "string" },
        State: { type: "string" }
      }
    });
    return customerModel;
  });

Creating a DataSource for the Customers Grid (Northwind.Web/Scripts/app/datasources/customersDatasource.js) If you’re familiar with data sources from ASP.NET Web Forms, the concept is the same here, where you create a data source for the Customers grid (Northwind.Web/Scripts/app/datasources/customersDatasource.js). The Kendo UI DataSource (bit.ly/1d0Ycvd) component is an abstraction for using local (arrays of JavaScript objects) or remote (XML, JSON or JSONP) data. It fully supports CRUD data operations and provides both local and server-side support for sorting, paging, filtering, grouping and aggregates.

Creating the View Model for the Customers Grid View If you’re familiar with MVVM from Windows Presentation Foundation (WPF) or Silverlight, this is the same exact concept, just on the client side (found in this project in Northwind.Web/Scripts/ViewModels/­Customer/indexViewModel.cs). MVVM is an architectural separation pattern used to separate the view and its data and business logic. You’ll see in a bit that all the data, business logic and so on is in the view model and that the view is purely HTML (presentation). Figure 10 shows the code for the Customer grid view.

Figure 10 The Customer Grid View Model

define(['kendo', 'customerDatasource'],
  function (kendo, customerDatasource) {
    var lastSelectedDataItem = null;
    var onClick = function (event, delegate) {
      event.preventDefault();
      var grid = $("#grid").data("kendoGrid");
      var selectedRow = grid.select();
      var dataItem = grid.dataItem(selectedRow);
      if (selectedRow.length > 0)
        delegate(grid, selectedRow, dataItem);
      else
        alert("Please select a row.");
      };
      var indexViewModel = new kendo.data.ObservableObject({
        save: function (event) {
          onClick(event, function (grid) {
            grid.saveRow();
            $(".toolbar").toggle();
          });
        },
        cancel: function (event) {
          onClick(event, function (grid) {
            grid.cancelRow();
            $(".toolbar").toggle();
          });
        },
        details: function (event) {
          onClick(event, function (grid, row, dataItem) {
            router.navigate('/customer/edit/' + dataItem.CustomerID);
          });
        },
        edit: function (event) {
          onClick(event, function (grid, row) {
            grid.editRow(row);
            $(".toolbar").toggle();
          });
        },
        destroy: function (event) {
          onClick(event, function (grid, row, dataItem) {
            grid.dataSource.remove(dataItem);
            grid.dataSource.sync();
          });
        },
        onChange: function (arg) {
          var grid = arg.sender;
          lastSelectedDataItem = grid.dataItem(grid.select());
        },
        dataSource: customerDatasource,
        onDataBound: function (arg) {
          // Check if a row was selected
          if (lastSelectedDataItem == null) return;
          // Get all the rows     
          var view = this.dataSource.view();
          // Iterate through rows
          for (var i = 0; i < view.length; i++) {
          // Find row with the lastSelectedProduct
            if (view[i].CustomerID == lastSelectedDataItem.CustomerID) {
              // Get the grid
              var grid = arg.sender;
              // Set the selected row
              grid.select(grid.table.find("tr[data-uid='" + view[i].uid + "']"));
              break;
            }
          }
        },
      });
      return indexViewModel;
  });

I’ll briefly describe various components of the code in Figure 10:

  • onClick (helper): This method is a helper function, which gets an instance of the Customer grid, the current selected row and a JSON model of the representation of the Customer for the selected row.
  • save: This saves changes when doing an inline edit of a Customer.
  • cancel: This cancels out of inline edit mode.
  • details: This navigates the SPA to the edit Customer view, appending the Customer’s ID to the URL.
  • edit: This activates inline editing for the current selected Customer.
  • destroy: This deletes the current selected Customer.
  • onChange (event): This fires every time a Customer is selected. You store the last selected Customer so you can maintain state. After performing any updates or navigating away from the Customer grid, when navigating back to the grid you reselect the last selected Customer.

Now add customerModel, indexViewModel and customersDatasource modules to your RequireJS configuration (Northwind.Web/Scripts/app/main.js). The code is shown in Figure 11.

Figure 11 RequireJS Configuration Additions

paths: {
  // Packages
  'jquery': '/scripts/jquery-2.0.3.min',
  'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
  'text': '/scripts/text',
  'router': '/scripts/app/router',
  // Models
  'customerModel': '/scripts/app/models/customerModel',
  // View models
  'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
  'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
  // Data sources
  'customerDatasource': '/scripts/app/datasources/customerDatasource',
  // Utils
  'util': '/scripts/util'
}

Add a Route for the New Customers Grid View Note that in the loadView callback (in Northwind.Web/Scripts/app/router.js) you’re binding the toolbar of the grid after it has been initialized and MVVM binding has taken place. This is because the first time you bind your grid, the toolbar hasn’t initialized, because it exists in the grid. When the grid is first initialized via MVVM, it will load in the toolbar from the Kendo UI template. When it’s loaded into the grid, you then bind only the toolbar to your view model so the buttons in your toolbar are bound to the save and cancel methods in your view model. Here’s the relevant code to register the route definition for the Customer edit view:

router.route("/customer/index", function () {
  require(['customer-indexViewModel', 'text!/customer/index'],
    function (viewModel, view) {
      loadView(viewModel, view, function () {
        kendo.bind($("#grid").find(".k-grid-toolbar"), viewModel);
      });
    });
});

You now have a fully functional Customers grid view. Load up localhost:25061/Home/Spa#/customer/index (the port number will likely vary on your machine) in a browser and you’ll see Figure 12.

The Customer Grid View with MVVM Using the Index View Model
Figure 12 The Customer Grid View with MVVM Using the Index View Model

Wiring Up the Customers Edit View Here are the key steps to add a Customer edit view to the SPA:

  • Create a customer edit view bound to your Customer model via MVVM (Northwind.Web/Views/Customer/Edit.cshtml).
  • Add an edit view model module for the Customer edit view (Northwind.Web/Scripts/app/viewModels/­editViewModel.js).
  • Add a utility helper module to get IDs from the URL (Northwind.Web/Scripts/app/util.js).

Because you’re using the Kendo UI framework, go ahead and style your edit view with Kendo UI styles. You can learn more about that at bit.ly/1f3zWuC. Figure 13 shows the edit view markup with an MVVM widget and event binding.

Figure 13 Edit View Markup with an MVVM Widget and Event Binding

<div class="demo-section">
  <div class="k-block" style="padding: 20px">
    <div class="k-block k-info-colored">
      <strong>Note: </strong>Please fill out all of the fields in this form.
    </div>
    <div>
      <dl>
        <dt>
          <label for="companyName">Company Name:</label>
        </dt>
        <dd>
          <input id="companyName" type="text"
            data-bind="value: Customer.CompanyName" class="k-textbox" />
        </dd>
        <dt>
          <label for="contactName">Contact:</label>
        </dt>
        <dd>
          <input id="contactName" type="text"
            data-bind="value: Customer.ContactName" class="k-textbox" />
        </dd>
        <dt>
          <label for="title">Title:</label>
        </dt>
        <dd>
          <input id="title" type="text"
            data-bind="value: Customer.ContactTitle" class="k-textbox" />
        </dd>
        <dt>
          <label for="address">Address:</label>
        </dt>
        <dd>
          <input id="address" type="text"
            data-bind="value: Customer.Address" class="k-textbox" />
        </dd>
        <dt>
          <label for="city">City:</label>
        </dt>
        <dd>
          <input id="city" type="text"
            data-bind="value: Customer.City" class="k-textbox" />
        </dd>
        <dt>
          <label for="zip">Zip:</label>
        </dt>
        <dd>
          <input id="zip" type="text"
            data-bind="value: Customer.PostalCode" class="k-textbox" />
        </dd>
        <dt>
          <label for="country">Country:</label>
        </dt>
        <dd>
          <input id="country" type="text"
          data-bind="value: Customer.Country" class="k-textbox" />
        </dd>
        <dt>
          <label for="phone">Phone:</label>
        </dt>
        <dd>
          <input id="phone" type="text"
            data-bind="value: Customer.Phone" class="k-textbox" />
        </dd>
        <dt>
          <label for="fax">Fax:</label>
        </dt>
        <dd>
          <input id="fax" type="text"
            data-bind="value: Customer.Fax" class="k-textbox" />
        </dd>
      </dl>
      <button data-role="button"
        data-bind="click: saveCustomer"
        data-sprite-css-class="k-icon k-i-tick">Save</button>
      <button data-role="button" data-bind="click: cancel">Cancel</button>
      <style scoped>
        dd
        {
          margin: 0px 0px 20px 0px;
          width: 100%;
        }
        label
        {
          font-size: small;
          font-weight: normal;
        }
        .k-textbox
        {
          width: 100%;
        }
        .k-info-colored
        {
          padding: 10px;
          margin: 10px;
        }
      </style>
    </div>
  </div>
</div>

Create a Utility to Get the ID of the Customer from the URL Because you’re creating concise modules with clean boundaries to create a nice separation of concerns, I’ll demonstrate how to create a Util module where all of your utility helpers will reside. I’ll start with a utility method that can retrieve the customer ID in the URL for the Customer DataSource (Northwind.Web/Scripts/app/datasources/customerDatasource.js), as shown in Figure 14.

Figure 14 The Utility Module

define([],
  function () {
    var util;
    util = {
      getId:
      function () {
        var array = window.location.href.split('/');
        var id = array[array.length - 1];
        return id;
      }
    };
    return util;
  });

Add the Edit View Model and Util Modules to the RequireJS Configuration (Northwind.Web/Scripts/app/main.js) The code in Figure 15 shows RequireJS configuration additions for the Customer edit modules.

Figure 15 RequireJS Configuration Additions for the Customer Edit Modules

require.config({
  paths: {
    // Packages
    'jquery': '/scripts/jquery-2.0.3.min',
    'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
    'text': '/scripts/text',
    'router': '/scripts/app/router',
    // Models
    'customerModel': '/scripts/app/models/customerModel',
    // View models
    'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
    'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
    // Data sources
    'customerDatasource': '/scripts/app/datasources/customerDatasource',
    // Utils
    'util': '/scripts/util'
    },
  shim : {
    'kendo' : ['jquery']
  },
  priority: ['text', 'router', 'app'],
  jquery: '2.0.3',
  waitSeconds: 30
  });
require([
  'app'
], function (app) {
  app.initialize();
});

Add the Customer Edit View Model (Northwind.Web/Scripts/app/viewModels/editViewModel.js) The code in Figure 16 shows how to add a Customer edit view model.

Figure 16 Customer Edit View Model Module for the Customer View

define(['customerDatasource', 'customerModel', 'util'],
  function (customerDatasource, customerModel, util) {
    var editViewModel = new kendo.data.ObservableObject({
      loadData: function () {
        var viewModel = new kendo.data.ObservableObject({
          saveCustomer: function (s) {
            customerDatasource.sync();
            customerDatasource.filter({});
            router.navigate('/customer/index');
          },
          cancel: function (s) {
            customerDatasource.filter({});
            router.navigate('/customer/index');
          }
        });
        customerDatasource.filter({
          field: "CustomerID",
          operator: "equals",
          value: util.getId()
        });
        customerDatasource.fetch(function () {
          console.log('editViewModel fetching');
          if (customerDatasource.view().length > 0) {
            viewModel.set("Customer", customerDatasource.at(0));
          } else
            viewModel.set("Customer", new customerModel());
        });
        return viewModel;
      },
    });
    return editViewModel;
  });

I’ll briefly describe various components of the code in Figure 16:

  • saveCustomer: This method is responsible for saving any changes on the Customer. It also resets the DataSource’s filter so the grid will be hydrated with all Customers.
  • cancel: This method will navigate the SPA back to the Customer grid view. It also resets the DataSource’s filter so that the grid will be hydrated with all Customers.
  • filter: This invokes the DataSource’s filter method and queries for a specific Customer by the ID that’s in the URL.
  • fetch: This invokes the DataSource’s fetch method after setting up the filter. In the callback of the fetch, you set the Customer property of your view model with the Customer that was returned from your DataSource fetch, which will be used to bind to your Customer edit view.

When RequireJS loads a module, code within the “define” method body will only get invoked once—which is when RequireJS loads the module—so you expose a method (loadData) in your edit view model so you have a mechanism to load data after the edit view model module has already been loaded (see this in Northwind.Web/­Scripts/app/router.js).

Add a Route for the New Customer Edit View (Northwind.Web/Scripts/app/router.js) Here’s the relevant code to add the router:

router.route("/customer/edit/:id",
        function () {
    require(['customer-editViewModel',
          'text!/customer/edit'],
      function (viewModel, view) {
      loadView(viewModel.loadData(), view);
    });
  });

Note that when the Customer edit view model is requested from RequireJS, you’re able to retrieve the Customer by invoking the loadData method from the view model. This way you’re able to load the correct Customer data based on the ID that’s in the URL each and every time the Customer edit view is loaded. A route doesn’t have to be just a hardcoded string. It can also contain parameters, such as a back-end server router (Ruby on Rails, ASP.NET MVC, Django and so on). To do this, you name a route segment with a colon before the variable name you want.

You can now load the Customer edit view in the browser (localhost:25061/Home/Spa#/customer/edit/ANATR) and see the screen depicted in Figure 17.

The Customer Edit View
Figure 17 The Customer Edit View

Note: Although the delete (destroy) functionality on the Customer grid view has been wired up, when clicking the “Delete” button in the toolbar (see Figure 18), you’ll see an exception, as shown in Figure 19.

The Customer Grid View
Figure 18 The Customer Grid View

Expected Exception When Deleting a Customer Due to CustomerID Foreign Key Referential Integrity
Figure 19 Expected Exception When Deleting a Customer Due to CustomerID Foreign Key Referential Integrity

This exception is by design, because most Customer IDs are foreign keys in other tables, for example, Orders, Invoices and so on. You’d have to wire up a cascading delete that would delete all records from all tables where Customer ID is a foreign key. Although you aren’t able to delete anything, I still wanted to show the steps and code for the delete functionality.

So there you have it. I’ve demonstrated how quick and easy it is to convert an out-of-the-box ASP.NET Web Application into an SPA using RequireJS and Kendo UI. Then I showed how easy it is to add CRUD-like functionality to the SPA.

You can see a live demo of the project at bit.ly/1bkMAlK and you can see the CodePlex project site (and downloadable code) at easyspa.codeplex.com.

Happy coding!


Long Le is the principal .NET app/dev architect at CBRE Inc. and a Telerik/Kendo UI MVP. He spends most of his time developing frameworks and application blocks, providing guidance for best practices and patterns and standardizing the enterprise technology stack. He has been working with Microsoft technologies for more than 10 years. In his spare time, he enjoys blogging (blog.longle.net) and playing Call of Duty. You can reach and follow him on Twitter at twitter.com/LeLong37.

Thanks to the following technical experts for reviewing this article: Derick Bailey (Telerik) and Mike Wasson (Microsoft)