CDNs and Fallbacks for Angular Modules

I’ve posted before about CDNs and fallbacks in the context of ASP.NET 5 and MVC 6. I discussed the whys and wherefores, so please read the aforementioned post if you want to understand the details of why CDNs and fallbacks are recommended practice when consuming javascript resources in your web applications.

Bottom line is that consuming javascript libraries from CDNs improves performance both on the web server and on the client browsers. But CDNs don’t come with SLAs and so there is no guarantee that the CDN will be operational when a client requests a resource. We therefore need to include a fallback position which directs the client to request the resource from our own servers when the CDN has failed.

Basic Fallback Test

<!-- Load AngularJS from Google CDN -->
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
<!-- Fallback to local file -->
<script>window.angular || document.write('<script src="scripts/angular.min.js">\x3C/script>')</script> 

The above script attempts to load the angular.min.js library from the Google CDNs.

It then executes a simple OR statement which checks to see if the windows.angular object resolves to true or false.

True indicates that the object exists and the library was successfully loaded from the Google CDN. The OR statement is short-circuited and all is well.

False indicates that the library failed to load. The expression on the second half of the OR statement is resolved, and a script element is written to the document object, directing the browser to request the library from our servers.

Note. All javascript objects can be evaluated in a boolean context, and will either evaluate to true (truthy values) or evaluate to false (falsy values). Falsy values include 0, null, undefined, false, “”, and NaN. All other values are truthy.

In short, the fallback position should check for the existence of a globally scoped object specific to the library in question. And if the object does not exist, then direct the browser to request the library from our servers.

But what if the library does not define any globally scoped objects, and instead only extends objects from another library that it depends on. This is the case with many of the angular UI libraries which extend the windows.angular object by injecting modules. In these cases we need to write a javascript statement that checks for the existence of modules and evaluates to true.

Complex Fallback Test

<!-- Load Angular Bootstrap UI from Google CDN -->
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.3/ui-bootstrap.min.js"></script>
<!-- Fallback to local file -->
<script>
    (function () {
        try {
            window.angular.module('ui.bootstrap');
        }
        catch (e) {
            return false;
        }
        return true;
    })() || document.write('<script src="scripts/ui-bootstrap.min.js">\x3C/script>')
</script>

In the example above, the fallback position uses a self-invoking anonymous function to check to see if the ui.bootstrap module has been injected into the window.angular.module collection. The function incorporates a try and catch block as attempting to reference a module that does not exist results in an errorThe catch block returns a false.if the module does not exist. Otherwise the function returns a true.

ASP.NET MVC 5 FallBack Test

When registering bundles for minification we can specify a CDN path and fallback position.

The ScriptBundle class has 2 arguments, the second of which we can use to register a CDN path. In addition we can assign a fallback expression string to the cdnFallbackExpression property – which I have done in the examples below using object initialization syntax.

If we then set the bundles.UseCdn property to true, and ensure that the web.config compilation debug flag is set to false – the CDN paths will be used to serve up script libraries, and the fallback expressions will be utilized to provide a fallback position.

using System.Web;
using System.Web.Optimization;

namespace MVC5TestApp
{
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            bundles.Add(
                new ScriptBundle(
                    "~/bundles/angular",
                    "//cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.3/angular.min.js") { CdnFallbackExpression = "window.angular" }
                    .Include("~/Scripts/angular.js"));

            bundles.Add(
                new ScriptBundle(
                    "~/bundles/angular-ui-bootstrap",
                    "//cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.3/ui-bootstrap2.min.js")
                        {
                            CdnFallbackExpression = @"   
                                (function () {
                                    try {
                                        window.angular.module('ui.bootstrap');
                                    }
                                    catch (e) {
                                        return false;
                                    }
                                    return true;
                                })()"
                        }
                    .Include("~/Scripts/angular-ui/ui-bootstrap.js"));

            bundles.UseCdn = true;
        }
    }
}

 ASP.NET MVC 6 FallBack Test

The Environment and Script TagHelper classes and attributes were introduced with MVC 6. They enable us to specify different sources for script, and fallback tests, based on the execution environment. For more details follow the link at the top of this post.

<environment names="Development">
    <script src="~/lib/angular/angular.js"></script>
    <script src="~/lib/angular-bootstrap/ui-bootstrap.js"></script>
</environment>
<environment names="Staging,Production">
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.3/angular.min.js"
            asp-fallback-src="~/lib/angular/angular.min.js"
            asp-fallback-test="window.angular">
    </script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.3/ui-bootstrap.min.js"
            asp-fallback-src="~/lib/angular-bootstrap/ui-bootstrap.min.js"
            asp-fallback-test="
                (function() {
                    try {

                        window.angular.module('ui.bootstrap');
                    } catch(e) {
                        return false;
                    }
                    return true;
                })()">
    </script>
</environment>

Server side paging with DataTables.js and MVC

Source code for this article can be found here – https://github.com/RossWhitehead/ServerSidePagingDataTables

When evaluating the usefulness of a javascript grid library the first thing I do is determine whether it is possible to implement server-side paging. This is a must-have feature for most business scenarios, and you will be backing yourself into a corner if you implement a grid that only supports client-side paging.

In a previous post I demonstrated how to implement server side paging with KoGrid. I’m going to repeat this demonstration, but this time for DataTables.NET.

I like KoGrid. It’s based on Knockout, which in my humble opinion is the bees-knees, and my go-to when developing all but the most basic of MVC views.

I also like DataTables.NET.

Both KoGrid and DataTables.NET are widely used and well respected in the community. They meet most demands placed on them, but they differ in terms of their implementation. There’s a good article by Jason Howard here comparing the two. And I agree with most of the statements with the caveat that I am reserving judgement on the assertion that DataTables.NET works with Knockout. I have not yet managed to get the 2 working together when implementing server-side paging. And I will reserve judgment until I have either succeeded or failed miserably.

So onto the implementation of server side paging with DataTables.NET and MVC (without Knockout).

This is what I’m going to build –

BasicDataTable

When initializing the grid, rather than requesting all products from the server, the client is going to request a single page of products. And when the user interacts with the grid – either changing the number of entries to view, selecting a new page, clicking on a column header to sort, clicking on the refresh button, or typing in the search box – the client will update the grid by requesting products one page at a time. In so doing the server will receive a greater number of requests. However, each request will only be for a small page of data, and so should take significantly less resources to meet. Additionally, the client will not be charged with initializing the page with a huge amount of data.

I will be presenting the solution in the following order –

  1. Model
  2. Controller
  3. View
  4. View Model

1. Model

ProductCategory.cs

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ServerSidePagingDataTables.Models
{
    public class ProductCategory
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int ProductCategoryId { get; set; }

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        // Navigational Properties
        public virtual ICollection<Product> Products { get; set; }
    }
}

Product.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ServerSidePagingDataTables.Models
{
    public class Product
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int ProductId { get; set; }

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        [Required]
        [StringLength(1000)]
        public string Description { get; set; }

        [Required]
        public int ProductCategoryId { get; set; }

        // Navigation properties
        public virtual ProductCategory ProductCategory { get; set; }
    }
}

ServerSidePagingDataTablesDbContext.cs

using System.Data.Entity;

namespace ServerSidePagingDataTables.Models
{
    public class ServerSidePagingDataTablesDbContext : DbContext
    {
        public virtual DbSet<Product> Products { get; set; }
        public virtual DbSet<ProductCategory> ProductCategories { get; set; }
    }
}

I am using Entity Framework code-first.

I have 2 entities – ProductCategory and Product, with a one-to-many relationship between them. And I have a DbContext called DataTablesDemoDbContext, which is used to map the model to the database.

2. Controller

using DataTables.Mvc;
using System;
using System.Linq;
using System.Linq.Dynamic;
using System.Web.Mvc;
using ServerSidePagingDataTables.Models;

namespace ServerSidePagingDataTables.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public JsonResult DataTableGet([ModelBinder(typeof(DataTablesBinder))] IDataTablesRequest requestModel)
        {
            var db = new ServerSidePagingDataTablesDbContext();

            IQueryable<Product> query = db.Products;

            var totalCount = query.Count();

            // Apply filters
            if (requestModel.Search.Value != String.Empty)
            {
                var value = requestModel.Search.Value.Trim();
                query = query.Where(p => p.Name.Contains(value) || p.Description.Contains(value));
            }

            var filteredCount = query.Count();

            // Sort
            var sortedColumns = requestModel.Columns.GetSortedColumns();
            var orderByString = String.Empty;

            foreach (var column in sortedColumns)
            {
                orderByString += orderByString != String.Empty ? "," : "";
                orderByString += (column.Data == "Category" ? "ProductCategory.Name" : column.Data) + (column.SortDirection == Column.OrderDirection.Ascendant ? " asc" : " desc");
            }

            query = query.OrderBy(orderByString == String.Empty ? "name asc" : orderByString);

            // Paging
            query = query.Skip(requestModel.Start).Take(requestModel.Length);

            var data = query.Select(p => new
            {
                ProductId = p.ProductId,
                Name = p.Name,
                Description = p.Description,
                Category = p.ProductCategory.Name
            }).ToList();

            return Json(new DataTablesResponse(requestModel.Draw, data, filteredCount, totalCount), JsonRequestBehavior.AllowGet);
        }
    }
}

I have an MVC controller with 2 actions.

Index serves up a .cshtml page that contains the UI markup (view) and the javascript (view model). I do not pass any data to the view – as the view model will be requesting the data once the page has been loaded.

DataTableGet returns a Json response containing the data required to initialize or update the grid. This action will be called by the view model using Ajax when the grid needs updating with new data.

The first thing of note about the DataTableGet action is that it uses a custom model binder, DataTableBinder, to map the request to the IDataTableRequest requestModel parameter. The DataTableGet action also responds with an instance of DataTablesResponse serialized to a Json string.

The aforementioned interfaces and classes can be downloaded from Nuget.

PM> Install-Package datatables.mvc5

The reason we need a custom model binder is that when the DataTable.NET client code makes Ajax requests for new data, the request details, such as the page required, number of records, sort orders, and search strings, are passed using query string parameters. And the naming convention used by DataTables.NET is not compatible with the default MVC model binders, as you can see from the example URL below.

http://localhost:50055/api/datatableproducts?draw=1&columns[0][data]=productId&columns[0][name]=&columns[0][searchable]=true&columns[0][orderable]=true&columns[0][search][value]=&columns[0][search][regex]=false&columns[1][data]=name&columns[1][name]=&columns[1][searchable]=true&columns[1][orderable]=true&columns[1][search][value]=&columns[1][search][regex]=false&columns[2][data]=description&columns[2][name]=&columns[2][searchable]=true&columns[2][orderable]=true&columns[2][search][value]=&
columns[2][search][regex]=false&columns[3][data]=category&columns[3][name]=&columns[3][searchable]=true&columns[3][orderable]=true&columns[3][search][value]=&columns[3][search][regex]=false&order[0][column]=0&order[0][dir]=asc&start=0&length=10&search[value]=&search[regex]=false&_=1437911688049

The DataTableGet action creates an instance of the database context, and constructs a query to grab a page of products, whilst applying the requested filters and sort orders. I have used System.Linq.Dynamic to simplify the construction of the query. This library is not included in an MVC project by default. It needs to be loaded from NuGet.

PM> Install-Package System.linq.Dynamic

3. View

 <link href="//cdn.datatables.net/plug-ins/1.10.7/integration/bootstrap/3/dataTables.bootstrap.css" rel="stylesheet" /> 

I am creating a DataTables.NET grid styled with bootstrap. I therefore need to include the dataTable.bootstrap.css stylesheet in my view.

<style type="text/css">
    .list-panel {
        margin-top: 20px;
    }

        .list-panel .panel-heading {
            overflow: auto;
            padding: 5px 20px;
        }

        .list-panel .panel-title {
            float: left;
            margin-top: 10px;
        }

        .list-panel .panel-body {
            padding: 20px 20px 10px 20px;
        }

        .list-panel .refresh-button {
            float: right;
        }

    .data-table {
        border-collapse: collapse;
        border-spacing: 0;
    }
</style>

And some styling for the panel that grid will sit in.

<div class="row">
    <div class="col-md-12">
        <div class="panel panel-primary list-panel" id="list-panel">
            <div class="panel-heading list-panel-heading">
                <h3 class="panel-title list-panel-title">Products</h3>
                <button type="button" class="btn btn-default btn-md refresh-button" id="refresh-button">
                    <span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Refresh
                </button>
            </div>
            <div class="panel-body">
                <table id="data-table" class="table table-striped table-bordered" style="width:100%"></table>
            </div>
        </div>
    </div>
</div>

The HTML for the grid is a simple <table> element, which I have placed in a bootstrap panel along with a refresh button.

4. View Model

<script src="//cdn.datatables.net/1.10.7/js/jquery.dataTables.min.js"></script>
<script src="//cdn.datatables.net/plug-ins/1.10.7/integration/bootstrap/3/dataTables.bootstrap.js"></script>

The view model includes the DataTables.NET JQuery library (jquery.dataTables.min.js), and additional includes the DataTables,NET Bootstrap integration library (dataTables.bootstrap.js). This library can be omitted if you do not want to use Bootstrap with you grid.

<script type="text/javascript">
    $(function () {
        var productListVM = {
            dt: null,

            init: function () {
                dt = $('#data-table').DataTable({
                    "serverSide": true,
                    "processing": true,
                    "ajax": "/home/datatableget",
                    "columns": [
                        { "title": "Product Id", "data": "ProductId", "searchable": false },
                        { "title": "Name", "data": "Name" },
                        { "title": "Description", "data": "Description" },
                        { "title": "Category", "data": "Category" }
                    ],
                    "lengthMenu": [[2, 5, 10, 25], [2, 5, 10, 25]]
                });
            },

            refresh: function () {
                dt.ajax.reload();
            }
        }

        $('#refresh-button').on("click", productListVM.refresh);

        /////////////////////////////////////////////////////////////////
        // Let's kick it all off
        productListVM.init();
    })
