Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Backbone.js in asp.net mvc razor Part 2

4.25/5 (4 votes)
26 Apr 2012CPOL3 min read 93.4K   1.1K  
Add, Update, Delete objects using backbone

Introduction

This is an upgrade on my first backbone tutorial. In the first tutorial we were hardcoding the data to be updated / inserted, now we would be doing real-time updates. 

Background 

Genre Update Screens -   

 Image 1 

 Image 2 

 Image 3 

Genre Insert Screens -  

 Image 4 

 Image 5 

 Image 6 

Genre Delete Screens -   

 Image 7 

Using the code

Genre.cshtml-  

We will now add two templates similar to last tutorial. '#Genre-Edit-Template' for Insert and Update and '#Genre-Delete-Template' for Delete. 

HTML
@{
    ViewBag.Title = "Genres";
}
<script src="@Url.Content("~/Scripts/StoreManager/Genre.js")" type="text/javascript"></script>
<div class="styler">
    <fieldset class="ui-widget">
        <legend class="ui-state-legend-default ui-corner-top ui-corner-bottom">Genre List -
            Using Backbone</legend>
        <div id="Genre_Container">
            <input type="button" value="Create New" class="Add" id="btnCreateNew" />
            <table id="Genre_List">
                <tr>
                    <th>
                        Name
                    </th>
                    <th>
                        Description
                    </th>
                    <th>
                    </th>
                </tr>
            </table>
            <div id="GenreEditDialog" style="height: 350">
                <div class="styler">
                    <fieldset class="ui-widget" id="Genre_Edit" style="height: 350">
                        <legend class="ui-state-legend-default ui-corner-top ui-corner-bottom">                          </legend>
                        <br />
                        <div id="valSum" >
                        </div>
                    </fieldset>
                </div>
            </div>
        </div>
        <script id='Genre-Template' type='text/template'>
                <td><%=Name%></td> <td><%=Description%></td>
                <td><input type="button" value= "Edit" class="Edit" /> | <input type="button" value= "Delete" class="Delete" /> </td>
        </script>
        <script id='Genre-Edit-Template' type='text/template'>
               <tr>
                <td>
                    <div class="editor-label">
                        Name</div>
                    <div class="editor-field">
                        <input type="text" class="gName" value='<%=Name%>' />
                    </div>
                </td>
                <td>
                    <div class="editor-label">
                        Description</div>
                    <div class="editor-field">
                        <input type="text" class="gDesc" value='<%=Description%>' />
                    </div>
                </td>
            </tr>
            <tr>
                <td colspan="2">
                    <input type="button" value="Save" class="Update" /> | <input type="button" id="btnCancel" value="Cancel" class="Cancel" />
                </td>
            </tr> 
        </script>
        <script id='Genre-Delete-Template' type='text/template'>
            <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0 .7em;
            width: 500px">
                <p>
                <span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>
                <strong>Confirm</strong><br />
                Are you sure you want to delete this Album - <%=Name%> ?</p>
            </div><br/>
            <table>
                <tr>
                    <td>
                        <div class="editor-label">
                            Name</div>
                        <div class="editor-field">
                            <%=Name%>
                        </div>
                    </td>
                    <td>
                        <div class="editor-label">
                            Description</div>
                        <div class="editor-field">
                            <%=Description%>
                        </div>
                    </td>
                </tr>
                <tr>
                    <td colspan="2">
                        <input type="button" value="Delete" class="Update" /> | <input type="button" id="btnCancel" value="Cancel" class="Cancel" />
                    </td>
                </tr>
            </table>
        </script>
    </fieldset>
</div> 

 Genre.js - 

Well this is no rocket science you would have guessed by now that we need to have two views EditView and DeleteView and Instantiate them (passing the genre model) on click of the update and delete button respectively. In case of a create we will be instantiating the EditView with new genre model. Then Render the views so that we can view them.

Genre Model - 

We have a new function validateModel added. This function is going to validate the model and show a validation summary.

Also in my last tutorial I had pointed out that after creating a new genre, clicking the corresponding Edit triggers a Http Post instead of a put. The reason is that, even after adding the new genre the genre is identified as a new object (genre.isNew() is true), backbone always does a post on a new object. Reason? Well our Genre entities in our database does not have a property called id. Backbone needs this property to validate if an object is new or old. We are handling this by setting the GenreId to id on change of the GenreId property.   

EditView - 

In the Editview note that we are directly binding the Name and Description textbox ('.gName', '.gDesc') and set the value to the model. By doing this we are making the best use of backbone.js, we are making the view and model to interact with each other. Isn't that cool.

While rendering the view (render function) we are opening a dialog which contains the edit view, setting the title 'Create' or 'Edit. Otherwise its the same old applying template on the 'el' and appending on the view container '#Genre_Edit'.

While updating the model, we call the validateModel () function of the model, display validation summary on error otherwise save the model. Note that while 'Create New' we add the new genre to the collection and save the model.

