You will need copies of the following files:
PLEASE NOTE: You must add a reference to the .d.ts file at the top of the typescript file
⁄⁄⁄ <path="typings/backbone.marionette/index.d.ts" ⁄>
W now know how to create Models, Collections and a number of different types of Views, but we want to load data from the webserver or a webservice instead of manually setting Model attribute values as we have been doing so far.
Instead of loading the webpage and then loading the initial data, the recommended approach is to get the webserver to write any initial data into the webpage itself and eliminate the delay caused by loading the data using AJAX. Once the any initial data is loaded, we want to use a REST web service approach to download any additional data and to make modifications. The Backbone Model and Collection classes take care of this for us, if we set the up correctly.
Lets begin by creating the Model, Collection and View classes that we need.. When we create the Modle we need to set the idAttribute option, this changes the default name of identifier attribute "id" used by the Model to a new name. In this case, we change it to "Id" to match the database table primary key attribute name (actually I haven't create the database in this example)..
namespace ExampleDataHandlingNS
{
export class MessageModel extends Backbone.Model
{
constructor(id: number = 0, text: string = "")
{
super({ // Model Attributes (from a database table?)
Id: id,
Text: text
},
{ // Model options
idAttribute: "Id"
});
}
get Id(): string { return this.get('Id'); }
get Text(): string { return this.get('Text'); }
SetValues(value: { Id: number, Text: string }, options?: any): void
{
this.set(value, options); // Set all the attributes in one go using Backbone
}
}
export class MessageView extends Backbone.Marionette.ItemView<MessageModel>
{
constructor(regionModel: MessageModel, el: any)
{
super({
model: regionModel,
el: el,
template: '#message-template'
});
}
}
// Specify the Collection
export class MessageCollection extends Backbone.Collection<MessageModel>
{
constructor() // No initial model data, added later
{
super(undefined, {
model: MessageCollection.CreateModel
});
}
private static CreateModel(attrs?: any, options?: any): MessageModel
{
let model: MessageModel = new MessageModel();
model.SetValues(attrs, options);
return model;
}
}
export class MessageListView extends Backbone.Marionette.ItemView<MessageModel>
{
constructor(messages: MessageCollection, el:any)
{
super({
collection: messages,
el: el,
template: "#messages-template"
});
}
}
}
The templates for displaying the messages looks like this:
<script id="message-template" type="text/html">
<p>Displaying MessageView {%= Id %} {%= Text %}</p>
</script>
<script id="messages-template" type="text/html">
<p>Displaying MessageListView</p>
<!-- Use the template to iterate through the items -->
<ul>
{% _.each(items, function(item){ %}
<li>{%= item.Id %} {%= item.Text %}</li>
{% }); %}
</ul>
</script>
We use these classes in the usual way BUT we can now load the initial data from the webpage, where the data is written into the page at the locations "#model-data" and "#collection-data".
class ExampleDataHandlerPage
{
PageLoad(): void
{
// Load a Model
// ============
let messageModel: ExampleDataHandlingNS.MessageModel = new ExampleDataHandlingNS.MessageModel();
// Read the contents of the <scrip>> tag as text and convert it to JSON.
messageModel.SetValues(JSON.parse($("#model-data").contents().text().trim())); // Pass object to set() to populate model
let view1: ExampleDataHandlingNS.MessageView = new ExampleDataHandlingNS.MessageView(messageModel, "#container1");
view1.render();
LOADING A COLLECTION
let messageCollection: ExampleDataHandlingNS.MessageCollection = new ExampleDataHandlingNS.MessageCollection();
// Use a reset method populate models in collection.
// JSON is an array of object literals, one for each model.
messageCollection.reset(JSON.parse($("#collection-data").contents().text().trim()));
let view2: ExampleDataHandlingNS.MessageListView = new ExampleDataHandlingNS.MessageListView(messageCollection, "#container2");
view2.render();
}
}
$(function ()
{
let page: ExampleDataHandlerPage = new ExampleDataHandlerPage();
page.PageLoad();
});
The .aspx webpage looks like this:
<!DOCTYPE html&g;
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<link href="Content/bootstrap.css" rel="stylesheet" />
</head>
<body>
.... Underscore templates ....
<!-- SERVER SIDE CODE INSERTS JSON HERE -->
<!-- We define type as text/json to prevent the JSON being executed as
JS, this also stops XSS attacks -->
<script id="model-data" type="text/json">
<asp:Literal ID="jsonText" runat="server">
</asp:Literal>
</script>
<!-- SERVER SIDE CODE INSERTS JSON HERE -->
<script id="collection-data" type="text/json">
<asp:Literal ID="jsonCollectionText" runat="server">
</asp:Literal>
</script>
<!-- Display here -->
<div id="container1" class="well">
</div>
<div id="container2" class="well">
</div>
<script src="Scripts/jquery-3.1.1.min.js"></script>
<script src="Scripts/underscore.js"></script>
<script type="text/javascript">
// Avoid content between ASP.Net use of delimiters and
// Underscore.js, change Underscore to use {= } instead.
_.templateSettings = {
interpolate: /\{%=(.+?)%\}/g,
escape: /\{%-(.+?)%\}/g,
evaluate: /\{%(.+?)%\}/g
};
</script>
<script src="Scripts/backbone.js"></script>
<script src="Scripts/backbone.marionette.js"></script>
<script src="Scripts/bootstrap.min.js"></script>
<script src="Scripts/example11.js"></script>
</body>
</html>
Next we need to add the server side code to populate this page with the initial Model and Collection data. We have used to the ASPX Literal tags "jsonText" and "jsonCollectionText" to mark the location where the server will write the data. Keeping it simple, it might look like this:
public partial class example11 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
var model = new { Id = 1, Text = "Hi There" };
var collection = new[] {
new { Id = 1, Text = "Hi" },
new { Id = 2, Text = "There" }
};
jsonText.Text = JsonConvert.SerializeObject(model);
jsonCollectionText.Text = JsonConvert.SerializeObject(collection);
}
}
As an alternative, we may want just live with the delay in reading the data when the View open. Lets extend the example by using the webservice that has been provided. First we need the Model, Collection and View classes. When creating the Collection class we need to specify the URL of the webservice so that the Collection can populate itself. Each database record would be passed as a JSON object, the Collection converts each object to a Model by calling the CreateModel() method we have added to the class.
The ItemView class is fairly simple, in this case we load the data immediately the view is created by calling the fetch() function within the constructor.
Also, notice how we call the render() function when the "sync" event occurs on the Collection in the ItemView class. This immediately, redraws the view when any changes are made to the Collection.
namespace ExampleDataHandlingNS
{
export class UserModel extends Backbone.Model
{
constructor()
{
// You don't have to specify the attributes, the model received from the REST service
// is merged straight in and they are created for us. Although, you can do if
// you wish to keep track of the attribute names.
super();
this.idAttribute = "usrId";
}
get UsrId(): number { return this.get('usrId'); }
get Name(): string { return this.get('name'); }
get Email(): string { return this.get('email'); }
get UName(): string { return this.get('username'); }
get Password(): string { return this.get('password'); }
SetValues(values: { usrId: number, name: string, email: string, username: string, password: string }, options?: any): void
{
this.set(values, options); // Set all the attributes in one go using Backbone
}
}
export class UserCollection extends Backbone.Collection<UserModel>
{
constructor()
{
// You don't have to specify the attributes, the model received from the REST service
// is merged straight in and they are created for us. Although, you can do if
// you wish to keep track of the attribute names.
super(undefined, {
model: UserCollection.CreateModel
});
this.url = 'http://localhost:1747/UserService.svc/users' // can use url: function() { } instead
}
private static CreateModel(attrs?: any, options?: any): UserModel
{
let model: UserModel = new UserModel();
model.SetValues(attrs, options);
return model;
}
}
export class UserListView extends Backbone.Marionette.ItemView<UserModel>
{
constructor(collection: UserCollection, el: any)
{
super({
collection: collection,
el: el,
template: "#users-template",
});
this.collectionEvents = {
"sync": this.render // Call this.render() method on collection "sync" event
}
this.delegateEvents();
this.collection.fetch({ // LOAD DATA IMMEDIATELY
error: this.OnError
});
}
private OnError = (collection: UserCollection, response: JQueryXHR, options: Backbone.Marionette.ItemViewOptions<UserModel>): void =>
{
window.alert('error!');
}
}
Use it like this:
PageLoad(): void
{
let userCollection: ExampleDataHandlingNS.UserCollection;
let view: ExampleDataHandlingNS.UserListView;
userCollection = new ExampleDataHandlingNS.UserCollection();
// Because we added fetch to constructor, just
// creating the view causes a fetch and render
view = new ExampleDataHandlingNS.UserListView(userCollection, "#container");
// THERE WILL BE A PAUSE UNTIL DATA LOADS AND VIEW IS DISPLAYED
}
There are a number of alternative ways to load the data:
1. You can display a loading message using the events "request" and "sync" like this:
constructor(collection: UserCollection, el: any)
{
super({
collection: collection,
el: el,
template: "#users-template",
});
this.collectionEvents = {
"sync": this.render // Call this.render() method on collection "sync" event
}
this.delegateEvents();
// Warning: Request doesn't work unless you do it here!!
// Known bug?: http://stackoverflow.com/questions/19053708/backbone-marionette-using-emptyview-for-loading
this.listenTo(this.collection, 'request', this.OnLoad);
// We could use the Marionette collectionEvents for this 'sync' event.
this.listenTo(this.collection, 'sync', this.OnSync);
this.collection.fetch({
error: this.OnError
});
}
private OnLoad = (): void =>
{
//show loading "screen" here
}
private OnSync = (): void =>
{
// hide loading "screen" here
this.render();
}
2. To delay the data loading, rather than calling fetch() immediately in the constructor, add a method to the ItemView that calls fetch() . Then call the method whenever you want to load new data.
export class UserListView extends Backbone.Marionette.ItemView<UserModel>
{
constructor(collection: UserCollection, el: any)
{
super({
collection: collection,
el: el,
template: "#users-template"
});
this.collectionEvents = {
"sync": this.render
}
this.delegateEvents();
}
// To delay the fetch.
Fetch(): void
{
this.collection.fetch();
}
}
PageLoad(): void
{
let view: ExampleDataHandlingNS.UserListView;
userCollection = new ExampleDataHandlingNS.UserCollection();
view = new ExampleDataHandlingNS.UserListView(userCollection, "#container");
// FETCH DATA NOW!!
// Calling this method updates the Collection, and also
// indirectly causes the Collection to produce a 'sync' event
// which the View handles causing it to re-render itself.
view.Fetch();
// THERE WILL BE A PAUSE UNTIL DATA LOADS AND VIEW IS DISPLAYED
}
3. We have already covered how to use a CollectionView and the code required (shown below) is identical except we use fetch() to load the data into the Collection. The UserCollection we have already created above handles the REST exchange for us.
namespace ExampleDataHandlingNS
{
export class LoadingUserView extends Backbone.Marionette.ItemView<UserModel>
{
constructor()
{
super({
template: "#empty-users-template"
});
}
}
export class UserView extends Backbone.Marionette.ItemView<UserModel>
{
constructor(childViewOptions: Backbone.Marionette.ItemViewOptions<UserModel>)
{
// Add "template" option to supplied ItemViewOptions
// before passing them to the parent constructor.
super(_.extend({ template: "#user-template" }, childViewOptions));
}
}
export class UserCollectionView extends Backbone.Marionette.CollectionView<UserModel, UserView>
{
constructor(collection: UserCollection, el: any)
{
super({
collection: collection,
el: el,
emptyView: LoadingUserView,
childView: UserView
});
this.collectionEvents = {
"sync": this.render // Call this.render() method on "sync" event
}
this.delegateEvents();
// Tell the collection to load the models
this.collection.fetch({
error: this.OnError
});
}
private OnError = (collection: UserCollection, response: JQueryXHR, options: Backbone.Marionette.ItemViewOptions<UserModel>): void =>
{
window.alert('error!');
}
}
}
PageLoad(): void
{
let view: ExampleDataHandlingNS.UserCollectionView;
userCollection = new ExampleDataHandlingNS.UserCollection();
view = new ExampleDataHandlingNS.UserCollectionView(userCollection, "#container");
// THERE WILL BE A PAUSE UNTIL DATA LOADS AND VIEW IS DISPLAYED
}
Continuing the previous exercise, look at the code for the provided REST webservice and write your own webservice for providng lists of books. Alter the application code you have been writing to use the webservice you just created, and display books provided by the webservice rather than the manual approach you previously used..
Populate the view with data written into the webpage, but provide a button to allow the user to refresh the data at any time.