</script>

I have encapsulated the view model functionality into a javascript object called productListVM.

productListVM has a init function which initializes the grid by calling the DataTables.NET DataTable function. It passes in a number of configuration options to define how the grid will look and behave. “serverSide”: true enables server side processing, “processing”: true tells DataTable.NET to display a “Processing..” message when the grid is being updated, and “ajax”: “/home/datatableget” defines the data-source for the Ajax requests. These 3 options are all that are required to implement server side paging. The grid will respond to all relevant user interaction – sorting, filtering, and paging – by automatically requesting the relevant data from the server and updating the grid. No other view model coding is required, and this is the beauty of server side processing with DataTables.NET. Compare this with KoGrid where it is necessary to write code to define subscriptions to a number of events and to construct the request data.

productListVM also has a refresh function, which forces a reload of the grid. The refresh button is assigned a click event handler which call the this function.

And finally, everything thing is kicked off, with the grid being initialized, by calling productListVM.init().

Client and Server Validation with Web API and Knockout

In this post I’m going to demonstrate how to implement both client and server side validation with ASP.NET Web API and Knockout. In particular I’m going to demonstrate how to convey server side binding and validation errors from the controller to the view model, and how to utilize Knockout client side validation features to display both client and server side errors in the view.

Model

My model comprises of 2 domain entities, Product and ProductCategory, with a one-to-many relationship between them.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace WebAPIKo.Models
{
    public class Product
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int ProductId { get; set; }

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        [Required]
        [StringLength(1000)]
        public string Description { get; set; }

        [Required]
        public int ProductCategoryId { get; set; }

        // Navigation properties
        public virtual ProductCategory ProductCategory { get; set; }
    }
}
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace WebAPIKo.Models
{
    public class ProductCategory
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int ProductCategoryId { get; set; }

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        // Navigational Properties
        public virtual ICollection<Product> Products { get; set; }
    }
}

I am using code first Entity Framework as my ORM. Here’s the DbContext containing DbSets for the aforementioned entities –

using System.Data.Entity;

namespace WebAPIKo.Models
{
    public class WebAPIKoDbContext : DbContext
    {
        public virtual DbSet<Product> Products { get; set; }
        public virtual DbSet<ProductCategory> ProductCategories { get; set; }
    }
}

View

My view is a simple “Create Product” form with product name and description text boxes, and a select list for product categories.

create project 2

Here’s the HTML –

<div class="row">
    <div class="col-md-12">
        <div class="panel panel-info" id="add-panel">
            <div class="panel-heading">
                <h2 class="panel-title">Create Product</h2>
            </div>
            <div class="panel-body">
                <form role="form" data-bind="validationOptions: validationOptions">
                    <div class="form-group">
                        <label for="name">Name</label>
                        <input id="name" type="text" class="form-control" data-bind="value: product.name" placeholder="Product Name" />
                    </div>
                    <div class="form-group">
                        <label for="description">Description</label>
                        <input id="description" class="form-control" data-bind="value: product.description" placeholder="Description" />
                    </div>
                    <div class="form-group">
                        <label for="product-category">Product Category</label>
                        <select id="product-category"
                                data-bind="options: productCategories, optionsText: 'name', optionsValue: 'productCategoryId',
                           optionsCaption: '-- Select a Product Category --', value: product.productCategoryId"></select>
                    </div>
                    <button type="button" class="btn btn-primary" data-bind="click: save, enable: canSave">
                        <span class="glyphicon glyphicon-save" aria-hidden="true"></span> Add
                    </button>
                    <ul id="errors" class="error-message" data-bind="foreach: generalErrors">
                        <li><span data-bind="text: $data"></span></li>
                    </ul>
                </form>
            </div>
        </div>
    </div>
</div>

Knockout data-bind attributes are used to declare the binding of the controls to the view model.

Of note are the following –

  • The <form> has a data-bind=”validationOptions: validationOptions” attribute, which applies global validation options to all validatable controls in the form.
  • There is a “errors” <ul> element at the bottom of the form, which has a data-bind=”foreach: generalErrors” attribute.  The <ul> contains a <li> element which in turn contains a <span> with a data-bind=”text: $data” attribute. A <li> will therefore be rendered for each error message in the view model’s generalErrors ko.observableArray property. I am using this to display any errors that are not specific to a field.

Here’s some styles to accompany the form –

<style type="text/css">
    select {
        height: 30px;
        display: block;
    }

    .panel {
        width: 450px;
        margin-top: 15px;
    }

    .panel-heading {
        padding: 10px 20px;
    }

    .panel-body {
        padding: 10px 20px 10px 20px;
    }

    .error-message {
        color: red;
    }

    .error-element {
        border-color: red;
    }

    #errors {
        padding-left: 0;
        padding-top: 5px;
    }

        #errors li {
            list-style-type: none;
        }
</style>

Controller

Before I present the view model I want to discuss Web API controller actions, and in particular the HTTP response header and body that will be returned depending on whether a post/put is successful or not.

With Visual Studio 2013 we have a number of Web API controller scaffolding extensions which provide basic implementation for Web API 2 controllers and actions.

newcontroller

createcontroller2

I have selected the “Web API Controller with actions, using Entity Framework” scaffolder. I am prompted to provide the model class – Product – and the data context class – WebAPIKoDbContext. And a controller is generated with working implementations of for get, post, put, and delete actions.

Here’s the basic implementation for the post action (used for creating new products) –

// POST: api/Product
[ResponseType(typeof(Product))]
public IHttpActionResult PostProduct([FromBody]Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    db.Products.Add(product);
    db.SaveChanges();

    return CreatedAtRoute("DefaultApi", new { id = product.ProductId }, product);
}

This action uses Web API model binding to bind request parameters, from the body of the request, to an instance of the Product entity.

The Product entity has a number of validation attributes, which are assessed during model binding. Any validation errors are added to the ModelState object, which is an instance of System.Web.Http.ModelBinding.ModelStateDictionary. For each validation error, the property name is added to the ModelState’s Keys collection and the error description is added to the ModelState’s Values collection. Each item in the Values collection is actually an array of strings, thus enabling more than one error description per Key.

If there are model binding or validation errors, the action returns BadRequest(ModelState). This results in a HTTP 400 response being sent to the client, along with the response body containing a JSON serialization of a Message string and the ModelState object. Here are some examples of the response body –

In the first example the model binding was unsuccessful as the parameters in the request body did not match any of the Product properties. The model binder was not able to generate the product object.

{   "Message":"The request is invalid.",
    "ModelState":{
        "product":["Error converting value \"ww\" to type 'WebAPIKo.Models.Product'. Path '', line 1, position 4."]}
}

In this second example, the model binder was able to generate the product object, but the name and description properties failed validation as no values were posted for them, whilst they are required.

{   "Message":"The request is invalid.",
    "ModelState":{
        "product.Name":["The Name field is required."],
        "product.Description":["The Description field is required."]}}

In addition to performing this validation on the server, I will also be performing model validation on the client. Validation is one example of where it is necessary to deviate from the DRY principal. If we were only to validate on the server then the user would have to submit the form and wait for a response before being notified of validation errors. This could be very frustrating for the user, and so it is better to have the browser perform validation whilst the user is interacting with the form. But the client side validation should be an addition to rather than a replacement for the server side validation. Firstly, because we cannot guarantee that the request will be correctly formed, and secondly, because not all validation can easily be performed on the client.

For example, if we have such a business rule stating that product names must be unique, and we have many products then we may need to validate the product against this rule on the server. If the product fails this rule we need to communicate this back to the client, and we can utilize the ModelState object to do this. We can also utilize the ModelState object to convey non-field-specific validation errors as shown below.

ModelState.AddModelError("product.Name", "Product name is already in use");

ModelState.AddModelError("General", "General error 1");
ModelState.AddModelError("General", "General error 2");

And resulting in the following response body –

{   "Message":"The request is invalid.",
    "ModelState":{
        "product.Name":["Product name is already in use"],
        "General":["General error 1","General error 2"]}}

Of note is that because we added 2 model errors with the “General” key, then we have 2 items in the array of error descriptions.

One further thing we need to account for in handling errors, is that it is possible that an error occurs on the server during the processing of the request. For example, there may be a network issue with the application temporarily not being able to connect to the database. In this case a HTTP 500 response will be generated with a response body similar to the following –

{   "Message":"An error has occurred.",
    "ExceptionMessage":"An exception has occurred",
    "ExceptionType":"System.Exception"}

So when we have a validation error or exception then we get a HTTP 400 or 500 response with a response body that always contains a “Message” property, and optionally contains a “ModelState” object. Our client code will therefore need to trap HTTP 400 and 500 responses, and parse the “ModelState” object if one exists. And if not, display the “Message”.

View Model

ko.observable.appendError

Knockout validation, by default, appends validation attributes and an error message span after each input control that is bound to a validateable property. For example, the view incorporates the following input control which is bound to the product.name property –

<input id="name" type="text" class="form-control" data-bind="value: product.name" placeholder="Product Name" />

After the bindings are applied, the following HTML is generated by Knockout –

<input id="name" type="text" class="form-control" data-bind="value: name" placeholder="Product Name" title="This field is required." data-orig-title="">
<span class="error-message" style="display: none;"></span>

When the user interacts with the view, entering or modifying data, the view model’s ko.observable properties are automatically updated. The Knockout validation library extends the ko.observable type to include an error property, and a setError method. Each validateable ko.observable property is validated according to it’s validation rules, and the setError method is used to assign a validation error to the error property. The setError method overwrites the value of the error property, and assigns the ko.observable as being not valid.

The mechanics of displaying client validation errors in the view is therefore available out-of-the-box with Knockout validation. We can also hook into these mechanics in order to display validation errors returned from the server.

The first thing I have done to hook into these mechanics is to extend the ko.observable type to include an appendError function, which appends new errors onto existing ones rather than overwriting them. This is needed because there is the possibility of more than one error per property being returned from the server.

///////////////////////////////////////////////////////////////////
// ko.observable.appendError

ko.observable.fn.appendError = function (error) {
    var thisError = this.error();
    if (thisError) {
        this.setError(thisError += '. ' + error);
    }
    else {
        this.setError(error)
    };
};

ProductCategory and ProductCategoryList

The productCategory and productCategoryList types contain properties and functions for the retrieval of a list of product categories from the server. The product categories are used to populate the form’s product category select control.

///////////////////////////////////////////////////////////////////
// ProductCategory

var productCategory = function (productCategoryId, name) {
    var self = this;

    // Properties
    self.productCategoryId = productCategoryId;
    self.name = name;
}

var productCategoryList = function () {
    var self = this;

    self.productCategories = ko.observableArray([]);

    self.get = function (callBack) {
        $.ajax({
            url: '/api/productCategory',
            type: 'get',
            contentType: 'application/json; charset=utf-8',
            success: function (data) {
                $.each(data, function (key, value) {
                    self.productCategories.push(new productCategory(value.ProductCategoryId, value.Name));
                });

                if (typeof callBack !== "undefined") {
                    callBack();
                };
            }
        });
    }
}

ProductVM

I will be binding an instance of ProductVM to the create product view. It is our view model object.

ProductVM  has a self.product object, which is a wrapper for the name, description, and productCategoryId ko.observable properties. I am using product as a wrapper for these properties as it makes the properties easier to work with as a group.

self.product = {
    name: ko.observable(null),
    description: ko.observable(null),
    productCategoryId: ko.observable(0)
}

ProductVM also has self.productCategories which is a ko.observableArray property for storing product categories. An array of product categories is passed into the ProductVM’s constructor during initialization, and this array is in turn passed into the ko.observableArray’s constructor. self.productCategories is used to populate the view’s product category select list.

self.productCategories = ko.observableArray(productCategories);

Next I have the assignment of Knockout validation validation rules to the ko.observable product properties. And the creation of a validation group, which enables us to display, hide, or remove all validation errors.

self.product.name.extend({
    required: true,
    maxLength: 100
});

self.product.description.extend({
    required: true,
    maxLength: 1000
});

self.product.productCategoryId.extend({
    required: true
});

self.errors = ko.validation.group(self.product);

I then define self.generalErrors which is a ko.observableArray that will store all validation errors that are not specific to one of the validatable properties. This array is bound to the errors list towards the bottom of the view using a foreach binding. As such, when it is populated with one or more validation errors, a list item element is created for each, and the messages are displayed.

self.generalErrors = ko.observableArray([]);

The next thing I do is define some validation options. These options are bound, using a validationOptions binding to the form. They are therefore applicable to all input controls in the form.

self.validationOptions = {
    decorateInputElement: true,
    errorElementClass: 'error-element',
    errorMessageClass: 'error-message'
};

Next we have some properties that manage state. The first of these, self.isSaving, is set to true at the start of a save, and then set to false once the save is complete. The next, self.isValid, is a ko.computed property that is set to true when there are no validation error in the errors group, and false when there are. The third property, self.canSave, is a ko.computed property that is set to true when self.isValid, and !self.isSaving. self.canSave is bound to the enable attribute of the view’s add button.

self.isSaving = ko.observable(false);

self.isValid = ko.computed(function () {
    return self.errors().length == 0;
});

self.canSave = ko.computed(function () {
    return self.isValid() && !self.isSaving();
});

The final thing we have in the ProductVM is the save method.

