Introduction
In this tutorial lets probe into the implementation of backbone.js on the mvc music store application (http://mvcmusicstore.codeplex.com/) created on mvc 3 razor.
Backbone.js (http://documentcloud.github.com/backbone/)
Backbone is a javascript plugin which provides a mvc framework to build client side application easily. Really!! well that's for you to assert. This is a claim from backbone.js.
Here is a tutorial available on youtube (http://www.youtube.com/watch?v=vJwgIth1I_w&feature=related) which pretty much explains the purpose of backbone.
In this tutorial we going to create a new view on the mvc music store to List, Add, Delete and Update Genres.
Background
- We are going to List the Genres from the database as collection the backbone view. (HTTP GET)
- While clicking update we would update the description to unknown and update the database. (HTTP PUT)
- While clicking the delete we would delete the record from the view as well as from the database. (HTTP DELETE)
- While clicking the create new we would add an entry like [{name:"Unknown", Description:"Unknown" }]. (HTTP POST)
Lets keep this as simple as possible. I would upload another tutorial in which we shall discuss the real time updates and inserts on the backbone.
Using the Code
Controller Methods
In the store manager controller lets add the following methods for the Genres view. The Genres Action is to return the StoreManager/Genres view which has our backbone script. Please note the return tupe of the GenreList Actions are all JSONResults and also note the AcceptVerbs. Backbone recognises the methods by name and the http action.
Http:Get StoreManager/GenreList/1 will call the get action for Genreid = 1 returns the selected genre.
Http:Get StoreManager/GenreList/ will call the get action returns all genres.
Http:put StoreManager/GenreList/1 will call the put action it will update the genre with Genreid =1
Http:Delete StoreManager/GenreList/1 will call the delete action to delete the genre with GenreId = 1
Http:Post StoreManager/GenreList/ will insert the new genre.
Things to note is that the action method names are all same 'GenreList'.
[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)
{
if (CreateGenre.GenreId != 0)
{
return GenreList(CreateGenre.GenreId, CreateGenre);
}
else
{
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);
}
Genre View
'#Genre_Container' is going to be our actual view in which we would be populating the Genre collection. We have this '#Genre_List' in which we have defined the header of the table 'Name' and 'Description'. We would be using the '#Genre-Template' to clone and populate table with the genre list. Note that the create new button and the edit, delete buttons are enclosed with in the '#Genre_Container'. Backbone will only recognise the events of the DOM within its application view.
<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>
<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= "Details" class="Detail" /> | <input type="button" value= "Delete" class="Delete" /> </td>
</script>
</fieldset>
</div>
Genre.Js
We would first create a Genre model inherited from the Backbone.Model.
Genre Model
- URL
- This property contains the url which backbone can use to ajax get a
Genre data. In this case it is pointing to /StoreManager/GenreList/id.
- Initialize - Constructor of the Genre model.
- defaults
- Set the default values for the Genre model properties. At the time of
instantiating an object if the object properties are null, it would
fill it with the default values.
Since our Genres page lists a
collection of genres, we will be creating a Genre Collection inherited
from Backbone.Collection. If a page you are dealing with displays a
single Object information say Genre you may not need to use the
collections. It all depends on the requirement.
Genre Collection
- model - This property sets the type of the Collection. In our case its set to Genre type.
- url
- This property contains the url which backbone can use to Ajax get the
Genres data and load the collection. The collection is loaded using the
fetch function. In our case its pointing to /StoreManager/GenreList/.
Now
we have the models created, lets create some views to display the data.
We will create a Genre View which for each Genre object in the
collection. Also a AppView to display to append the genreview to the
'#Genre_List'.
$(function () {
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');
},
defaults: {
GenreId: 0,
Name: 'Unknown',
Description: 'Unknown'
}
});
var GenreCollection = Backbone.Collection.extend({
model: Genre,
url: '/StoreManager/GenreList/'
});
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 () {
this.model.set({ Description: 'Unknown' });
var self = this;
this.model.save(this.model, { success: function () {
$("input:button", $(self.el)).button();
}
});
},
DeleteGenre: function () {
this.model.destroy();
}
});
var AppView = Backbone.View.extend({
initialize: function () {
this.collection.bind('add', this.AppendGenre, this);
},
el: '#Genre_Container',
counter: 15,
events: {
"click #btnCreateNew": "AddNewGenre"
},
AddNewGenre: function () {
console.log('Add Genre....');
this.counter++;
var newGenre = new Genre({ Name: 'Unknown ' + this.counter, Description: 'Damn ' + this.counter });
this.collection.add(newGenre);
newGenre.save(newGenre, { success: function () {
$("input:button", "#Genre_List").button();
}
});
},
AppendGenre: function (genre) {
var genreView = new GenreView({ model: genre });
$(this.el).find('table').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();
}
});
});
Genre View
- template - This property
points to the template script in the html. We will use _.template
function to substitute the object data inside the template. It our case
it returns the html
<td>name</td><td>description</td>
- tag -
This property has the DOM tag to be used to enclose the genre view. In
our case its a <tr>. So the Genre View would return a html like
<tr>template</tr>. Failing to give the tag property will
force backbone to assume that the tag is <div>.
- model - The model property will hold the Genre object.
- Initialize
- Its the constructor. Inside the constructor we will bind any change
event on the model to render event of the genre view and remove event to
unrender event.
- render - the render function parses the template against the model object and returns the genre view in our case the table row.
- unrender - the unrender function removes the corresponding genre view from the html.
- Events
- Any DOM element enclosed within the genre view can be event handled
inside the view. We are handling the Edit, Delete button events in this
case. Note that we need to use the class name of the buttons to trigger
the events.
- EditGenre - Triggered when Edit button is clicked. It
updates the model.description to 'unknown' and saves the model. Saving
the model will Ajax put StoreManager/GenreList/Id controller method.
Since the model is changed, it would trigger the render method and that
would update the DOM.
- DeleteGenre - Triggered when Delete button
is clicked. It will delete the model (destroy). It would Ajax delete
StoreManager/GenreList/Id controller method. Since model is deleted as
per the code in the constructor it wil trigger the unrender method.
App View
Since we are dealing with Genre List we will be using the Collection property on the App View rather than the model property.
- Initialize - In the constructor we bind the add event on the collection to AppendGenre event.
- el - It is the DOM on which the Application view will be applied. In our case it is the '#Genre_Container'.
- events - We bind the Add Genre button to the AddNewGenre event.
- AppendGenre - It is called for each object in the collection. It instantiates a GenreView with model = genre and appends the genre view (after rendering) on the el.
- AddNewGenre - In this event we instantiate a new genre model object, add that to the collection. Then call the save on the genre object. Which will call the controller method and render the genre view. Add on the collection will trigger AppendGenre.
- render - loops through the genre collection and appends the genre to the DOM.
Now we are all set. The only this remaining is to instantiate a Genre Collection. Then Instantiate a Genre view and set its collection to genrecollection. The next step is to call fetch on the collection. On fetch Success we will render the App View.
Hope that explains the Backbone concepts. I' m working on enhancing the application to do real time updates and inserts, using dialog boxes and some validations. Keep watching this space for more updates. Also I would do a tutorial on Routes in backbone.
Points of Interest
After creating a new genre in the view, clicking the corresponding Edit triggers a POST instead of a PUT. I' m still reasearching into this. Right now the Post controller is written to handle both inserts and updates. Note that there is a weird javascript bug in the uploaded code which prevents it from working properly in internet explorer. The application works fine in Chrome, Firefox and Safari.
- ANON