Workings of ASP.NET MVC SPA app

ASP.NET MVC 4 Single Page Application template depends on ASP.NET Web API Data WebHost Nuget package (https://nuget.org/packages/AspNetWebApi.Data) which provides two main things:  Html Metadata helper  and DataController.

Html Metadata helper defined in System.Web.Http.Data.Helpers assembly have signature as given below:

 1: public static IHtmlString Metadata<TDataController>(this HtmlHelper htmlHelper) where TDataController : DataController

The Metadata helper generates metadata that will be used by Upshot.js. It takes a type which should derive from DataController. The DataController is defined in System.Web.Http.Data assembly and derives from ApiController.

The call to Metadata helper is made in the javascript file as :

 1: upshot.metadata(@(Html.Metadata<MvcApplication7.Controllers.MvcApplication7Controller>()));

This generates the following content for the controller.

 1: upshot.metadata({

 2:     "TodoItem:#MvcApplication7.Models": {

 3:         "key": ["TodoItemId"],

 4:         "fields": {

 5:             "IsDone": {

 6:                 "type": "Boolean:#System"

 7:             },

 8:             "Title": {

 9:                 "type": "String:#System"

 10:             },

 11:             "TodoItemId": {

 12:                 "type": "Int32:#System"

 13:             }

 14:         },

 15:         "rules": {

 16:             "Title": {

 17:                 "required": true

 18:             }

 19:         },

 20:         "messages": {}

 21:     }

 22: });

TodoItem.cs

 1: namespace MvcApplication7.Models

 2: {

 3:

 4:     public class TodoItem

 5:     {

 6:         public int TodoItemId { get; set; }

 7:         [Required]

 8:         public string Title { get; set; }

 9:         public bool IsDone { get; set; }

 10:     }

 11: }

MvcApplication7Context

 1: namespace MvcApplication7.Models

 2: {

 3:     public class MvcApplication7Context : DbContext

 4:     {

 5:         public DbSet<TodoItem> TodoItems { get; set; }

 6:     }

 7: }

Using the code generated by SPA template, on creating a new TodoItem a POST request is made. Looking into the POST request we can see the format in which data is submitted.

 1: [{

 2:     "Id": "0",

 3:     "Operation": 1,

 4:     "Entity": {

 5:         "__type": "TodoItem:#MvcApplication7.Models",

 6:         "Title": "To blog on Uphot.js internals"

 7:     }

 8: }]

Update TodoItem request looks like this:

 1: [{

 2:     "Id": "0",

 3:     "Operation": 2,

 4:     "Entity": {

 5:         "__type": "TodoItem:#MvcApplication7.Models",

 6:         "IsDone": false,

 7:         "Title": "Try Knockout.js",

 8:         "TodoItemId": 1

 9:     },

 10:     "OriginalEntity": {

 11:         "__type": "TodoItem:#MvcApplication7.Models",

 12:         "IsDone": false,

 13:         "Title": "Test",

 14:         "TodoItemId": 1

 15:     }

 16: }]

Adding a new ViewModel

Disclaimer: The following code is a learning exercise and not the best or recommended way of developing single page application.

To add a new ViewModel I did following steps:

  1. Added server side data model class “Airport.cs”
  2. Updated context with it’s value
  3. Added Controller methods
  4. Added partial view
  5. Added JavaScript

Model Airport and Context class

 1: public class Airport

 2:     {

 3:         public int AirportId { get; set; }

 4:         [Required]

 5:         public String Code {get;set;}

 6:         public String Name { get; set; }

 7:         public String City { get; set; }

 8:     }

 1: public class MvcApplication7Context : DbContext

 2:    {

 3:         public DbSet<TodoItem> TodoItems { get; set; }

 4:

 5:        public DbSet<Airport> Airports { get; set; }

 6:    }

Controller Action Methods for new model

 1: public partial class MvcApplication7Controller : DbDataController<MvcApplication7.Models.MvcApplication7Context>

 2:     {

 3:         public IQueryable<MvcApplication7.Models.Airport> GetAirports()

 4:         {

 5:             return DbContext.Airports.OrderBy(t => t.AirportId);

 6:         }

 7:

 8:         public void InsertAirport(MvcApplication7.Models.Airport entity)

 9:         {

 10:             InsertEntity(entity);

 11:         }

 12:

 13:         public void UpdateAirport(MvcApplication7.Models.Airport entity)

 14:         {

 15:             UpdateEntity(entity);

 16:         }

 17:

 18:         public void DeleteAirport(MvcApplication7.Models.Airport entity)

 19:         {

 20:             DeleteEntity(entity);

 21:         }

 22:     }

Partial View for creating airport entry

 1: <h2> Add Airport</h2>

 2:

 3: <form data-bind="with: editingAirportEntry, submit: addNewAirport">

 4:     <div class="row" >

 5:             <div class="control-group">

 6:                 <label class="control-label">

 7:                     Name :</label>

 8:                 <div class="controls">

 9:                     <input name="name" type="text"  data-bind="value: Name"  />

 10:                 </div>

 11:             </div>

 12:         </div>

 13:         <div class="row">

 14:             <div class="control-group">

 15:                 <label class="control-label">

 16:                     Code :</label>

 17:                 <div class="controls">

 18:                     <input name="code" type="text"   data-bind="value: Code" />

 19:                 </div>

 20:             </div>

 21:         </div>

 22:         <div class="row">

 23:             <div class="control-group">

 24:                 <label class="control-label">

 25:                     City :</label>

 26:                 <div class="controls">

 27:                     <input name="name" type="text"   data-bind="value: City" />

 28:                 </div>

 29:             </div>

 30:         </div>

 31:

 32:         <div class="row">

 33:         <button type="submit" class="btn">Add</button>

 34:         </div>

 35: </form>

Note here  I have used with and submit knockout bindings.

JavaScript code for ViewModel and Navigation

Add code for mapping raw data to entity
 1: var airportEntityType = "Airport:#MvcApplication7.Models";

 2:

 3:     MyApp.AirportEntry = function (data) {

 4:         var self = this;

 5:         self.AirportId = ko.observable(data.AirportId);

 6:         self.Code = ko.observable(data.Code);

 7:         self.City = ko.observable(data.City);

 8:         self.Name = ko.observable(data.Name);

 9:         upshot.addEntityProperties(self, airportEntityType);

 10:     }

DataSource for Airport Entries
 1: var airportDataSourceOptions = {

 2:     providerParameters: { url: options.serviceUrl, operationName: "GetAirports" },

 3:     entityType: airportEntityType,

 4:     bufferChanges: true,

 5:     mapping: MyApp.AirportEntry

 6: };

 7:

 8: var airportEditorDataSource = new upshot.RemoteDataSource(airportDataSourceOptions);

Data property for airport entry being created or edited
 1: self.editingAirportEntry = airportEditorDataSource.getFirstEntity();

Submit event handler
 1: self.addNewAirport = function (formElement) {

 2:     airportEditorDataSource.commitChanges(function () {

 3:         self.showGrid();

 4:     });

 5: };

Navigation code to show Airport editor form
 1: self.createAirportEntry = function () {

 2:    self.nav.navigate({ edit: "new", entity: 'airport' })

 3: };

 4:

 5: self.nav = new NavHistory({

 6:    params: { edit: null, page: 1, pageSize: 10, entity: 'todo' },

 7:    onNavigate: function (navEntry, navInfo) {

 8:

 9:        if (navEntry.params.entity === 'airport') {

 10:            //reset airport editor data source

 11:            airportEditorDataSource.revertChanges();

 12:            airportEditorDataSource.reset();

 13:            if (navEntry.params.edit) {

 14:                if (navEntry.params.edit === 'new') {

 15:                     //for creating new airport entry

 16:                    airportEditorDataSource.getEntities().push(new MyApp.AirportEntry({}));

 17:                }else{

 18:                     //code for editing exiting entry

 19:                 }

 20:            }

 21:        } else {

 22:             //Todo Item navigation code

 23:        }

 24:    }

 25: }).initialize({ linkToUrl: true });

Binding to show Airport Form and button conditionally
 1: <div data-bind="visible: editingAirportEntry">

 2:     @Html.Partial("_AirportEditor")

 3: </div>

 4: <p><button data-bind="click: createAirportEntry, visible: !editingAirportEntry()">Create Airport Entry</button></p>

Conclusion: We have seen today basic steps to add new views to the Single Page Application.

Advertisements