If there are no validation errors the save method sets self.isSaving to true, which has the effect of disabling the view’s add button. This stops the user from invoking concurrent saves. The save method then uses ko.toJSON to generate a JSON string containing the product properties.

The save method then performs an ajax POST to the /api/products url, passing the JSON string in the request body, and if the ajax request is successful then self.isSaving to false.

However, if the ajax request is not successful, and a HTTP 400 response is returned from the server then the following logic is performed –

  • Check to see if there is a ModelState object in the response body.
  • If there is a ModelState object then loop through the keys, and for each key loop through the errors.
  • For each error, if the key matches a validateable property name then append the error to the property.
  • If the key does not match a property name then push the error to the generalErrors observableArray.
  • If the response body does not contain a ModelState object – which is optional – then push the Message string – which is required – to the generalErrors array.

And if a HTTP 500 response is returned then the following logic is performed –

  • The response body will not contain a ModelState object. Push the Message string to the generalErrors array.
self.save = function () {
    if (self.errors().length == 0) {
        self.isSaving(true);

        var dataObject = ko.toJSON(self.product);

        $.ajax({
            url: '/api/products',
            type: 'post',
            data: dataObject,
            dataType: 'json',
            contentType: 'application/json; charset=utf-8',
            success: function (data) {
                self.isSaving(false);
            },
            statusCode: {
                400: function (data) {
                    if (typeof data.responseJSON.ModelState !== 'undefined') {
                        $.each(data.responseJSON.ModelState, function (key, errors) {
                            $.each(errors, function (index, error) {
                                switch (key) {
                                    case 'Name':
                                        self.product.name.appendError(error);
                                        break;
                                    case 'Description':
                                        self.product.description.appendError(error);
                                        break;
                                    case 'ProductCategory':
                                        self.product.productCategoryId.appendError(error);
                                        break;
                                    default:
                                        self.generalErrors.push(error);
                                        break;
                                };
                            });
                        });
                    }
                    else {
                        self.generalErrors.push(data.responseJSON.Message);
                    };
                },
                500: function (data) {
                    self.generalErrors.push(data.statusText + '. Please try again.');
                }
            }
        });
    }
    else {
        self.errors.showAllMessages(true);
    }
};

Because we are appending and pushing the errors to the validatable ko.observable properties and the generalErrors ko.observableArray, and these are bound to the form, then the validation errors will automatically be displayed in the appropriate places in the view.

This is what we will see.

createformerrors

Kick-off

The final thing I have in the view model is the code to kick it all off –

/////////////////////////////////////////////////////////////////
// Let's kick it all off

var productCategoryList = new productCategoryList();

productCategoryList.get(function () {
    var vm = new productVM(productCategoryList.productCategories());
    ko.applyBindings(vm, $addPanel[0]);
    vm.errors.showAllMessages(false);
});

 

For completeness, here’s the full view model.

<script type="text/javascript">
    ///////////////////////////////////////////////////////////////////
    // ko.observable.appendError

    ko.observable.fn.appendError = function (error) {
        var thisError = this.error();
        if (thisError) {
            this.setError(thisError += '. ' + error);
        }
        else {
            this.setError(error)
        };
    };

    $(function () {
        var $addPanel = $('#add-panel');
        ///////////////////////////////////////////////////////////////////
        // ProductCategory

        var productCategory = function (productCategoryId, name) {
            var self = this;

            // Properties
            self.productCategoryId = productCategoryId;
            self.name = name;
        }

        var productCategoryList = function () {
            var self = this;

            self.productCategories = ko.observableArray([]);

            self.get = function (callBack) {
                $.ajax({
                    url: '/api/productCategory',
                    type: 'get',
                    contentType: 'application/json; charset=utf-8',
                    success: function (data) {
                        $.each(data, function (key, value) {
                            self.productCategories.push(new productCategory(value.ProductCategoryId, value.Name));
                        });

                        if (typeof callBack !== "undefined") {
                            callBack();
                        };
                    }
                });
            }
        }

        ///////////////////////////////////////////////////////////////////
        // ProductVM

        var productVM = function (productCategories) {
            var self = this;

            // Properties

            self.product = {
                name: ko.observable(null),
                description: ko.observable(null),
                productCategoryId: ko.observable(0)
            }

            self.productCategories = ko.observableArray(productCategories);

            // Validation

            self.product.name.extend({
                required: true,
                maxLength: 100
            });

            self.product.description.extend({
                required: true,
                maxLength: 1000
            });

            self.product.productCategoryId.extend({
                required: true
            });

            self.errors = ko.validation.group(self.product);

            self.generalErrors = ko.observableArray([]);

            self.validationOptions = {
                decorateInputElement: true,
                errorElementClass: 'error-element',
                errorMessageClass: 'error-message'
            };

            // State

            self.isSaving = ko.observable(false);

            self.isValid = ko.computed(function () {
                return self.errors().length == 0;
            });

            self.canSave = ko.computed(function () {
                return self.isValid() && !self.isSaving();
            });

            // Methods

            self.save = function () {
                if (self.errors().length == 0) {
                    self.isSaving(true);

                    var dataObject = ko.toJSON(self.product);

                    $.ajax({
                        url: '/api/products',
                        type: 'post',
                        data: dataObject,
                        dataType: 'json',
                        contentType: 'application/json; charset=utf-8',
                        success: function (data) {
                            self.isSaving(false);
                        },
                        statusCode: {
                            400: function (data) {
                                if (typeof data.responseJSON.ModelState !== 'undefined') {
                                    $.each(data.responseJSON.ModelState, function (key, errors) {
                                        $.each(errors, function (index, error) {
                                            switch (key) {
                                                case 'product.Name':
                                                    self.product.name.appendError(error);
                                                    break;
                                                case 'product.Description':
                                                    self.product.description.appendError(error);
                                                    break;
                                                case 'product.ProductCategory':
                                                    self.product.productCategoryId.appendError(error);
                                                    break;
                                                default:
                                                    self.generalErrors.push(error);
                                                    break;
                                            };
                                        });
                                    });
                                }
                                else {
                                    self.generalErrors.push(data.responseJSON.Message);
                                };
                            },
                            500: function (data) {
                                self.generalErrors.push(data.statusText + '. Please try again.');
                            }
                        }
                    });
                }
                else {
                    self.errors.showAllMessages(true);
                }
            };
        }

        /////////////////////////////////////////////////////////////////
        // Let's kick it all off

        var productCategoryList = new productCategoryList();

        productCategoryList.get(function () {
            var vm = new productVM(productCategoryList.productCategories());
            ko.applyBindings(vm, $addPanel[0]);
            vm.errors.showAllMessages(false);
        });
    });
</script>

 

Create, Update, and Delete with KoGrid

In my previous post I demonstrated how to implement server-side paging with KoGrid and Knockout within the context of an ASP.NET web application, and utilizing MVC and WebApi controllers. In this post I’m going to demonstrate how to extend the grid to enable the user to create, update, and delete records.

I’m going to add an ‘action’ column to the grid, with edit and delete buttons for each record. And I’m going to add an add button.

kogrid2

When the user clicks on a record’s edit button, an edit panel is displayed. The save button is initially disabled, and will be enabled if the user changes the details and all fields are valid. When the user clicks on the save button the changes will be saved, the edit panel will be hidden, and the grid will be refreshed.

kogrid3

When the user clicks on the add button, an add panel is displayed. The add button is initially disabled and will be enabled when the user has entered valid values for all fields. When the user clicks on the add button, the new record is added, the add panel is hidden, and the grid is refreshed.

kogrid4

And when the user clicks on a delete button, the record will be deleted and the grid refreshed.

As per my previous post I’m going to present the solution in the following order –

  1. Model
  2. Controller
  3. View
  4. ViewModel

1. Model

Domain Model

The domain model remains unchanged, with a one-to-many relationship between product categories and products.

ProductCategory.cs

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace WebAPIKo.Models
{
    public class ProductCategory
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int ProductCategoryId { get; set; }

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        // Navigational Properties
        public virtual ICollection<Product> Products { get; set; }
    }
}

Product.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace WebAPIKo.Models
{
    public class Product
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int ProductId { get; set; }

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        [Required]
        [StringLength(1000)]
        public string Description { get; set; }

        [Required]
        public int ProductCategoryId { get; set; }

        // Navigation properties
        public virtual ProductCategory ProductCategory { get; set; }
    }
}

WebApiDbContext.cs

using System.Data.Entity;

namespace WebAPIKo.Models
{
    public class WebAPIKoDbContext : DbContext
    {
        public virtual DbSet<Product> Products { get; set; }
        public virtual DbSet<ProductCategory> ProductCategories { get; set; }
    }
}

Data Transfer Objects (DTOs)

ProductCategoryDTO.cs

When editing and adding products we need to allow the user to select a product category from a list. We therefore need a ProductCategoryDTO class to transfer product category details to the view model.

namespace WebAPIKo.Models
{
    public class ProductCategoryDTO
    {
        public int ProductCategoryId { get; set; }
        public string Name { get; set; }
    }
}

ProductDTO.cs

I have added ProductCategoryId to the ProductDTO class, as we now need to uniquely identify a product’s category when editing the product.

namespace WebAPIKo.Models
{
    public class ProductDTO
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public int ProductCategoryId { get; set; }
        public string ProductCategoryName { get; set; }
    }
}

ProductListDTO.cs

The ProductListDTO class has not changed. The grid still requires a page of products and a count of the total number of products matching the selected filter.

using System.Collections.Generic;

namespace WebAPIKo.Models
{
    public class ProductListDTO
    {
        public int ProductCount { get; set; }
        public List<ProductDTO> PageOfProducts { get; set; }
    }
}

2. Controller

HomeController.cs

The view/view model – ProductKoGrid.cshtml – is served up by the home controller.

using System.Web.Mvc;

namespace WebAPIKo.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult ProductKoGrid()
        {
            return View();
        }
    }
}

ProductController.cs

In addition to GetProducts, the Product controller has the following actions –

  • GetProduct(int id)
    • Queries the domain model for a Product with the specified id.
    • If not found then returns a 404 (not found) response.
    • If found the returns a 200 (OK) response along with a ProductDTO class containing the product details.
  • PutProduct([FromBody]Product product)
    • Updates an existing product.
    • The [FromBody] attribute tells the controller to map the values contained in the request body to a Product object.
    • If the product is not valid a 400 (bad request) response is returned.
    • An attempt is made to save the changes to the product. If successful a 204 (success, no content) response is returned.
    • If a DbUpdateConcurrencyException occurs whilst saving the changes then either a 404 (not found) response is returned if the product does not exist on the database (it may have been deleted by another user), or a 500 (internal server error) is returned if the product does exist.
  • PostProduct([FromBody]Product product)
    • Adds a new product.
    • The [FromBody] attribute tells the controller to map the values contained in the request body to a Product object.
    • If the product is not valid a 400 (bad request) response is returned.
    • An attempt is made to add the product. If successful a 201 (created) response is returned.
  • DeleteProduct(int id)
    • Queries the domain model for a Product with the specified id.
    • If not found then returns a 404 (not found) response.
    • If found then deletes the product and returns a 200 (OK) response.
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Dynamic;
using System.Net;
using System.Web.Http;
using System.Web.Http.Description;
using WebAPIKo.Models;

namespace WebAPIKo.Controllers
{
    public class ProductsController : ApiController
    {
        private WebAPIKoDbContext db = new WebAPIKoDbContext();

        // GET: api/Product
        public IHttpActionResult GetProducts([FromUri]int page, [FromUri]int pageSize, [FromUri]string filter, [FromUri]string sort)
        {       
            ProductListDTO dto = new ProductListDTO();

            IQueryable<Product> query = db.Products;

            if (filter != null)
            {
                if (filter.Contains(':'))
                {
                    string[] filterArray = filter.Split(':');
                    switch (filterArray[0].ToLower())
                    {
                        case "name":
                            query = query.Where(p => p.Name.Contains(filterArray[1].Trim()));
                            break;
                        case "description":
                            query = query.Where(p => p.Description.Contains(filterArray[1].Trim()));
                            break;
                    }
                }
                else
                {
                    query = query.Where(p => p.Name.Contains(filter.Trim()) || p.Description.Contains(filter.Trim()));
                }
            }

            dto.ProductCount = query.Count();

            query = query.OrderBy(sort == null ? "name asc" : sort).Skip((page - 1) * pageSize).Take(pageSize);

            dto.PageOfProducts = query.Select(p => new ProductDTO()
                {
                    ProductId = p.ProductId,
                    Name = p.Name,
                    Description = p.Description,
                    ProductCategoryId = p.ProductCategoryId,
                    ProductCategoryName = p.ProductCategory.Name
                }
                ).ToList(); 

            return Ok(dto);
        }

        // GET: api/Product/5
        [ResponseType(typeof(Product))]
        public IHttpActionResult GetProduct(int id)
        {
            Product product = db.Products.Find(id);
            if (product == null)
            {
                return NotFound();
            }

            return Ok(new ProductDTO() { ProductId = product.ProductId, Name = product.Name, ProductCategoryId = product.ProductCategoryId });
        }

        // PUT: api/Product/5
        [ResponseType(typeof(void))]
        public IHttpActionResult PutProduct([FromBody]Product product)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            db.Entry(product).State = EntityState.Modified;

            try
            {
                db.SaveChanges();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ProductExists(product.ProductId))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return StatusCode(HttpStatusCode.NoContent);
        }

        // POST: api/Product
        [ResponseType(typeof(Product))]
        public IHttpActionResult PostProduct([FromBody]Product product)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            db.Products.Add(product);
            db.SaveChanges();