By this we are able to accomplish Insert and Edit Genre using a single View.

DeleteView - 

 I kinda feel this view is self explainatory. The only thing we doing different is we are destroying the model. 

JavaScript
$(function () {
    $("#GenreEditDialog").dialog({
        autoOpen: false,
        show: "blind",
        hide: "explode",
        modal: true,
        height: 250,
        width: 600,
        open: function (event, ui) {
            $('.ui-dialog-titlebar', ui.dialog || ui).hide();
        }
    });

    // Genre Model
    var Genre = Backbone.Model.extend({
        url: function () {
            var base = '/StoreManager/GenreList/';
            if (this.isNew())
                return base;
            return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id;
        },
        initialize: function () {
            console.log('Genre Constructor Triggered');
            this.bind("change:GenreId", function () {
                var genreId = this.get('GenreId');
                this.set({ id: genreId });
            });
        },
        defaults: {
            GenreId: 0,
            Name: '',
            Description: ''
        },
        validateModel: function () {
            var validate = true;
            var divHtml = "<div class='ui-state-error ui-corner-all' style='display: block; padding-top: 0px; padding-bottom: 0px; width: 500px; padding-left: 0.7em; padding-right: 0.7em;'>";
            divHtml += "<p><span class='ui-icon ui-icon-alert' style='float: left; margin-right: .3em;'></span><strong>Please correct these Errors</strong> </p><div class='validation-summary-errors' data-valmsg-summary='true'>";
            divHtml += "</div><p></p></div>";
            var ulHtml = "<ul></ul>";
            var div = $(divHtml);
            var ul = $(ulHtml);
            if (this.get('Name') == "") {
                ul.append("<li>Name is Mandatory</li>");
                validate = false;
            }
            if (this.get('Description') == "") {
                ul.append("<li>Description is Mandatory</li>");
                validate = false;
            }
            div.find(".validation-summary-errors").append(ul);

            if (!validate) {
                $('#valSum').show("slow");
                $('#valSum').html(div);
            }
            else {
                $('#valSum').hide("slow");
                $('#valSum').html("");
            }

            return validate;
        }
    });

    // Genre Collection
    var GenreCollection = Backbone.Collection.extend({
        model: Genre,
        url: '/StoreManager/GenreList/'
    });

    //Genre-Edit view
    var EditView = Backbone.View.extend({
        template: _.template($('#Genre-Edit-Template').html()),
        tagName: "table",
        container: "#Genre_Edit",
        initialize: function () {
            console.log('Genre Edit View Constructor Triggered');
        },
        events: {
            "change .gName": 'NameChange',
            "change .gDesc": 'DescChange',
            "click .Update": 'UpdateGenre',
            "click .Cancel": 'CancelGenre'
        },
        NameChange: function () {
            // Directly bind the name change to the model, best use of back bone.
            // Otherwise set the data during save which ever you prefer
            this.model.set({ Name: $(".gName").val() });
        },
        DescChange: function () {
            this.model.set({ Description: $(".gDesc").val() });
        },
        render: function () {
            $(this.container).append($(this.el).html(this.template(this.model.toJSON())));
            $("input:button", $(this.el)).button();
            $("#GenreEditDialog").dialog("open");
            var title = 'Create Genre';
            if (this.collection == null)
                title = 'Edit Genre - ' + this.model.get('Name');
            $(this.container).find('legend').html(title);
            $('.ui-dialog-titlebar').hide();
            $('#valSum').hide("slow");
            return this;
        },
        unrender: function () {
            $(this.el).remove();
            return this;
        },
        UpdateGenre: function () {
            if (this.model.validateModel()) {
                var self = this;
                if (this.collection != null) {
                    this.collection.add(this.model);
                }
                this.model.save(this.model, { success: function () {
                    self.unrender();
                    $("#GenreEditDialog").dialog("close");
                    var genreId = self.model.get('GenreId');
                    $("input:button", '#Genre_List').button();
                }
                });
            }
            $("input:button", '#Genre_List').button();
        },
        CancelGenre: function () {
            this.model.fetch({ success: function () {
                $("input:button", '#Genre_List').button();
            } 
            });
            this.unrender();
            $("#GenreEditDialog").dialog("close");
        }
    });

    // Genre Delete View
    var DeleteView = Backbone.View.extend({
        template: _.template($('#Genre-Delete-Template').html()),
        container: "#Genre_Edit",
        initialize: function () {
            console.log('Genre Edit View Constructor Triggered');
        },
        events: {
            "click .Update": 'DeleteGenre',
            "click .Cancel": 'CancelGenre'
        },
        render: function () {
            $(this.container).append($(this.el).html(this.template(this.model.toJSON())));
            $("input:button", $(this.el)).button();
            $("#GenreEditDialog").dialog("open");
            $('.ui-dialog-titlebar').hide();
            var title = 'Delete Genre - ' + this.model.get('Name');
            $(this.container).find('legend').html(title);
            return this;
        },
        unrender: function () {
            $(this.el).remove();
            return this;
        },
        CancelGenre: function () {
            this.unrender();
            $("#GenreEditDialog").dialog("close");
        },
        DeleteGenre: function () {
            var self = this;
            this.model.destroy({ success: function () {
                self.unrender();
                $("#GenreEditDialog").dialog("close");
            }
            });
        }
    });

    // Genre View - el returns the template enclosed within a tr
    var GenreView = Backbone.View.extend({
        template: _.template($('#Genre-Template').html()),
        tagName: "tr",
        initialize: function () {
            console.log('GenreView Constructor Triggered');
            this.model.bind('change', this.render, this);
            this.model.bind('remove', this.unrender, this);
        },
        render: function () {
            console.log('Rendering...');
            $(this.el).html(this.template(this.model.toJSON()));
            return this;
        },
        unrender: function () {
            console.log('Un-Rendering...');
            $(this.el).remove();
            return this;
        },
        events: {
            "click .Edit": 'EditGenre',
            "click .Delete": 'DeleteGenre'
        },
        EditGenre: function () {
            var editView = new EditView({ model: this.model });
            editView.render();
        },
        DeleteGenre: function () {
            var deleteView = new DeleteView({ model: this.model });
            deleteView.render();
        }
    });

    // Actual App view
    var AppView = Backbone.View.extend({
        initialize: function () {
            this.collection.bind('add', this.AppendGenre, this);
        },
        el: '#Genre_Container',
        events: {
            "click #btnCreateNew": "AddNewGenre"
        },
        AddNewGenre: function () {
            console.log('Add Genre....');
            var newGenre = new Genre();
            var editView = new EditView({ model: newGenre, collection: this.collection });
            editView.render();
        },
        AppendGenre: function (genre) {
            var genreView = new GenreView({ model: genre });
            $(this.el).find('#Genre_List').append(genreView.render().el);
        },
        render: function () {
            if (this.collection.length > 0) {
                this.collection.each(this.AppendGenre, this);
            }
            $("input:button", "#Genre_List").button();
        }
    });

    var genres = new GenreCollection();
    var view = new AppView({ collection: genres });

    genres.fetch({ success: function () {
        view.render();
    }
    });
}); 

