Using the Ignite UI Grid and Breeze.JS in a Hot Towel SPA – a complete CRUD app

Alexander Todorov / Tuesday, August 06, 2013

   Couple of days ago, I’ve blogged about how easy and cool it is to integrate the Ignite UI grid with Durandal. In this blog post, I am going to explore the Hot Towel SPA template by John Papa, Breeze.JS, and develop a full-blown CRUD application using the Ignite UI grid in the view. 

The Hot Towel SPA is an ASP.NET MVC project template which provides several things:

  •  Great project structure that gives you a starting point for building SPA apps
  • Includes Durandal, Breeze, Knockout, Require.js, Toastr, and Bootstrap
  •  NuGet package for the template, which you can install in an existing ASP.NET MVC project
  •  Manages the rendering of initial scripts through a BundleConfig class (very convenient feature indeed!)

and more. 

   So you can just create a new blank ASP.NET MVC project, add the Hot Towel using NuGet, and you’ve got everything you need to build the most robust single-page apps, including a server-side API (WebAPI) that you can use to persist, load and manage your data through EntityFramework, for example.

   In one of my previous blog posts I focused on Durandal, and this time I would like to write more about Breeze.JS, and how you can combine it with the Ignite UI Grid in order to create a full blown CRUD app with very little amount of code, which loads data through the Web API and supports batch CRUD.

   Before I continue, I would like to draw your attention to a blog post, which discusses how the Ignite Grid itself has out of the box, built-in full support for REST CRUD operations. This means that you can hook it to *any* REST API which implements the RFCs for GET/POST/PUT/DELETE HTTP requests, regardless of the server technology. So you are not limited to ASP.NET MVC only and you don’t need to change your REST API, if it implements the RFCs correctly, and you don’t have to write any client-side code in order to support updating, deleting, and adding of records. Pretty sweet, isn’t it? But chances are that, as a web developer using modern technologies, you are already using one or more of Knockout, Durandal, Breeze.JS, and you love that. Moreover, you may already have code based on those frameworks that you don’t want to change. That’s why we don’t want to restrict you to any development approach, and as you will see, the Ignite UI grid can easily integrate with Breeze in order to achieve the same kind of CRUD – and what’s more, on top of that you get Breeze’s outstanding API for managing entities.

   Let’s start by creating a blank ASP.NET MVC 4 Project, and augmenting it with the Hot Towel SPA template. In order to do that, you just need to add it via NuGet:

PM> Install-Package HotTowel

   The next thing we’d like to do is use the MVC Controller & Model code from the Breeze.js TODO app sample, which you can check out here and download it from here. (It’s part of the extended Breeze download). So, from the TODO sample solution, we take the Controllers/TodosController.cs, and the files in the Models folder and add them to our project. We basically need to add them as “Existing Items”, like this:

   The TodosController that we’ve added is an ApiController, which is also acting as a BreezeController, by marking it with the [BreezeController] attribute. The idea of that is to ensure that the breeze.JS client-side code, and the Web API server-side code are in “understanding” of the format of the data that is being exchanged. This is all explained in detail in the following article.

The methods in the TodosController, like Todos(), delegate the work to the TodosContext, which is basically an EntityFramework DbContext implementation. The TodoDatabaseInitializer class provides some mock-up data which we can use in order to work with CRUD operations, without having to worry about setting up a real database (but to the developer, it’s pretty much the same thing since it is abstracted using EF).

One important thing to look at is the Metadata method in the TodosController, which provides some metadata information to the client-side javascript breeze code, so that the entity manager can operate correctly. The metadata contents includes information about the server-side types of properties, their entity types (such as TodoItem), whether properties are nullable, and so on.

Now the next step is to hook our Ignite Grid to show the Todo items.

First, we need to setup the extra script and CSS references that we will need. In order to do that, we just need to modify the App_Start/BundleConfig.cs:

.Include("~/scripts/knockout.mapping-latest.debug.js")
.Include("~/Scripts/jquery-ui.js")
.Include("~/Scripts/infragistics.core.js")
.Include("~/Scripts/infragistics.lob.js")
.Include("~/Scripts/extensions/infragistics.datasource.knockoutjs.js")
.Include("~/Scripts/extensions/infragistics.ui.grid.knockout-extensions.js")