            return CreatedAtRoute("DefaultApi", new { id = product.ProductId }, product);
        }

        // DELETE: api/Product/5
        [ResponseType(typeof(Product))]
        public IHttpActionResult DeleteProduct(int id)
        {
            Product product = db.Products.Find(id);
            if (product == null)
            {
                return NotFound();
            }

            db.Products.Remove(product);
            db.SaveChanges();

            return Ok(product);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }

        private bool ProductExists(int id)
        {
            return db.Products.Count(e => e.ProductId == id) > 0;
        }
    }
}

ProductCategoryController.cs

The ProductCategoryController class is an ApiController, containing a single action – GetProductCategories – which returns a list of ProductCategoryDTO objects.

using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using WebAPIKo.Models;

namespace WebAPIKo.Controllers
{
    public class ProductCategoryController : ApiController
    {
        private WebAPIKoDbContext db = new WebAPIKoDbContext();

        // GET: api/ProductCategory
        public IHttpActionResult GetProductCategories()
        {
            db.Configuration.ProxyCreationEnabled = false;

            List<ProductCategoryDTO> dto = new List<ProductCategoryDTO>();

            dto = db.ProductCategories.Select(pc =>
                new ProductCategoryDTO()
                {
                    ProductCategoryId = pc.ProductCategoryId,
                    Name = pc.Name
                }
                ).ToList();

            return Ok(dto);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

3. View

HTML

When the add or edit panel is displayed, we need to simulate modality by disabling the rest of the page. This is achieved by displaying an opaque <div> over the whole page with a z-order that is greater than all elements on the page except for the panel.

<div id="disabling-div"></div>

I have added a footer to the list panel. The footer contains an add button, which has it’s click event bound to the view models “get” method.

<div class="row">
    <div class="col-md-12">
        <div class="panel panel-primary list-panel" id="list-panel">
            <div class="panel-heading list-panel-heading">
                <h3 class="panel-title list-panel-title">Products</h3>
                <button type="button" class="btn btn-default btn-md refresh-button" data-bind="click: get">
                    <span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Refresh
                </button>
            </div>
            <div class="panel-body">
                <div class="gridStyle" data-bind="koGrid: gridOptions"></div>
            </div>
            <div class="panel-footer">
                <button type="button" class="btn btn-primary btn-md" data-bind="click: add">
                    <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add
                </button>
            </div>
            <img src="~/Content/Images/ajax-loader.gif" class="loading-indicator" id="loading-indicator" />
        </div>
    </div>
</div>

I have added 2 separate panels, containing the add and edit forms. It does not matter where on the page these panels are placed as initially they will be hidden and then will be displayed at an absolute position using a SlideDown transition.

Each panel is comprised of the following –

  • A close icon in the panel header, which has it’s click event bound to the respective view model’s cancel() method.
  • A form containing form controls that are bound to the appropriate view model’s properties.
  • The id field is hidden in the add panel, and read-only in the edit.
  • A select control is used to display the product category options. The options are populated from the view model’s productCategories field. We will see in the next section that this field is a ko.observableArray containing productCategory objects. The selected value is bound to the view model’s productCategoryId property.
  • A save button bound to the view model’s cancel() method.
  • The add form has an add button bound to the add view model’s add() method. This button is initially disabled, and will be enabled when the user has completed entering valid values.
  • The edit form has an edit button bound to the edit view model’s update() method. This button is initially disabled and will be enabled when the user has changed at least one value, and all values are valid.
<div class="panel panel-info add-panel" id="add-panel">
    <div class="panel-heading">
        <span class="closeIcon"><span class="glyphicon glyphicon-remove" data-bind="click: cancel"></span></span>
        <h2 class="panel-title">Add New Product</h2>
    </div>
    <div class="panel-body">
        <form id="add-form" role="form">
            <div class="form-group">
                <input id="add-id" type="hidden" class="form-control" data-bind="value: productId" />
            </div>
            <div class="form-group">
                <label for="add-name">Name</label>
                <input id="add-name" type="text" class="form-control" data-bind="value: name" placeholder="Product Name" />
            </div>
            <div class="form-group">
                <label for="add-description">Description</label>
                <input id="add-description" class="form-control" data-bind="value: description" placeholder="Description" />
            </div>
            <div class="form-group">
                <label for="add-product-category">Product Category</label>
                <select id="add-product-category"
                        data-bind="options: productCategories, optionsText: 'name', optionsValue: 'productCategoryId',
                           optionsCaption: '-- Select a Product Category --', value: productCategoryId"></select>
            </div>
            <button type="button" class="btn btn-primary" data-bind="click: add, enable: canSave">
                <span class="glyphicon glyphicon-save" aria-hidden="true"></span> Add
            </button>
            <button type="button" class="btn btn-primary" data-bind="click: cancel">
                <span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Cancel
            </button>
        </form>
    </div>
</div>

<div class="panel panel-info edit-panel" id="edit-panel">
    <div class="panel-heading">
        <span class="closeIcon"><span class="glyphicon glyphicon-remove" data-bind="click: cancel"></span></span>
        <h2 class="panel-title">Edit Product</h2>
    </div>
    <div class="panel-body">
        <form id="edit-form" role="form">
            <div class="form-group">
                <label for="edit-id">Id</label>
                <input id="edit-id" type="hidden" class="form-control" data-bind="value: productId" />
                <p data-bind="text: productId"></p>
            </div>
            <div class="form-group">
                <label for="edit-name">Name</label>
                <input id="edit-name" type="text" class="form-control" data-bind="value: name" placeholder="Product Name" />
            </div>
            <div class="form-group">
                <label for="edit-description">Description</label>
                <input id="edit-description" class="form-control" data-bind="value: description" placeholder="Description" />
            </div>
            <div class="form-group">
                <label for="edit-product-category">Product Category</label>
                <select id="add-product-category"
                        data-bind="options: productCategories, optionsText: 'name', optionsValue: 'productCategoryId',
                           optionsCaption: '-- select a Product Category --', value: productCategoryId"></select>
            </div>
            <button type="button" class="btn btn-primary" data-bind="click: update, enable: canSave">
                <span class="glyphicon glyphicon-save" aria-hidden="true"></span> Save
            </button>
            <button type="button" class="btn btn-primary" data-bind="click: cancel">
                <span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Cancel
            </button>
        </form>
    </div>
</div>

CSS

I have made some small changes to the .gridStyle, kg*, and .listPanel styles, which style the grid and grid panel.

The .addPanel and .editPanel styles, as their names suggest style the add and edit panels. The panels are initially hidden and are positioned towards the top of the page. The z-index is set at 9999. This ensures that they are displayed over the disabling div.

The .closeIcon style is used to position remove glyphicons towards the right hand side of the add and edit panel headers. This simulates a modal close button.

The #disabling-div style positions the disabling div across the whole page, sets it opacity to 50%, and it’s z-index to 1001. The div when visible should then be displayed over all page elements except for the add and edit panels.

<style type="text/css">
    select {
        height: 30px;
        display: block;
    }

    .panel-heading {
        padding: 10px 20px;
    }

    .panel-body {
        padding: 20px 20px 10px 20px;
    }

    .gridStyle {
        border: 1px solid rgb(212,212,212);
        width: 100%;
        height: 300px;
        margin: auto;
    }

    .kgColMenu {
        width: 200px;
    }

        .kgColMenu input[type=text] {
            width: 100%;
        }

    .kgColList {
        padding-left: 25px;
    }

    .kgColListItem label {
        width: 100%;
    }

    .row-button {
        margin-top: 6px;
    }

    .closeIcon {
        position: absolute;
        right: 10px;
        top: 10px;
        font-weight: bold;
        font-family: sans-serif;
        cursor: pointer;
    }

    .add-panel, .edit-panel {
        position: absolute;
        top: 105px;
        width: 400px;
        display: none;
        margin: auto;
        left: 50%;
        margin-left: -200px;
        z-index: 9999;
    }

    .list-panel {
        margin-top: 20px;
    }

        .list-panel .panel-heading {
            overflow: auto;
            padding: 5px 20px;
        }

        .list-panel .panel-title {
            float: left;
            margin-top: 10px;
        }

        .list-panel .panel-footer {
            padding: 5px 20px;
        }

        .list-panel .refresh-button {
            float: right;
        }

    .loading-indicator {
        position: absolute;
        left: 50%;
        top: 50%;
        margin-left: -26px;
        margin-top: -26px;
        display: none;
        z-index: 9999;
    }

    #disabling-div {
        display: none;
        z-index: 1001;
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: white;
        opacity: .50;
        filter: alpha(opacity=50);
    }
</style>

4. ViewModel

Notifier

To keep the code modular, I have created a separate view model for each of the add, edit, and list panels. I want to keep these view models independent, with no knowledge of each other.

For example, the edit view model contains an update() method that communicates changes to the controller via an ajax post. After a successful update, we want the product list to be refreshed, and the list view model contains a get() method to achieve this. We could have the edit view model directly call the list view model’s get() method. But the edit view model would now be dependent on the list view model, making changes more difficult to implement.

A simple answer to our problem is provided by Knockout.

ko.subscribable, is the base object that all Knockout observables derive from. It contains a subscribe() method which enables subscribers to register a function to be called when an event occurs. The subscribe() method accepts three parameters –

  • callBack – the function that is called whenever the event notification occurs.
  • target (optional) – defines the value of this in the callback function.
  • event (optional; default is “change”) – the name of the event.

ko.subscribable also contains a notifySubscribers() method which accepts two parameters –

  • valueToNotify (optional) – value to pass through to the subscribers.
  • event – the names of the event.

I have defined a notifier object to be a global instance of ko.subscribable(), and I have defined some event names –

///////////////////////////////////////////////////////////////////
// Notifier
var notifier = new ko.subscribable();
var PRODUCT_UPDATED = "PRODUCT_UPDATED";
var INVOKE_ADD = "INVOKE_ADD";
var INVOKE_EDIT = "INVOKE_EDIT";

And for our example where we want the list view model’s get() method to be called on successful completion of the edit view model’s update() – the list view model registers it’s get() method using the following code –

notifier.subscribe(self.get, null, PRODUCT_UPDATED);

And the edit view model raises the notification on successful completion of the update() method using the following code –

notifier.notifySubscribers(null, PRODUCT_UPDATED);

Transitions

The add and edit panels are initially hidden. When the user chooses to add a new record, or edit an existing record, we need to reveal the relevant panel, whilst displaying the disabling div that obscures the rest of the view.

I have created a transition object to encapsulate this logic. It has 2 methods – show() and hide(), both of which receive a target and an optional callBack function.

///////////////////////////////////////////////////////////////////////////
// Transitions

var transition = {

    duration: 200,

    show: function (target, callBack) {
        if (target.is(':hidden')) {
            $disablingDiv.show();
            target.slideDown(transition.duration, function () {
                target.find('input[type!=hidden]:first').focus();
                if (typeof callBack !== "undefined") {
                    callBack();
                }
            });
        }
        else if (typeof callBack !== "undefined") {
            callBack();
        }
    },

    hide: function (target, callBack) {
        if (target.is(':visible')) {
            $disablingDiv.hide();
            target.slideUp(transition.duration, callBack);
        }
        else if (typeof callBack !== "undefined") {
            callBack();
        }
    }
}

ProductCategory and ProductCategoryList

The productCategoryList object is comprised of a ko.observableArray, and a get() method.

The get() method retrieves product categories via an ajax get call, and for each creates an instance of productCategory and pushes it to the ko.observableArray.

I have added callBack as an optional argument to the get() method, and will be using this to ensure that a complete list of product categories has been populated prior to binding the add and edit view models.

///////////////////////////////////////////////////////////////////
// ProductCategory

var productCategory = function (productCategoryId, name) {
    var self = this;

    // Properties
    self.productCategoryId = productCategoryId;
    self.name = name;
}

var productCategoryList = function () {
    var self = this;

    self.productCategories = ko.observableArray([]);

    self.get = function (callBack) {
        $.ajax({
            url: '/api/productCategory',
            type: 'get',
            contentType: 'application/json; charset=utf-8',
            success: function (data) {
                $.each(data, function (key, value) {
                    self.productCategories.push(new productCategory(value.ProductCategoryId, value.Name));
                });

                if (typeof callBack !== "undefined") {
                    callBack();
                };
            }
        });
    }
}

Product

The product object type is a data container, with ko.observable properties. The list view model has a ko.observableArray containing product objects, which is bound to the grid. Also, as discussed in the next section, the add product and edit product view models are based on the product type.

///////////////////////////////////////////////////////////////////
// Product

var product = function (data) {
    var self = this;

    self.productId = ko.observable(data.productId || 0);
    self.name = ko.observable(data.name || null);
    self.description = ko.observable(data.description || null);
    self.productCategoryId = ko.observable(data.productCategoryId || 0);
    self.productCategoryName = ko.observable(data.productCategoryName || null);
}

ProductVM

The productVM object type is an extension of product, and is comprised of data, configuration properties, and methods shared by the add and edit product view models.

///////////////////////////////////////////////////////////////////
// ProductVM

var productVM = function (productCategories, panel) {
    var self = this;

    // Functional Inheritance

    ko.utils.extend(self, new product({}));

    // Properties

    self.productCategories = ko.observableArray(productCategories);

    // Validation

    self.name.extend({
        required: true,
        maxLength: 100
    });

    self.description.extend({
        required: true,
        maxLength: 1000
    });

    self.productCategoryId.extend({
        required: true
    });

    self.errors = ko.validation.group([self.productId, self.name, self.description, self.productCategoryId]);

    // State

    self.hasChanged = ko.observable(false);
    self.isSaving = ko.observable(false);

    self.isValid = ko.computed(function () {
        return self.errors().length == 0;
    });

    self.canSave = ko.computed(function () {
        return self.isValid() && self.hasChanged() && !self.isSaving();
    });

    // Subscriptions

    self.name.subscribe(function (value) {
        self.hasChanged(true);
    });

    self.description.subscribe(function (value) {
        self.hasChanged(true);
    });

    self.productCategoryId.subscribe(function (value) {
        self.hasChanged(true);
    });

    // Methods

    self.initialize = function (data) {
        if (data != null) {
            self.productId(data.productId() || 0);
            self.name(data.name() || null);
            self.description(data.description() || null);
            self.productCategoryId(data.productCategoryId() || 0);
        }
        else {
            self.errors.showAllMessages(false);
        }

        self.hasChanged(false);

        self.show();
    };

    self.cancel = function () {
        self.hide();
    };

    self.show = function () {
        transition.show(panel);
    }

    self.hide = function () {
        // Hide and tidy up
        transition.hide(panel, function () {
            self.productId(0);
            self.name(null);
            self.description(null);
            self.hasChanged(false);
            self.errors.showAllMessages(false);
        });
    }
}

productVM is comprised of the following –