Controller Methods -  

Note that we are handling only the inserts in the httppost action now. Our earlier code handled both Inserts and Updates. Since we added the id attribute on the model, the update handling is no more neccessary.  

C#
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Genres(int? Id)
{
    return View();
}
// Get Genres
[AcceptVerbs(HttpVerbs.Get)]
public JsonResult GenreList (int? Id)
{
    if (Id.HasValue)
    {
        var genre = db.Genres
                        .Where(x => x.GenreId == Id.Value)
                        .Select(x => new { id = x.GenreId, GenreId = x.GenreId, Name = x.Name, Description = x.Description })
                        .FirstOrDefault();
        return Json(genre, JsonRequestBehavior.AllowGet);
    }
    var genres = db.Genres
                    .Select(x => new { id = x.GenreId, GenreId = x.GenreId, Name = x.Name, Description = x.Description })
                    .ToList();
    var result =  Json(genres, JsonRequestBehavior.AllowGet);
    return result;
}
// Update Genre
[AcceptVerbs(HttpVerbs.Put)]
public JsonResult GenreList(int Id, Genre UpdatedGenre)
{
    var genre = db.Genres.Where(x => x.GenreId == Id).FirstOrDefault();
    genre.Name = UpdatedGenre.Name;
    genre.Description = UpdatedGenre.Description;

    db.SaveChanges();
    return Json(genre, JsonRequestBehavior.DenyGet);
}
//Add Genre
[AcceptVerbs(HttpVerbs.Post)]
public JsonResult GenreList(Genre CreateGenre)
{
    db.Genres.Add(CreateGenre);
    db.SaveChanges();
    return Json(CreateGenre, JsonRequestBehavior.DenyGet);
}
//Delete Genre
[AcceptVerbs(HttpVerbs.Delete)]
public JsonResult GenreList(int Id)
{
    var genre = db.Genres.Where(x => x.GenreId == Id).FirstOrDefault();
    db.Genres.Remove(genre);
    db.SaveChanges();
    return Json(genre, JsonRequestBehavior.DenyGet);
} 

Points of Interest  

We are not using the Backbone.Validate function in our Genre Model, simply because we want to display the errors on a validation summary. The Validate function on the model is triggered on Save and Set of the object, but it does not validate the entire model. It will only validate the changed attributes in the  model. so implementing a validation summary using the Validate function is quite challenging.

One more thing In our 'validateModel' function we are doing some DOM manipulation. Well this is a bad practice. Its best practice to have our model seperated from any DOM manipulation code. All DOM manipulation should be inside the View. I have just taken some liberty in this tutorial. The better way to do this would be to handle the  validation summary inside the EditView.UpdateGenre function. Well I will leave that for you to figure it out. It should be easy I guess. 

Happy Coding.... 

-- ANON  

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)