And the CSS:

.Include("~/Content/infragistics/css/themes/metro/infragistics.theme.css")
.Include("~/Content/infragistics/css/structure/infragistics.css")

   All of those are present in the attached sample. Now we can also add the jQuery UI base styles in the Content folder, or reference it directly in the index.cshtml:

@Styles.Render("~/Content/css")
<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />

After we do that, we can start adding our content – we need to have Durandal view and viewmodel.

So we create an iggridbreezejs.html in ~/App/views/, with the following content:

<section>
    <div class="row">
		<div class="span2"></div>
		<div class="span1" style="padding:10px;">
			<input type="button" data-bind="click: buttonClick, value: buttonText"/>
		</div>
		<div class="span4" style="padding:10px;">
			<span data-bind="text: labelText"/>
		</div>
	</div>
    <div class="row">
		<div class="span2"></div>
		<div class="span1" style="padding:10px;">
			<input type="button" data-bind="click: commitClick, value: commitButtonText"/>
		</div>
		<div class="span4" style="padding:10px;">
			<span data-bind="text: commitText"/>
		</div>
	</div>
	<table id="grid1" data-bind="igGrid: {dataSource: data, caption: 'TODOs', primaryKey: 'Id', features: [{name: 'Updating'}]}">
	</table>
</section>

Again, you can also take a look at my blog about Ignite UI and Durandal, and you’ll see very similar view code. The idea of the inputs above is to allow two things:

  • To demonstrate how updating the knockout model automatically updates the grid, re-rendering it fully
  • To demonstrate batch updating – after doing adds/deletes/edits in rows, and hitting the Save Changes button, you get everything committed to the backend using Breeze’s API.

Finally, we need to add the viewmodel code in a iggridbreezejs.js file, and place it in ~/App/viewmodels:

define(['durandal/http', 'durandal/app'], function (http, app) {
    
    return {
        data: ko.observableArray([]),
        buttonText: "Update!",
        manager: null,
        labelText: "Hit the button to update the model 'desc' value of the first record with the value of 'some custom description & watch out the grid!",
        buttonClick: function () {
            // update data (the model for the IGnite Grid)
            this.data()[0].Description("some custom description");
        },
        commitClick: function () {
            var transactions = $("#grid1").igGrid("pendingTransactions"), self = this;
            transactions.forEach(function (t) {
                if (t.type == "newrow") {
                    // create an entity using Breeze's API
                    self.manager.createEntity('TodoItem', t.row);
                } else if (t.type == "deleterow") {
                    // find the entity
                    var entity = self.manager.getEntityByKey("TodoItem", t.rowId);
                    entity.entityAspect.setDeleted();
                } else {
                    // update of an existing entity
                    // 1. find the entity
                    var entity = self.manager.getEntityByKey("TodoItem", t.rowId);
                    // 2. fill new values
                    //entity.Id(t.row.Id);
                    entity.Description(t.row.Description);
                    entity.CreatedAt(t.row.CreatedAt);
                    entity.IsDone(t.row.IsDone);
                    // 3. update is done automatically (internally) by Breeze / ko, as soon as you modify a prop
                    // so we don't need to set any state manually
                }
            });
            this.manager.saveChanges().then(function (saveResult) {
                console.log("# of Todos saved = " + saveResult.entities.length);
                // now we need to clear the grid's transactions and add the items to the data array
                // via knockout, which will also update the grid itself
                // we do this so that we hijack the grid's updating, by telling it not to keep any transaction and dirty UI state,
                //we will do this through breeze and knockout
                $("#grid1").igGrid("rollback");
                console.log("performed rollback");
                saveResult.entities.forEach(function (item) {
                    var state = item.entityAspect.entityState;
                    if (state.isDetached()) {
                        // delete it from self.data
                        for (var i = 0; i < self.data().length; i++) {
                            if (self.data()[i].Id() == item.Id()) {
                                self.data.remove(self.data()[i]);
                                break;
                            }
                        }
                        // unfortunately Breeze treats modified and added entities as "Unchanged"
                        // so we need to differentiate between an update and a new add by ourselves
                    } else if (state.isAdded() || state.isModified() || state.isUnchanged()) {
                        var found = false;
                        // find the item in self.data, and modify the values
                        for (var i = 0; i < self.data().length; i++) {
                            if (self.data()[i].Id() == item.Id()) {
                                found = true;
                                // set new values
                                var currentItem = self.data()[i];
                                //currentItem.Id(item.Id());
                                currentItem.Description(item.Description());
                                currentItem.CreatedAt(item.CreatedAt());
                                currentItem.IsDone(item.IsDone());
                                break;
                            }
                        }
                        // Added
                        if (!found) {
                            self.data.push({
                                Id: item.Id,
                                Description: item.Description,
                                CreatedAt: item.CreatedAt,
                                IsDone: item.IsDone
                            }); // the rows in the grid will be added automatically
                        }
                    } 
                });
            }).fail(function (error) {
                console.log("Save failed: " + error.message);
            });
        },
        commitText: "Click to Save Changes using Breeze Entity Manager. the grid's transactions are read and passed to Breeze's API",
        commitButtonText: "Save Changes",
        activate: function () {
            //the router's activator calls this function and waits for it to complete before proceding
            if (this.data().length > 0) {
                return;
            }
            function Item(id, desc, createdAt, isDone) {
                return {
                    // we don't have to wrap with ko.observable() because it's already done by Breeze ! -; ) 
                    Id: id,
                    Description: desc,
                    CreatedAt: createdAt,
                    IsDone: isDone
                };
            };
            this.manager = new breeze.EntityManager("/api/todos");
            var query = breeze.EntityQuery
               .from("Todos")
               .orderBy("CreatedAt");
            var self = this;
            return this.manager.executeQuery(query).then(function (data) {
                data.results.forEach(function (item) {
                    self.data().push(new Item(item.Id, item.Description, item.CreatedAt, item.IsDone));
                });
            });
        },
        canDeactivate: function () {
            //the router's activator calls this function to see if it can leave the screen
            return app.showMessage('Are you sure you want to leave this page?', 'Navigate', ['Yes', 'No']);
        }
    };
});

   Now, here it gets really interesting, and the viewmodel code contains the bulk of our Breeze CRUD functionality. I will walk you over the “activate” and “commitClick” parts.

   In the “activate” function, which is part of the Durandal SPA lifecycle, we are basically doing several important things. First, we are creating a simple Item object, which holds several properties – Id, Description, CreatedAt, and IsDone. We don't want to bind the TodoItem entities directly to the grid, and have auto generation of columns at the same time, because the entities contain a lot more properties and metadata that we don’t need. Alternatively, we can bind the grid directly to the entities, and define our columns manually in the view (in the knockout data-bind attribute). Then, we are querying the Todos Web API using Breeze, and if the query is successful, we are putting the TodoItems in our “data” knockout observable array, which binds to the grid:

this.manager = new breeze.EntityManager("/api/todos");
            var query = breeze.EntityQuery
               .from("Todos")
               .orderBy("CreatedAt");
            var self = this;
            return this.manager.executeQuery(query).then(function (data) {
                data.results.forEach(function (item) {
                    self.data().push(new Item(item.Id, item.Description, item.CreatedAt, item.IsDone));
                });
            });

   Now once the grid is loaded, we can do any number of batch updates – edit rows, add records ,delete record. We have autoCommit set to false (which is the default), so the grid won’t try to update anything automatically until we ask it to do so. The key here is that as we are doing our client-side updates, the grid will build-up a transaction log for every operation. The transaction log is additionally optimized in the sense that if we add a record, and delete it afterwards, no transaction is stored in the log. Also, if we update a value, and revert back to the original one, no transaction is added, too.

   Here is a grid with a couple of updates performed to it (as you can see from the CSS styles, the updates are pending and aren’t persisted):

   When we hit the “Save Changes” button, the “commitClick” viewmodel function gets invoked what we do is retrieve the grid’s pending transaction log, iterate over it, use the Breeze API in order to perform the updates through the Breeze entity manager, and then after we save the changes through the breeze entity manager, we rollback our grid changes using the grid API, so that the grid doesn’t have to do the same thing using its own persistence functionality. This allows us to have all the great updating functionality in the grid, as well as the freedom to override the grid’s persistence logic and do that using Breeze instead.

   Let me provide some more details about how this is done. We retrieve the transaction log by using the Grid widget’s public API:

transactions = $("#grid1").igGrid("pendingTransactions");

Then we iterate through the transactions, and check if it’s a new row, deleted row, or an edited row, and execute the corresponding breeze entity manager calls:

transactions.forEach(function (t) {
    if (t.type == "newrow") {
        // create an entity using Breeze's API
        self.manager.createEntity('TodoItem', t.row);
    } else if (t.type == "deleterow") {
        // find the entity
        var entity = self.manager.getEntityByKey("TodoItem", t.rowId);
        entity.entityAspect.setDeleted();
    } else {
        // update of an existing entity
        // 1. find the entity
        var entity = self.manager.getEntityByKey("TodoItem", t.rowId);
        // 2. fill new values
        //entity.Id(t.row.Id);
        entity.Description(t.row.Description);
        entity.CreatedAt(t.row.CreatedAt);
        entity.IsDone(t.row.IsDone);
        // 3. update is done automatically (internally) by Breeze / ko, as soon as you modify a prop
        // so we don't need to set any state manually
    }
});

Then, in the success callback for Breeze’s saveChanges(), we rollback our grid transactions, and read the save result, in order to add/delete/update our knockout observable array with the actual updated data. Note that this happens after the server-side code has persisted this data, and the beauty of it is that since the Ignite Grid supports full two-way databinding with Knockout.JS, we only need to perform those updates in the knockout observable, and the affected UI (grid rows and cells) will be automatically updated!

saveResult.entities.forEach(function (item) {
    var state = item.entityAspect.entityState;
    if (state.isDetached()) {
        // delete it from self.data
        for (var i = 0; i < self.data().length; i++) {
            if (self.data()[i].Id() == item.Id()) {
                self.data.remove(self.data()[i]);
                break;
            }
        }
        // unfortunately Breeze treats modified and added entities as "Unchanged"
        // so we need to differentiate between an update and a new add by ourselves
    } else if (state.isAdded() || state.isModified() || state.isUnchanged()) {
        var found = false;
        // find the item in self.data, and modify the values
        for (var i = 0; i < self.data().length; i++) {
            if (self.data()[i].Id() == item.Id()) {
                found = true;
                // set new values
                var currentItem = self.data()[i];
                //currentItem.Id(item.Id());
                currentItem.Description(item.Description());
                currentItem.CreatedAt(item.CreatedAt());
                currentItem.IsDone(item.IsDone());
                break;
            }
        }
        // Added
        if (!found) {
            self.data.push({
                Id: item.Id,
                Description: item.Description,
                CreatedAt: item.CreatedAt,
                IsDone: item.IsDone
            }); // the rows in the grid will be added automatically
        }
    } 
});

   After this is done, the italic “dirty” css styles in the grid are removed, and we get a UI which doesn’t have any pending updates on it. In the meantime, you can use the Chrome Dev tools or Firebug in order to track the requests that Breeze makes to the controller:

   Last but not least, we need to modify ~/App/viewmodels/shell.js, in order to register our view with the Durandal router:

router.mapNav('iggridbreezejs');

   The attached zip file contains the whole VS 2012 IGniteUI HotTowel project. I’ve deleted the Bin folder and the packages folder contents in order to make the zip smaller, and also enabled NuGet’s package restore, so that the required references get added automatically when you build the project.

   To sum up, in this blog post I’ve demonstrated how easy it is to combine the power of the Ignite grid with frameworks like Breeze, Durandal and knockout. In order to make this experience really seamless, I’ve used John Papa’s Hot Towel SPA template. One of our goals as a reusable components vendor is to give you as much out of the box functionality as possible, while at the same time let you use any third party framework that you like, thus keeping flexibility for you, the developer, at a maximum level.

I hope you’ll enjoy playing with this app – please don’t hesitate to contact me at atodorov@infragistics.com in case you have any feedback! 

HotTowel_IgniteUI.zip