  • A constructor that takes 2 arguments – productCategories which is a collection of productCategory objects, and panel which is the JQuery reference to either the add or edit panel.
  • A self.productCategories property of type ko.observableArray. The productCategories argument is passed into it’s constructor, and it is used to populate a select list when the view model is bound to the view.
  • Extension calls which add Knockout validation configuration information to the product properties.
  • A Knockout validation group, self.errors, containing the properties that require validation.
  • Properties used for tracking state –
    • self.hasChanged – tracks whether one or more properties have been changed.
    • self.isSaving – tracks whether the view model is currently saving. This is necessary as the save operations are asynchronous.
    • self.isValid – tracks whether the product is valid. This is a computed based on the number of validation errors in the self.errors validation group.
    • self.canSave – tracks whether the product can be saved, and is computed as self.isValid() && self.hasChanged && !self.isSaving.
  • Subscriptions which assign functions that set self.hasChanged(true) in response to a change to one of the product properties.
  • A self.initialize method that initializes the state of the view model, including setting product properties. The reason I have an initialize method to set the state, rather than using the view model’s constructor, is that I want to create a single instance of each the add and edit view models and bind them against the view once. But then I want to reuse these view models for adding and editing products consecutively without having to rebind.
  • A self.cancel() function which is bound to the add and edit panel cancel buttons. The self.cancel function calls the self.hide() function.
  • A self.show() function which utilizes the transition object to display the panel. I will describe how and when this method gets executed when I discuss the productAddVM and productEditVM implementations.
  • A self.hide() function which utilizes the transition object to hide the panel, and includes some tidy-up code in a callBack function.

ProductAddVM

productAddVM is the view model that is bound to the add product panel. It extends productVM, and adds in functionality that is specific to adding products.

///////////////////////////////////////////////////////////////////
// ProductAddVM

var productAddVM = function (productCategories) {
    var self = this;

    var panel = $addPanel;

    // Functional Inheritance

    ko.utils.extend(self, new productVM(productCategories, panel));

    // Subscriptions

    notifier.subscribe(self.initialize, null, INVOKE_ADD);

    // Methods

    self.add = function () {

        if (self.errors().length == 0) {

            self.isSaving(true);

            var dataObject = ko.toJSON(this);

            $.ajax({
                url: productsUri,
                type: 'post',
                data: dataObject,
                dataType: 'json',
                contentType: 'application/json; charset=utf-8',
                success: function (data) {
                    self.isSaving(false);

                    self.hide();

                    notifier.notifySubscribers(null, PRODUCT_UPDATED);
                }
            });
        }
        else {
            self.errors.showAllMessages();
        }
    };
}

productAddVM is comprised of the following –

  • A constructor that takes a productCategories argument, which is then passed through to the base productVM and assigned to self.productCategories. I have taken the decision to retrieve a set of product categories from the server when the page is initialized, and then pass these into the productAddVM when they are constructed. I am assuming that the product categories will not be changed during the life-time of the page.
  • A JQuery reference to the add panel. This is also passed through to the base productVM and used by the show and hide methods.
  • A notifier subscription, which requests the execution of the self.initialize method when the INVOKE_ADD event notification is triggered. The list view model will trigger this event when the user clicks on the grid’s add button.
  • An update() method which utilizes a ajax put to pass the new product details to the server. On successful execution the method hides the panel and triggers the PRODUCT_UPDATED event notification.

ProductEditVM

productEditVM is bound to the edit panel, and also extends productVM. It contains functionality that is specific to editing products.

///////////////////////////////////////////////////////////////////
// ProductEditVM

var productEditVM = function (productCategories) {
    var self = this;

    var panel = $editPanel;

    // Functional Inheritance

    ko.utils.extend(self, new productVM(productCategories, panel));

    // Subscriptions

    notifier.subscribe(self.initialize, null, INVOKE_EDIT);

    // Methods

    self.update = function () {
        if (self.errors().length == 0) {

            self.isSaving(true);

            var dataObject = ko.toJSON(this);

            $.ajax({
                url: productsUri,
                type: 'put',
                data: dataObject,
                dataType: 'json',
                contentType: 'application/json; charset=utf-8',
                success: function (data) {
                    self.isSaving(false);

                    self.hide();

                    notifier.notifySubscribers(null, PRODUCT_UPDATED);
                }
            });
        }
        else {
            self.errors.showAllMessages();
        }
    };
}

It is comprised of the following –

  • A constructor that takes a productCategories argument, which is passed through to the base productVM.
  • A JQuery reference to the edit panel. This is also passed through to the base productVM and used by the show and hide methods.
  • A notifier subscription, which requests the execution of the self.initialize method when the INVOKE_EDIT event notification is triggered. The list view model will trigger this event when the user clicks on the one of grid’s edit buttons.
  • An update() method which utilizes an ajax post to send product updates to the server. On successful execution the method hides the panel and triggers the PRODUCT_UPDATED event notification.

ProductListVM

prodictListVM  is the view model that is bound to the product grid.

///////////////////////////////////////////////////////////////////
// ProductListVM

var productListVM = function () {
    var self = this;

    // Properties
    self.products = ko.observableArray([]);

    self.columnDefs = [
        { field: 'productId', displayName: 'Id', width: 80 },
        { field: 'name', displayName: 'Name', width: 200 },
        { field: 'description', displayName: 'Description' },
        { field: 'productCategoryName', displayName: 'Product Category', width: 200 },
        { field: 'productId', displayName: ' ', cellTemplate: $('#editDeleteCellTemplate').html(), width: 150, sortable: false }
    ];

    self.filterOptions = {
        filterText: ko.observable(""),
        useExternalFilter: false
    };

    self.pagingOptions = {
        currentPage: ko.observable(1),
        pageSizes: ko.observableArray([2, 5, 10, 20, 50]),
        pageSize: ko.observable(2),
        totalServerItems: ko.observable(0)
    };

    self.sortInfo = ko.observable({ column: { 'field': 'name' }, direction: 'asc' });

    self.gridOptions = {
        data: self.products,
        columnDefs: self.columnDefs,
        autogenerateColumns: false,
        showGroupPanel: true,
        canSelectRows: false,
        showFilter: true,
        filterOptions: self.filterOptions,
        enablePaging: true,
        pagingOptions: self.pagingOptions,
        sortInfo: self.sortInfo,
        rowHeight: 35
    };

    // Subscriptions

    self.pagingOptions.pageSize.subscribe(function (data) {
        self.pagingOptions.currentPage(1);
        self.get();
    });

    self.pagingOptions.currentPage.subscribe(function (data) {
        self.get();
    });

    self.sortInfo.subscribe(function (data) {
        self.pagingOptions.currentPage(1);
        self.get();
    });

    notifier.subscribe(self.get, null, PRODUCT_UPDATED);

    // Methods

    self.get = function () {
        $loadingIndicator.show();

        $.ajax({
            url: productsUri,
            type: 'get',
            data: {
                'page': self.pagingOptions.currentPage(),
                'pageSize': self.pagingOptions.pageSize(),
                'filter': self.filterOptions.filterText == undefined ? '' : self.filterOptions.filterText(),
                'sort': self.sortInfo().column.field + ' ' + self.sortInfo().direction
            },
            contentType: 'application/json; charset=utf-8',
            success: function (data) {
                self.pagingOptions.totalServerItems(data.ProductCount);

                var productsArray = [];
                $.each(data.PageOfProducts, function (key, value) {
                    productsArray.push(
                        new product({
                            productId: value.ProductId,
                            name: value.Name,
                            description: value.Description,
                            productCategoryId: value.ProductCategoryId,
                            productCategoryName: value.ProductCategoryName
                        }));
                });
                self.products(productsArray);

                $loadingIndicator.hide();
            }
        });
    };

    self.add = function () {
        notifier.notifySubscribers(null, INVOKE_ADD);
    };

    self.edit = function (item) {
        notifier.notifySubscribers(item, INVOKE_EDIT);
    };

    self.delete = function (item) {

        bootbox.confirm("Are you sure?", function (result) {
            Example.show("Confirm result: " + result);
        });
        if (confirm("Are you sure you want to delete " + item.name() + "?")) {
            var dataObject = JSON.stringify({ id: item.productId() });

            $.ajax({
                url: productsUri + '/' + item.productId(),
                type: 'delete',
                dataType: 'json',
                contentType: 'application/json; charset=utf-8',
                success: function (data) {
                    self.get();
                }
            });
        }
    }
}

In addition to the functionality described in the previous blog post, productListVM is comprised of the following –

  • An additional column definition, which generates the column containing edit and delete buttons for each product. The column is generated based on a editDeleteCellTemplate column template. this template is outlined in the nest section.
  • A notifier subscription, registering the get() method to be called when the PRODUCT_UPDATED event is invoked. This event is invoked by either the add or edit view model when a product is updated. The grid is therefore refreshed in response to a product being updated.
  • An add() method, which invokes the INVOKE_ADD notification. This method is bound to the add buttons click event.
  • An edit() method, which invokes the INVOKE_EDIT notification. This method is bound to the grid’s edit buttons click event, and passes through the view model item specific to the button that was clicked.
  • A delete() method, which asked for conformation, and then uses an ajax delete call to delete the product. On successfull completion the get() method is called and hence the grid is refreshed.

Edit/Delete Cell Template

The following html template is used by the list view model to generate the column containing the edit and delete buttons –

<script type="text/template" id="editDeleteCellTemplate">
    <div>
        <button type="button" class="btn btn-primary btn-xs row-button" data-bind="click: function() { $parent.$userViewModel.edit($parent.entity); }">
            <span class="glyphicon glyphicon-edit" aria-hidden="true"></span> Edit
        </button>
        <button type="button" class="btn btn-danger btn-xs row-button" data-bind="click: function() { $parent.$userViewModel.delete($parent.entity); }">
            <span class="glyphicon glyphicon-minus" aria-hidden="true"></span> Delete
        </button>
    </div>
</script>

Kicking it all off

First thing I do is create an instance of productListVM, and bind this instance to the list panel.

I then create an instance of productCategoryList, and call the get() method, which gets the product categories from the server.

I pass a callBack function to the productCategoryList.get() method, to ensure that the full list is retrieved prior to performing the following –

