RemoteDataSource in Upshot.js

RemoteDataSource in Upshot.js can be used to fetch and save data using DataProvider.

Let’s see it’s constructor.

RemoteDataSource takes an options object as single parameter.

The options can have following parameters:

1. bufferChanges: (Optional) If ‘true’, edits to model data are buffered until RemoteDataSource.commitChanges.  Otherwise, edits are committed to the server immediately.
            
2. result:
(Optional) The observable array into which the RemoteDataSource will load model data.
       
3. dataContext:
(Optional) A DataContext instance that acts as a shared cache for multiple DataSource instances.  When not supplied, a DataContext instance is instantiated for this RemoteDataSource.
       
4. entityType:
The type of model data that will be loaded by this RemoteDataSource instance.
       
5. provider:
(Optional) Specifies the DataProvider that will be used to get model data and commit edits to the model data.  Defaults to upshot.DataProvider which works with System.Web.Http.Data.DataController.
       
6. providerParameters:
(Optional) Parameters that are supplied to the DataProvider for this RemoteDataSource and used by the DataProvider when it gets model data from the server.
       
7. mapping:
(Optional) A function (typically a constructor) used to translate raw model data loaded via the DataProvider into model data that will be surfaced by this RemoteDataSource.
       

RemoteDataSource have four public methods:

1. setSort : Establishes the sort specification that is to be applied as part of a server query when loading model data.

Parameter: sort (required)

Should be supplied as an object of the form { property: <propertyName> [, descending: <bool> ] } or an array of ordered objects of this form.
 
When supplied as null or undefined, the sort specification for this RemoteDataSource is cleared.

2. setFilter : Establishes the filter specification that is to be applied as part of a server query when loading model data.

Parameter: filter (required)

Should be supplied as an object of the form { property: <propertyName>, value: <propertyValue> [, operator: <operator> ] } or an array of ordered objects of this form.
 
When supplied as null or undefined, the filter specification for this RemoteDataSource is cleared.

3. refresh: Initiates an asynchronous get to load model data matching the query established with setSort, setFilter and setPaging.

Parameters:

  1. options (optional) : There are no valid options recognized by RemoteDataSource.
  2. success (optional) : A success callback with signature function(entities, totalCount).
  3. error(optional): An error callback with signature function(httpStatus, errorText, context).

OData access using jsonp

The upshot.js ver. 1.0 does not support jsonp calls. To enable jsonp I have modified the DataProvider.OData get method enable jsonp callback like this.

var jsonpCall ;
            if (parameters) {
                operation = parameters.operationName;
                operationParameters = parameters.operationParameters;
                jsonpCall = parameters.jsonpCall ? (parameters.jsonpCall === true): false;
            }
            if(jsonpCall){
              OData.defaultHttpClient.enableJsonpCallback = true;
             }

Using odata provider access Netflix OData :

(function () {
    var Netflix = window.Netflix = window.Netflix || {};

    Netflix.Movie = function (properties) {
        var self = this;

        self.Name = properties.Name;
        self.ShortName = properties.ShortName;
        self.ShortSynopsis = properties.ShortSynopsis;
        self.AverageRating = properties.AverageRating;
        self.ReleaseYear = properties.ReleaseYear;
        self.Runtime = properties.Runtime;

    }
    Netflix.Movie.Type = "Title:#Netflix.Catalog.v2"

    Netflix.CourseViewModel = function (options) {
        var self = this;

        var dataprovider = new upshot.ODataDataProvider();
        var courseDataSourceParameters = {
            provider: dataprovider,
            providerParameters: { url: options.serviceUrl, dataType: 'jsonp', operationName: "Titles", operationParameters: { "$top": "10"}, jsonpCall : true },            
            entityType: Netflix.Movie.Type,
            bufferChanges: true
        };
        var courseDataSource = new upshot.RemoteDataSource(courseDataSourceParameters);
        self.Titles = [];
        courseDataSource.refresh({}, function (entities, totalCount) {
            self.Titles = [];
            $.each(entities, function (index, item) {
                var entity = new Netflix.Movie(item);
                self.Titles.push(entity);
            });

            alert("Total movies loaded " + self.Titles.length);
        });

    }

})();
Advertisements

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.

Upshot.js

This post is part of series of post on Single Page Application using ASP.NET MVC 4. We will explore the upshot, nav and history JavaScript libraries in this post.

What is Upshot.js?

Upshot is a new JavaScript library coming out of Microsoft. It’s based on previously previewed JavaScript library RIA/JS.  It is available through NuGet http://nuget.org/packages/upshot. As it is relatively undocumented, expect some of what is written below to change.

Upshot is for data access and related functionalities like paging, sorting, change tracking. Upshot.js is designed to work with DataController and oData (only data reading).

Objects provided by Upshot

  • EntitySource :  a wrapper of array of Entities which are observable. provides method to bind/unbind events
  • EntityView : inherits from EntitySource and provides change tracking on it
  • DataSource : inherits from EntityView and add paging, sorting and filterting support on it.
  • RemoteDataSource 
  • LocalDataSource
  • DataProvider : a wrapper over jQuery ajax to get and submit entity values
  • ODataDataProvider
  • riaDataProvider
  • EntitySet : inherits from EntitySource and provides change tracking on EntityType. This object does the heavy lifting of managing entities
  • DataContext
  • AssociatedEntitiesView

TODO: Update more about these objects

Usage of Upshot in ASP.NET MVC4 SPA

On creating a Single Page Application controller, we got code generated for the View and ViewModel. In the ViewModel code we can some usage of Upshot.

Creating RemoteDataSource

var gridDataSource = new upshot.RemoteDataSource(dataSourceOptions);

var editorDataSource = new upshot.RemoteDataSource(dataSourceOptions);

The RemoteDataSource are created with dataSourceOptions. DataSource options contains property values for dataProvider parameters ( url and oprationName), entityType , bufferChanges, mapping

Fetching data using DataSource

self.todoItems = gridDataSource.getEntities();

self.editingTodoItem = editorDataSource.getFirstEntity();

In the above code getEntities() method is used to get all TodoItem entities, and getFirstEntity() to get first TodoItem entity.

Another way to load data is to call refresh.

// Load and begin editing an existing todoItem instance

editorDataSource.setFilter({ property: "TodoItemId", value: Number(navEntry.params.edit) }).refresh();

// Not editing, so load the requested page of data to display in the grid

gridDataSource.refresh();

Saving data using DataSource

editorDataSource.commitChanges(function () {

                self.successMessage("Saved TodoItem").errorMessage("");

                self.showGrid();

            });

Deleting data using DataSource

editorDataSource.deleteEntity(todoItem);