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 -
Genre Insert Screens -
Genre Delete Screens -
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.
@{
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.
$(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();
}
});
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;
}
});
var GenreCollection = Backbone.Collection.extend({
model: Genre,
url: '/StoreManager/GenreList/'
});
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 () {
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");
}
});
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");
}
});
}
});
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();
}
});
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.
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Genres(int? Id)
{
return View();
}
[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;
}
[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);
}
[AcceptVerbs(HttpVerbs.Post)]
public JsonResult GenreList(Genre CreateGenre)
{
db.Genres.Add(CreateGenre);
db.SaveChanges();
return Json(CreateGenre, JsonRequestBehavior.DenyGet);
}
[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