  • create a new instance of productAddVM, passing the product category list into the constructor, and binding this instance to the add panel.
  • create a new instance of productEditVM, passing the product category list into the constructor, and binding this instance to the edit panel.
  • and finally, call the prroductListVM.get() method to populate the product grid.
/////////////////////////////////////////////////////////////////
// Let's kick it all off

var productListVM = new productListVM();
ko.applyBindings(productListVM, $listPanel[0]);

var productCategoryList = new productCategoryList();

productCategoryList.get(function () {
    ko.applyBindings(new productAddVM(productCategoryList.productCategories()), $addPanel[0]);
    ko.applyBindings(new productEditVM(productCategoryList.productCategories()), $editPanel[0]);
    productListVM.get();
});

For completeness, here’s the full View Model code –

<script type="text/template" id="editDeleteCellTemplate">
<div>
    <button type="button" class="btn btn-primary btn-xs row-button" data-bind="click: function() { $parent.$userViewModel.edit($parent.entity); }">
        <span class="glyphicon glyphicon-edit" aria-hidden="true"></span> Edit
    </button>
    <button type="button" class="btn btn-danger btn-xs row-button" data-bind="click: function() { $parent.$userViewModel.delete($parent.entity); }">
        <span class="glyphicon glyphicon-minus" aria-hidden="true"></span> Delete
    </button>
</div>
</script>

<script type="text/javascript">
$(function () {
    var $listPanel = $('#list-panel');
    var $addPanel = $('#add-panel');
    var $editPanel = $('#edit-panel');

    var $disablingDiv = $('#disabling-div');
    var $loadingIndicator = $('#loading-indicator');

    var productsUri = '/api/products';

    ///////////////////////////////////////////////////////////////////
    // Notifier
    var notifier = new ko.subscribable();
    var PRODUCT_UPDATED = "PRODUCT_UPDATED";
    var INVOKE_ADD = "INVOKE_ADD";
    var INVOKE_EDIT = "INVOKE_EDIT";

    ///////////////////////////////////////////////////////////////////
    // ProductCategory

    var productCategory = function (productCategoryId, name) {
        var self = this;

        // Properties
        self.productCategoryId = productCategoryId;
        self.name = name;
    }

    var productCategoryList = function () {
        var self = this;

        self.productCategories = ko.observableArray([]);

        self.get = function (callBack) {
            $.ajax({
                url: '/api/productCategory',
                type: 'get',
                contentType: 'application/json; charset=utf-8',
                success: function (data) {
                    $.each(data, function (key, value) {
                        self.productCategories.push(new productCategory(value.ProductCategoryId, value.Name));
                    });

                    if (typeof callBack !== "undefined") {
                        callBack();
                    };
                }
            });
        }
    }

    ///////////////////////////////////////////////////////////////////
    // Product

    var product = function (data) {
        var self = this;

        self.productId = ko.observable(data.productId || 0);
        self.name = ko.observable(data.name || null);
        self.description = ko.observable(data.description || null);
        self.productCategoryId = ko.observable(data.productCategoryId || 0);
        self.productCategoryName = ko.observable(data.productCategoryName || null);
    }

    ///////////////////////////////////////////////////////////////////
    // ProductVM, ProductAddVM, and ProductEditVM

    var productVM = function (productCategories, panel) {
        var self = this;

        // Functional Inheritance

        ko.utils.extend(self, new product({}));

        // Properties

        self.productCategories = ko.observableArray(productCategories);

        // Validation

        self.name.extend({
            required: true,
            maxLength: 100
        });

        self.description.extend({
            required: true,
            maxLength: 1000
        });

        self.productCategoryId.extend({
            required: true
        });

        self.errors = ko.validation.group([self.productId, self.name, self.description, self.productCategoryId]);

        // State

        self.hasChanged = ko.observable(false);
        self.isSaving = ko.observable(false);

        self.isValid = ko.computed(function () {
            return self.errors().length == 0;
        });

        self.canSave = ko.computed(function () {
            return self.isValid() && self.hasChanged() && !self.isSaving();
        });

        // Subscriptions

        self.name.subscribe(function (value) {
            self.hasChanged(true);
        });

        self.description.subscribe(function (value) {
            self.hasChanged(true);
        });

        self.productCategoryId.subscribe(function (value) {
            self.hasChanged(true);
        });

        // Methods

        self.initialize = function (data) {
            if (data != null) {
                self.productId(data.productId() || 0);
                self.name(data.name() || null);
                self.description(data.description() || null);
                self.productCategoryId(data.productCategoryId() || 0);
            }
            else {
                self.errors.showAllMessages(false);
            }

            self.hasChanged(false);

            self.show();
        };

        self.cancel = function () {
            self.hide();
        };

        self.show = function () {
            transition.show(panel);
        }

        self.hide = function () {
            // Hide and tidy up
            transition.hide(panel, function () {
                self.productId(0);
                self.name(null);
                self.description(null);
                self.hasChanged(false);
                self.errors.showAllMessages(false);
            });
        }
    }

    var productAddVM = function (productCategories) {
        var self = this;

        var panel = $addPanel;

        // Functional Inheritance

        ko.utils.extend(self, new productVM(productCategories, panel));

        // Subscriptions

        notifier.subscribe(self.initialize, null, INVOKE_ADD);

        // Methods

        self.add = function () {

            if (self.errors().length == 0) {

                self.isSaving(true);

                var dataObject = ko.toJSON(this);

                $.ajax({
                    url: productsUri,
                    type: 'post',
                    data: dataObject,
                    dataType: 'json',
                    contentType: 'application/json; charset=utf-8',
                    success: function (data) {
                        self.isSaving(false);

                        self.hide();

                        notifier.notifySubscribers(null, PRODUCT_UPDATED);
                    }
                });
            }
            else {
                self.errors.showAllMessages();
            }
        };
    }

    var productEditVM = function (productCategories) {
        var self = this;

        var panel = $editPanel;

        // Functional Inheritance

        ko.utils.extend(self, new productVM(productCategories, panel));

        // Subscriptions

        notifier.subscribe(self.initialize, null, INVOKE_EDIT);

        // Methods

        self.update = function () {
            if (self.errors().length == 0) {

                self.isSaving(true);

                var dataObject = ko.toJSON(this);

                $.ajax({
                    url: productsUri,
                    type: 'put',
                    data: dataObject,
                    dataType: 'json',
                    contentType: 'application/json; charset=utf-8',
                    success: function (data) {
                        self.isSaving(false);

                        self.hide();

                        notifier.notifySubscribers(null, PRODUCT_UPDATED);
                    }
                });
            }
            else {
                self.errors.showAllMessages();
            }
        };
    }

    ///////////////////////////////////////////////////////////////////
    // ProductListVM

    var productListVM = function () {
        var self = this;

        // Properties
        self.products = ko.observableArray([]);

        self.columnDefs = [
            { field: 'productId', displayName: 'Id', width: 80 },
            { field: 'name', displayName: 'Name', width: 200 },
            { field: 'description', displayName: 'Description' },
            { field: 'productCategoryName', displayName: 'Product Category', width: 200 },
            { field: 'productId', displayName: ' ', cellTemplate: $('#editDeleteCellTemplate').html(), width: 150, sortable: false }
        ];

        self.filterOptions = {
            filterText: ko.observable(""),
            useExternalFilter: false
        };

        self.pagingOptions = {
            currentPage: ko.observable(1),
            pageSizes: ko.observableArray([2, 5, 10, 20, 50]),
            pageSize: ko.observable(2),
            totalServerItems: ko.observable(0)
        };

        self.sortInfo = ko.observable({ column: { 'field': 'name' }, direction: 'asc' });

        self.gridOptions = {
            data: self.products,
            columnDefs: self.columnDefs,
            autogenerateColumns: false,
            showGroupPanel: true,
            canSelectRows: false,
            showFilter: true,
            filterOptions: self.filterOptions,
            enablePaging: true,
            pagingOptions: self.pagingOptions,
            sortInfo: self.sortInfo,
            rowHeight: 35
        };

        // Subscriptions

        self.pagingOptions.pageSize.subscribe(function (data) {
            self.pagingOptions.currentPage(1);
            self.get();
        });

        self.pagingOptions.currentPage.subscribe(function (data) {
            self.get();
        });

        self.sortInfo.subscribe(function (data) {
            self.pagingOptions.currentPage(1);
            self.get();
        });

        notifier.subscribe(self.get, null, PRODUCT_UPDATED);

        // Methods

        self.get = function () {
            $loadingIndicator.show();

            $.ajax({
                url: productsUri,
                type: 'get',
                data: {
                    'page': self.pagingOptions.currentPage(),
                    'pageSize': self.pagingOptions.pageSize(),
                    'filter': self.filterOptions.filterText == undefined ? '' : self.filterOptions.filterText(),
                    'sort': self.sortInfo().column.field + ' ' + self.sortInfo().direction
                },
                contentType: 'application/json; charset=utf-8',
                success: function (data) {
                    self.pagingOptions.totalServerItems(data.ProductCount);

                    var productsArray = [];
                    $.each(data.PageOfProducts, function (key, value) {
                        productsArray.push(
                            new product({
                                productId: value.ProductId,
                                name: value.Name,
                                description: value.Description,
                                productCategoryId: value.ProductCategoryId,
                                productCategoryName: value.ProductCategoryName
                            }));
                    });
                    self.products(productsArray);

                    $loadingIndicator.hide();
                }
            });
        };

        self.add = function () {
            notifier.notifySubscribers(null, INVOKE_ADD);
        };

        self.edit = function (item) {
            notifier.notifySubscribers(item, INVOKE_EDIT);
        };

        self.delete = function (item) {

            bootbox.confirm("Are you sure?", function (result) {
                Example.show("Confirm result: " + result);
            });
            if (confirm("Are you sure you want to delete " + item.name() + "?")) {
                var dataObject = JSON.stringify({ id: item.productId() });

                $.ajax({
                    url: productsUri + '/' + item.productId(),
                    type: 'delete',
                    dataType: 'json',
                    contentType: 'application/json; charset=utf-8',
                    success: function (data) {
                        self.get();
                    }
                });
            }
        }
    }

    ///////////////////////////////////////////////////////////////////////////
    // Transitions

    var transition = {

        duration: 200,

        show: function (target, callBack) {
            if (target.is(':hidden')) {
                $disablingDiv.show();
                target.slideDown(transition.duration, function () {
                    target.find('input[type!=hidden]:first').focus();
                    if (typeof callBack !== "undefined") {
                        callBack();
                    }
                });
            }
            else if (typeof callBack !== "undefined") {
                callBack();
            }
        },

        hide: function (target, callBack) {
            if (target.is(':visible')) {
                $disablingDiv.hide();
                target.slideUp(transition.duration, callBack);
            }
            else if (typeof callBack !== "undefined") {
                callBack();
            }
        }
    }

    /////////////////////////////////////////////////////////////////
    // Let's kick it all off

    var productListVM = new productListVM();
    ko.applyBindings(productListVM, $listPanel[0]);

    var productCategoryList = new productCategoryList();

    productCategoryList.get(function () {
        ko.applyBindings(new productAddVM(productCategoryList.productCategories()), $addPanel[0]);
        ko.applyBindings(new productEditVM(productCategoryList.productCategories()), $editPanel[0]);
        productListVM.get();
    });
});

</script>

Server side paging with KoGrid

Source code for article – https://github.com/RossWhitehead/ServerSidePagingKoGrid

In this post I’m going to demonstrate how to implement server side paging with KoGrid. I’m going to be doing this within the context of a ASP.NET MVC project, and will utilize both MVC and WebAPI controllers.

When the number of items is relatively small it is advisable to retrieve all items from the server when initializing a page, and then use client-side binding with KoGrid – or a similar grid framework. This offers the best experience for the user as they are able page through the data without the need for the browser to contact the server. However, when the number of items gets particularly large then we may encounter performance issues, both client-side and server-side. In this case it is worth considering the implementation of server-side paging whereby the client requests data from the server one page at a time.

This is what I’m going to be building –

BasicKoGrid

KoGrid is built on top of Knockout, a javascript library that takes a lot of the leg work out of separating UI mark-up (View) and presentation data/logic (ViewModel) when creating dynamic web pages.

As I will be presenting KoGrid and Knockout in the context of an MVC application, I will essentially be implementing the MCVVM (Model-Controller-View-ViewModel) pattern. There’s a good debate here on MVC and MVVM, and a number of acronyms are proposed for MVC combined with MVVM. My preference is MCVVM as in terms of application flow we have Controllers which act as intermediaries between the domain Model and the UI, responding to requests by generating markup (View) and data/logic (ViewModel). The terminology isn’t important. I’m only mentioning it as it defines the order in which I will be presenting the solution  –

  1. Model
  2. Controller
  3. View
  4. ViewModel

1. Model

ASP.NET Web Application Project

Using VS 2013, the first step is to create a ASP.NET Web Application project and select either the MVC or WebApi template, ensuring that both MVC and WebApi core references are included. This will allow requests to be routed to both MVC and WebApi controllers. I will be using an MVC controller to serve up the web page, as I can then benefit from the Razor view engine, and the layout page to generate the HTML for the header, footer, and navigation elements. I will then use Ajax to get data from a WebApi controller. I could have used the MVC controller to service the Ajax requests, but I wanted to demonstrate MVC and WebApi controllers working in the same project.

Domain Model

The next step is to create the domain model. I’m using Code-First Entity Framework 6.0 to do this.

I have 2 entities – ProductCategory and Product, with a one-to-many relationship between them. And I have a DbContext called WebApiDbContext, which is used to generate the database.

ProductCategory.cs

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace WebAPIKo.Models
{
    public class ProductCategory
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int ProductCategoryId { get; set; }

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        // Navigational Properties
        public virtual ICollection<Product> Products { get; set; }
    }
}

Product.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace WebAPIKo.Models
{
    public class Product
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int ProductId { get; set; }

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        [Required]
        [StringLength(1000)]
        public string Description { get; set; }

        [Required]
        public int ProductCategoryId { get; set; }

        // Navigation properties
        public virtual ProductCategory ProductCategory { get; set; }
    }
}

WebApiDbContext.cs

using System.Data.Entity;

namespace WebAPIKo.Models
{
    public class WebAPIKoDbContext : DbContext
    {
        public virtual DbSet<Product> Products { get; set; }
        public virtual DbSet<ProductCategory> ProductCategories { get; set; }
    }
}

Data Transfer Objects (DTOs)

The WebApi controller, that I will detail in the next section, will service Ajax Get requests for product lists. It will interrogate the domain model to get the data that it requires.

The data requirements of the UI generally differ from the domain. To avoid a mismatch and to create a clear separation of concerns, it is good practice to create Data Transfer Objects (DTOs) to structure the data passed to the UI. The controller will transpose the data from the domain model into DTOs, which will then be serialized to Json and passed to the UI.

ProductListDTO.cs

The ProductListDTO class will be instantiated and serialized into Json by the WebApi controller in response to a Get request.

It contains a list of ProductDTO objects, representing a page of products. And an integer, ProductCount, which will hold the count of all products that match the supplied filter. If there are 30 products matching the supplied filter then ProductCount will be set equal to 30, but if the page size is set to 5 then there will only be 5 ProductDTO objects in the list.

using System.Collections.Generic;

namespace WebAPIKo.Models
{
    public class ProductListDTO
    {
        public int ProductCount { get; set; }
        public List<ProductDTO> PageOfProducts { get; set; }
    }
}

ProductDTO.cs

In the domain model, Product has a ProductCategoryId field and a navigation property to ProductCategory. In our simple example, ProductCategoryId is not required by the UI. So we flatten the Product-ProductCategory relationship and populate ProductDTO with ProductCategoryName.

namespace WebAPIKo.Models
{
    public class ProductDTO
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string ProductCategoryName { get; set; }
    }
}

2. Controller

There are 2 controllers.

HomeController.cs

The first is an MVC controller which services the initial request by responding with the BasicKoGrid.cshtml page that contains the View/ViewModel. In this case we are not parsing any view data to the view, although this is possible.

using System.Web.Mvc;

namespace WebAPIKo.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult BasicKoGrid()
        {
            return View();
        }
    }
}

ProductController.cs

The second controller is a WebApi controller which contains a GetProducts action. When the grid is initialized, and when the user interacts with it – by navigating to a different page, or changing the filter and then clicking on refresh – the ViewModel will send an Ajax Get request in order to get a new page of products. The following information will be included in the Get request and will be bound to the appropriate GetProducts argument –

  • page
    • requested page number
  • pageSize
    • page size
  • filter
    • in one of 2 formats –
      • [ColumnName:filter] e.g. “name:trek” – filters on a specific column
      • [filter] e.g. “trek” – filters on all relevant columns (both name OR description)
  • sort
    • [ColumnName asc||desc] e.g. “description desc”

The GetProducts action constructs a Linq query, IQueryable<Product>. It checks to see if a filter has been supplied, and if so applies a Where clause to the query. I am using System.Linq.Dynamic extension methods to simplify the building of the query. You will need to install this library from NuGet.

The action then executes the Count() method on the query to determine the total number of products that match the filter. It then applies a SortBy clause, defaulting the sort to “name asc”, and skips to and takes the required page.

It finally populates an instance of ProductListDTO with the product count and with the page of products transposed to ProductDTO objects. The instance of ProductListDTO is serialized and returned with an OK (200) HTTP status code.

using System.Linq;
using System.Linq.Dynamic;
using System.Web.Http;
using WebAPIKo.Models;

namespace WebAPIKo.Controllers
{
    public class ProductsController : ApiController
    {
        private WebAPIKoDbContext db = new WebAPIKoDbContext();

        // GET: api/Product
        public IHttpActionResult GetProducts([FromUri]int page, [FromUri]int pageSize, [FromUri]string filter, [FromUri]string sort)
        {
            ProductListDTO dto = new ProductListDTO();

            IQueryable<Product> query = db.Products;

            if (filter != null)
            {
                if (filter.Contains(':'))
                {
                    string[] filterArray = filter.Split(':');
                    switch (filterArray[0].ToLower())
                    {
                        case "name":
                            query = query.Where(p => p.Name.Contains(filterArray[1].Trim()));
                            break;
                        case "description":
                            query = query.Where(p => p.Description.Contains(filterArray[1].Trim()));
                            break;
                    }
                }
                else
                {
                    query = query.Where(p => p.Name.Contains(filter.Trim()) || p.Description.Contains(filter.Trim()));
                }
            }

            dto.ProductCount = query.Count();

            query = query.OrderBy(sort == null ? "name asc" : sort).Skip((page - 1) * pageSize).Take(pageSize);

            dto.PageOfProducts = query.Select(p => new ProductDTO()
                {
                    ProductId = p.ProductId,
                    Name = p.Name,
                    Description = p.Description,
                    ProductCategoryName = p.ProductCategory.Name
                }
                ).ToList(); 

            return Ok(dto);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

3. View

There are 2 aspects to the view. The CSS styles and the HTML markup.

CSS styles

.gridStyle is applied to the <div> element that acts as the container for the grid. As it’s name suggests it styles the grid.

There seems to be styling issues with the KoGrid filter. This is what I get straight out-of-the-box in both Chrome and IE –

filterbefore

I have fixed these issues with the .kg* styles  –

filterafter

I then have a set of styles for the list panel and refresh button.

And finally a style to position a loading indicator in the center of it’s parent container.

<style type="text/css">
    .gridStyle {
        border: 1px solid rgb(212,212,212);
        width: 100%;
        height: 300px;
        margin: auto;
    }

    .kgColMenu {
        width: 200px;
    }

        .kgColMenu input[type=text] {
            width: 100%;
        }

    .kgColList {
        padding-left: 25px;
    }

    .kgColListItem label {
        width: 100%;
    }

    .list-panel {
        margin-top: 20px;
    }

        .list-panel .panel-heading {
            overflow: auto;
            padding: 5px 20px;
        }

        .list-panel .panel-title {
            float: left;
            margin-top: 10px;
        }

        .list-panel .panel-body {
            padding: 20px 20px 10px 20px;
        }

        .list-panel .refresh-button {
            float: right;
        }

    .loading-indicator {
        position: absolute;
        left: 50%;
        top: 50%;
        margin-left: -26px;
        margin-top: -26px;
        display: none;
        z-index: 9999;
    }
</style>

 HTML

All that’s required to render the grid is a <div> element containing a Knockout declarative binding – data-bind=”koGrid: gridOptions” – attribute. gridOptions contains the configuration options for the grid and is a property of the ViewModel that we will be binding to the View. I will discuss this further in the ViewModel section.

<div class="gridStyle" data-bind="koGrid: gridOptions"></div>

I have placed this <div> in a Bootstrap panel, along with a refresh button and a loading indicator.

The refresh button’s click event is bound to the ViewModel’s “get” method – “data-bind= click: get”. I will discuss the “get” method in the ViewModel section.

The loading indicator will be positioned to the center of the panel using the styles mentioned above. Note. A variety of loading indicators can be downloaded from this site – http://www.ajaxload.info/

<div class="row">
<div class="col-md-12">
<div class="panel panel-primary list-panel" id="list-panel">
<div class="panel-heading list-panel-heading">
<h3 class="panel-title list-panel-title">Products</h3>
<button type="button" class="btn btn-default btn-md refresh-button" data-bind="click: get">
                    <span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Refresh
                </button></div>
<div class="panel-body">
<div class="gridStyle" data-bind="koGrid: gridOptions"></div>
</div>
<img src="~/Content/Images/ajax-loader.gif" id="loading-indicator" class="loading-indicator" /></div>
</div>
</div>

 4. ViewModel

We need to add references to the KoGrid and Knockout libraries, and also to JQuery if not already included in the app. As I am building a ASP.NET Web application, I have added references to the libraries in BundleConfig.cs –

bundles.Add(new ScriptBundle("~/bundles/knockout").Include(
            "~/Scripts/knockout-3.0.0.js",
            "~/Scripts/knockout.validation.js",
            "~/Scripts/koGrid-2.1.1.js"));

The ViewModel comprises of the following –

Product

A definition for a Product type with a constructor with productId, name, description, and productCategoryName parameters. Product contains properties defined as ko.observable objects, which are Knockout specific JavaScript objects that can notify subscribers about changes, and can automatically detect dependencies. The Product object parameters are passed to the ko.observable object constructors.

ProductListVM

We will be instantiating an instance of this type and binding it to the KoGrid.

The ProductListVM type contains the following properties –

  • self.products – an array which contains the data to be bound to the grid.
  • self.columnDefs – an array of objects defining the columns.
  • self.filterOptions – I have the useExternalFilter option set to false, so when a user enters a filter it will be applied to the current page of data in the grid. But I will also pass this filter with the Ajax get request when the user chooses to refresh the grid or navigate to a new page of data.
  • self.pagingOptions – with the following options –
    • currentPage – current page
    • pageSizes – an array of page sizes that the user can choose from
    • pageSize – default page size. Must be equal to one of the sizes specified in the pageSizes array.
    • totalServerItems – used by KoGrid to determine the number of pages to show in the pagination control. We will assign this value each time we get a new page of data from the server.
  • self.gridOptions – contains properties for configuring the grid. A full set of configuration options can be found here. All of the aforementioned ViewModel properties are assigned to configuration options. e.g data: self.products.

The ProductListVM type contains a single method – self.get(). The first thing the get() method does is show the loading indicator. It then performs an Ajax Get call to /api/products, passing in the required page, pageSize, sort and filter. It gets all these values from observable properties, hence reflecting the users interaction with the view.

On success the Ajax Get call executes a callback function that sets the self.pagingOptions.totalServerItems property to data.ProductCount, and for each ProductDTO in data.PageOfProducts pushes a new instance of the Product type to the self.products array. As the self.pagingOptions.totalServerItems and self.products properties are observable and bound to the grid, the grid is updated.

The final section of ProductListVM defines 3 event subscriptions.

The 1st is called when the user selects a new page size. It sets the current page to 1 and then calls the self.Get() method in order to refresh the data.

The 2nd is called when a user selects a new page from the pager control. If calls the self.Get() method to get the new page.

And the 3rd is called when the user clicks on a column heading in order to sort the grid. It sets the current page to 1 and then calls the self.Get() method in order to refresh the data.

Initialization code

After the DOM has loaded I create and instance of ProductListVM called vM. I then bind vM to the list panel, and then call the Get() method in order to populate the grid with the default page of data from the server.

@section scripts{

    <script type="text/javascript">
       $(function () {

            var $loadingIndicator = $('#loading-indicator');

            var productsUri = '/api/products';

            ///////////////////////////////////////////////////////////////////
            // Product

            var Product = function (productId, name, description, productCategoryName) {
                var self = this;

                // Properties

                self.productId = ko.observable(productId);
                self.name = ko.observable(name);
                self.description = ko.observable(description);
                self.productCategoryName = ko.observable(productCategoryName);
            }

            ///////////////////////////////////////////////////////////////////
            // Product List

            var ProductListVM = function () {
                var self = this;

                // Properties
                self.products = ko.observableArray([]);

                self.columnDefs = [
                   { field: 'productId', displayName: 'Id', width: 100 },
                   { field: 'name', displayName: 'Name', width: 200 },
                   { field: 'description', displayName: 'Description' },
                   { field: 'productCategoryName', displayName: 'Product Category', width: 200 }
                ];

                self.filterOptions = {
                    filterText: ko.observable(""),
                    useExternalFilter: false
                };

                self.pagingOptions = {
                    currentPage: ko.observable(1),
                    pageSizes: ko.observableArray([2, 5, 10, 20, 50]),
                    pageSize: ko.observable(5),
                    totalServerItems: ko.observable(0)
                };

                self.sortInfo = ko.observable({ column: { 'field': 'name'}, direction: 'asc' });

                self.gridOptions = {
                    data: self.products,
                    columnDefs: self.columnDefs,
                    autogenerateColumns: false,
                    showGroupPanel: true,
                    canSelectRows: false,
                    showFilter: true,
                    filterOptions: self.filterOptions,
                    enablePaging: true,
                    pagingOptions: self.pagingOptions,
                    sortInfo: self.sortInfo,
                    rowHeight: 35
                };

                // Methods

                self.get = function () {
                    $loadingIndicator.show();

                    $.ajax({
                        url: productsUri,
                        type: 'get',
                        data: {
                            'page': self.pagingOptions.currentPage(),
                            'pageSize': self.pagingOptions.pageSize(),
                            'filter': self.filterOptions.filterText == undefined ? '' : self.filterOptions.filterText(),
                            'sort': self.sortInfo().column.field + ' ' + self.sortInfo().direction
                        },
                        contentType: 'application/json; charset=utf-8',
                        success: function (data) {
                            self.pagingOptions.totalServerItems(data.ProductCount);

                            var productsArray = [];
                            $.each(data.PageOfProducts, function (key, value) {
                                productsArray.push(
                                    new Product(value.ProductId, value.Name, value.Description, value.ProductCategoryId, value.ProductCategoryName));
                            });
                            self.products(productsArray);

                            $loadingIndicator.hide();
                        }
                    });
                };

                // Subscriptions

                self.pagingOptions.pageSize.subscribe(function (data) {
                    self.pagingOptions.currentPage(1);
                    self.get();
                });

                self.pagingOptions.currentPage.subscribe(function (data) {
                    self.get();
                });

                self.sortInfo.subscribe(function (data) {
                    self.pagingOptions.currentPage(1);
                    self.get();
                });
            }

            /////////////////////////////////////////////////////////////////
            // Let's kick it all off

            var vM = new ProductListVM();
            ko.applyBindings(vM, $("#list-panel")[0]);
            vM.get();
        });

    </script>
}

Caveat Emptor

KoGrid works well with server-side binding, and is relatively simple to implement. However, the default filter functionality does not fit particularly well with server-side binding.

The filter input control updates the view model on the afterkeydown event. When a user types a filter string then the filter is applied to the bound ko.observableArray and the view with each key-stroke. This works very well with client-side binding as the ko.observableArray is populated with the all items. For example, if there are 5 pages of items, then the filter is applied to all pages. The users sees the results immediately.

But with server-side binding where the ko.observableArray is populated with a single page of items, then the filter is only applied to the visible page. This is the primary reason I added the Refresh button to my implementation of the grid. The user can type a filter string and then click on Refresh in order to update the product count and first page of products that match the filter.

If your users do not like the way the filter works then it is possible to modify the HTML template used by KoGrid to implement the filter. Possibly to add a filter button along with the filter and have the view model and view updated when the user clicks on the button rather than whilst they are typing a filter string. However, I have avoided doing this so far as KoGrid defines the filter HTML as part of the default grid template (window.kg.defaultGridTemplate). The only way to change the filter HTML is to overwrite this template. At which point we are essentially modifying the KoGrid library and so may hurt backwards compatibility.

What’s Next?

In my next post I’m going to expand on this solution by adding Add, Edit, and Delete functionality to the grid.

 

 

 

Zen Coding with Visual Studio

Zen Coding is a plugin that can significantly speed up the generation of HTML.

It enables the coder to enter HTML using CSS-style abbreviations, which are then expanded into full HTML.

Plugins are available for a variety of editors. For Visual Studio, Zen Coding functionality is incorporated into the Web Essential extension, which can be downloaded from here – http://vswebessentials.com/download

I’m going to demonstrate how to code HTML using Zen Coding abbreviations. This is the HTML snippet that I’m going to create –

<div id="wrap" class="pnl pnl-lg">
    <h1>Zen Test</h1>
    <div class="content">
        <p>Here's some paragraph text</p>
        <div class="box"></div>
    </div>
    <aside class="sidebar">
        <ul>
            <li id="listItem_1"><a id="link1" href="" target="_blank">Link 1</a></li>
            <li id="listItem_2"><a id="link2" href="" target="_blank">Link 2</a></li>
            <li id="listItem_3"><a id="link3" href="" target="_blank">Link 3</a></li>
        </ul>
    </aside>
</div>

I will do this step-by-step, introducing a feature with each step.

 HTML Elements

Open an HTML or CSHTML page.

Type div and then hit the [TAB] key. The editor expands the abbreviation into full HTML.

div [tab]

<div></div>

Class and ID Atrributes

A class attribute can be applied to an element by appending .classname to the element.

An id can be applied by appending #id.

Multiple classes can be applied by chaining the class names.

div.pnl [tab]

<div class="pnl"></div>

div#wrap [tab]

<div id="wrap"></div>

div.pnl.pnl-lg#wrap [tab]

<div id="wrap" class="pnl pnl-lg"></div>

Nesting Elements

A right-angled bracket, >, is used to denote nested or child elements. 

div.pnl.pnl-lg#wrap>h1 [tab]

<div id="wrap" class="pnl pnl-lg">
    <h1></h1>
</div>

Sibling Elements

To add multiple sibling elements, use +.

div.pnl.pnl-lg#wrap>h1+div.content+aside.sidebar [tab]

<div id="wrap" class="pnl pnl-lg">
    <h1></h1>
    <div class="content"></div>
    <aside class="sidebar"></aside>
</div>

Element Multiplication

Appending *n to an element multiplies the element n times.

In the example below li*3 outputs 3 <li> elements.

div.pnl.pnl-lg#wrap>h1+div.content>p+div.box+aside.sidebar>ul>li*3 [tab]

<div id="wrap" class="pnl pnl-lg">
    <h1></h1>
    <div class="content">
        <p></p>
        <div class="box"></div>
        <aside class="sidebar">
            <ul>
                <li></li>
                <li></li>
                <li></li>
            </ul>
        </aside>
    </div>
</div>

Grouping

Elements can be grouped together using parenthesis, ( ).

In the example above the parser has output the <aside> elements within the <div class=”content”> element.

The example below uses parenthesis to group elements, hence outputting the <aside> as a sibling of the <div>.

div.pnl.pnl-lg#wrap>h1+(div.content>p+div.box)+(aside.sidebar>ul>li*3) [tab]

<div id="wrap" class="pnl pnl-lg">
    <h1></h1>
    <div class="content">
        <p></p>
        <div class="box"></div>
    </div>
    <aside class="sidebar">
        <ul>
            <li></li>
            <li></li>
            <li></li>
        </ul>
    </aside>
</div>

Custom Attributes

Custom attributes can be added to an element by appending the element with the attribute name and value in square brackets, [ ].

[target=_blank] is appended to the <a> attribute below.

div.pnl.pnl-lg#wrap>h1+(div.content>p+div.box)+(aside.sidebar>ul>li*3>a#link[target=_blank]) [tab]

<div id="wrap" class="pnl pnl-lg">
    <h1></h1>
    <div class="content">
        <p></p>
        <div class="box"></div>
    </div>
    <aside class="sidebar">
        <ul>
            <li><a id="link" href="" target="_blank"></a></li>
            <li><a id="link" href="" target="_blank"></a></li>
            <li><a id="link" href="" target="_blank"></a></li>
        </ul>
    </aside>
</div>

Item Numbering

Item numbering for multiple elements can be added by applying the $ character.

Item numbering can be added to attributes as well as text.

And the numbering can be padded with zeros by applying multiple $ characters, e.g. $$

div.pnl.pnl-lg#wrap>h1+(div.content>p+div.box)+(aside.sidebar>ul>li*3>a#link$[target=_blank]) [tab]

<div id="wrap" class="pnl pnl-lg">
    <h1></h1>
    <div class="content">
        <p></p>
        <div class="box"></div>
    </div>
    <aside class="sidebar">
        <ul>
            <li><a id="link1" href="" target="_blank"></a></li>
            <li><a id="link2" href="" target="_blank"></a></li>
            <li><a id="link3" href="" target="_blank"></a></li>
        </ul>
    </aside>
</div>

 Text

Curly brackets, { }, are used to denote text.

div.wrap>h1{Zen Test}+(div.content>p{Here's some paragraph text}+div.box)+(aside.sidebar>ul>li#listItem_$*3>a#link$[target=_blank]{Link $}) [tab]

<div class="wrap">
    <h1>Zen Test</h1>
    <div class="content">
        <p>Here's some paragraph text</p>
        <div class="box"></div>
    </div>
    <aside class="sidebar">
        <ul>
            <li id="listItem_1"><a id="link1" href="" target="_blank">Link 1</a></li>
            <li id="listItem_2"><a id="link2" href="" target="_blank">Link 2</a></li>
            <li id="listItem_3"><a id="link3" href="" target="_blank">Link 3</a></li>
        </ul>
    </aside>
</div>

For more details on the Zen Coding standard, a good starting place is the Zen Coding project repository – https://code.google.com/p/zen-coding/

 

 

 

 

Surfacing SSAS Data With Google Charts

In this post I’m going to demonstrate how to surface SSAS data in an MVC view using ADOMD.NET and Google Charts.

This is what I’m going to create –

charts

1. ASP.NET MVC Project

First step is to create a new ASP.NET MVC Web Application project.

Then add a reference to the ADOMD.NET client components library (microsoft.analysisservices.adomdclient.dll).

Unfortunately, the library is not available through NuGet – don’t ask me why. Instead we need to download and install the SQL Server feature pack from the following link –

https://www.microsoft.com/en-gb/download/details.aspx?id=42295

And then reference the library in the following folder –

C:\Program Files\Microsoft.NET\ADOMD.NET\120\Microsoft.AnalysisServices.AdomdClient.dll

2. ReportData data-access class

Next step is to create a Data folder and then add the following data-access class  –

using Microsoft.AnalysisServices.AdomdClient;
using System.Collections.Generic;
using System.Data;
using System.Linq;

namespace Charts.Data
{
    public class ReportData
    {
        public static List<object> SalesByYear()
        {
            string command = @" SELECT 
                                    NON EMPTY [Date].[Calendar].[Calendar Year] ON ROWS, 
                                    {[Measures].[Reseller Sales Amount], [Measures].[Internet Sales Amount]} ON COLUMNS
                                FROM 
                                    [Adventure Works]";

            return GetData(command);
        }

        public static List<object> InternetSalesByCategory()
        {
            string command = @" SELECT 
                                    NON EMPTY [Product].[Product Categories].[Category] ON ROWS, 
                                    {[Measures].[Internet Sales Amount]} ON COLUMNS
                                FROM 
                                    [Adventure Works]
                                WHERE
                                    [Date].[Calendar].[Calendar Year].&[2013]";

            return GetData(command);
        }

        public static List<object> ResellerSalesByCategory()
        {
            string command = @" SELECT 
                                    NON EMPTY [Product].[Product Categories].[Category] ON ROWS, 
                                    {[Measures].[Reseller Sales Amount]} ON COLUMNS
                                FROM 
                                    [Adventure Works]
                                WHERE
                                    [Date].[Calendar].[Calendar Year].&[2013]";

            return GetData(command);
        }

        private static List<object> GetData(string command)
        {
            DataSet ds = new DataSet();

            using (AdomdConnection conn = new AdomdConnection("Data Source=localhost"))
            {
                conn.Open();
                using (AdomdCommand cmd = new AdomdCommand(command, conn))
                {
                    AdomdDataAdapter adapter = new AdomdDataAdapter(cmd);
                    adapter.Fill(ds);
                }
                conn.Close();
            }

            return ConvertDataTableToObjectList(ds.Tables[0]);
        }

        private static List<object> ConvertDataTableToObjectList(DataTable dt)
        {
            List<object> data = new List<object>();
            int columnCount = dt.Columns.Count;

            string[] columnObject = new string[columnCount];
            for (int i = 0; i < columnCount; i++)
            {
                string name = dt.Columns[i].ColumnName.Replace(".[MEMBER_CAPTION]", "");
                string[] nameParts = name.Split(new char[] { '.' });

                columnObject[i] = nameParts[nameParts.Count() - 1].Replace("[", "").Replace("]", "");
            }
            data.Add(columnObject);

            foreach (DataRow row in dt.Rows)
            {
                data.Add(row.ItemArray);
            }

            return data;
        }
    }
}

There are 3 data-access methods, 1 per chart –

  1. SalesByYear
  2. InternetSalesByCategory
  3. ResellerSalesByCategory

Each of these methods defines an MDX statement, and then passes the statement to the GetData method.

The GetData method opens a connection to the SSAS database, and then executes the MDX statement, utilizing the AdomdDataAdapter.Fill method to populate a DataSet with the results.

The DataSet contains a collection of DataTable objects. The code assumes that the first in the collection is populated with the results from the query. I’m not sure under what circumstances there would be more than one DataTable?

The ConvertDataTableToObjectList method converts the results into a format that Google charts will understand, populating a list of objects with column names and values from the DataTable.

The column names contain dimension, hierarchy, level information. And additionally are surrounded by curly brackets, and may be suffixed with “‘[MEMBER_CAPTION]”. For example, here are the column names from the 1st query –

  • {[Date].[Calendar].[Calendar Year].[MEMBER_CAPTION]}
  • {[Measures].[Reseller Sales Amount]}
  • {[Measures].[Internet Sales Amount]}

The ConvertDataTableToObjectList strips off the superfluous information, leaving the level names –

  • Calendar Year
  • Reseller Sales Amount
  • Internet Sales Amount

3. Home Controller

using Charts.Data;
using System.Collections.Generic;
using System.Web.Mvc;

namespace Charts.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public JsonResult SalesByYear()
        {
            List<object> data = ReportData.SalesByYear();
            return Json(data, JsonRequestBehavior.AllowGet);
        }

        public JsonResult InternetSalesByCategory()
        {
            List<object> data = ReportData.InternetSalesByCategory();
            return Json(data, JsonRequestBehavior.AllowGet);
        }

        public JsonResult ResellerSalesByCategory()
        {
            List<object> data = ReportData.ResellerSalesByCategory();
            return Json(data, JsonRequestBehavior.AllowGet);
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your application description page.";

            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";

            return View();
        }
    }
}

The following action methods have been added to the Home controller –

  • SalesByYear
  • InternetSalesByCategory
  • ResellerSalesByCategory

Each of the methods gets a List<object> containing the chart data from the appropriate data access method, and then returns this data serialized as JSON.

4.Home Index View

@{
    ViewBag.Title = "Home Page";
}

<h1>Google Charts Demo</h1>

<div class="row">
    <div class="col-md-12">
        <div class="chart"
             data-url="/Home/SalesByYear"
             data-chart-type="Line"
             data-options='{ "title": "Sales by Year", "vAxis": { "format": "$#,###" } }'>
        </div>
    </div>
</div>
<div class="row">
    <div class="col-md-6">
        <div class="chart"
             data-url="/Home/InternetSalesByCategory"
             data-chart-type="Pie"
             data-options='{ "title": "2013 Internet Sales by Category", "is3D": "true" }'>
        </div>
    </div>
    <div class="col-md-6">
        <div class="chart"
             data-url="/Home/ResellerSalesByCategory"
             data-chart-type="Pie"
             data-options='{ "title": "2013 Reseller Sales by Category", "is3D": "true" }'>
        </div>
    </div>
</div>

@section scripts
{
    <script type="text/javascript"
            src="https://www.google.com/jsapi?autoload={
            'modules':[{
              'name':'visualization',
              'version':'1',
              'packages':['corechart']
            }]
          }"></script>

    <script type="text/javascript">
        $().ready(function () {
            google.setOnLoadCallback(drawCharts);
        });

        function drawCharts() {
            var chart = $('div.chart');
            $.each(chart, drawChart);
        }

        function drawChart() {
            var $this = $(this);
            var url = $this.data('url');
            var chartType = $this.data('chart-type');
            var options = $this.data('options');
            var chartArea = $this[0];

            $.get(url)
             .done(function (jsonArray) {
                 var chart;
                 var data = google.visualization.arrayToDataTable(jsonArray);
                 var defaultOptions = {
                     title: 'Chart',
                     chartArea: { left: '100' },
                     height: '400'
                 };
                 $.extend(true, defaultOptions, options);

                 switch (chartType) {
                     case "Pie":
                         chart = new google.visualization.PieChart(chartArea);
                         break;
                     default:
                         chart = new google.visualization.LineChart(chartArea);
                 }

                 chart.draw(data, defaultOptions);
             });
        }
    </script>
}

After the DOM has loaded, the google.setOnLoadCallback(drawCharts) function is called, ensuring that the Google API is fully loaded prior to calling the drawCharts function.

The drawCharts function gets all <div> elements with the “chart” class, and for each calls the drawChart function.

The drawChart function interrogates the target <div> elements data- attributes to get the following information –

  • data-url
    • the controller action to call to get the data for the chart
  • data-chart-type
    • currently either “Pie” or “Line”, but can easily be extended to include other Google chart types.
  • data-options
    • A Json string representing an options object. Used to specify chart specific options.

The drawChart function then makes an Ajax get request to the url, specifying a callback function which receives the JSON result.

The callback function calls the google.visualization.arrayToDataTable function to convert the JSON result to a google.visualization.DataTable object.

It then uses the chart specific options to extend a default set of options, and then initializes either a Pie or Line chart before drawing the chart based on the data and the options.

And bobs your uncle.