Introduction
Ember.js is a powerful JavaScript MVC framework to create complex web applications that eliminates boilerplate and provides a standard application architecture, it supports UI Bindings, Composed Views, Web Presentation Layers, and plays nicely with others. In order to create a real-time interaction web application, I add a SignalR hub and REST service with ASP.NET MVC.
MVC Basics
The purpose of MVC pattern is to separate concerns of view, model, and controller. The model is where data is kept, the view describes the presentation of application, and the controller acts as the link between the model and the view.
This is Ember.js's MVC implementation, client side part.
There is another important concept in Ember.js, that is Router
or StateManager
, it works mostly like ASP.NET MVC Route, but in client side. In this article, a router does its responsibility as connecting the main controller to the main application view. Ember.js uses Handlebars integrated templates, for easy creating and maintaining views.
Set Up Project
At first, we need to create an empty ASP.NET MVC project. I use ASP.NET MVC4 with Razor view engine, this is the _Layout.cshtml.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
<script src="../../Scripts/libs/jquery-1.7.2.min.js" type="text/javascript"></script>
<script src="../../Scripts/libs/json3.min.js" type="text/javascript"></script>
<script src="../../Scripts/libs/handlebars-1.0.0-rc.4.js" type="text/javascript"></script>
<script src="../../Scripts/libs/ember-1.0.0-rc.6.js" type="text/javascript"></script>
<script src="../../Scripts/libs/ember-data.js" type="text/javascript"></script>
<script src="../../Scripts/jquery.signalR-1.0.0-alpha2.min.js" type="text/javascript"></script>
</head>
<body>
@RenderBody()
</body>
</html>
And Index.cshtml:
@{
ViewBag.Title = "Ember.n.SignalR";
}
<script type="text/x-handlebars">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="application">
{{view App.ApplicationView}}
</script>
<script src="../../Scripts/u.js" type="text/javascript"></script>
<script src="../../Scripts/app.js" type="text/javascript"></script>
- u.js contains two methods to random a string and a number in JavaScript
- app.js contains all JavaScript code for the application.
The Model in Server Side
We create a simple customer DTO (in this example, we use DTO as model) with basic information and simple result for REST method.
Customer.cs
[Serializable]
public class Customer
{
public Guid? Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public bool Active { get; set; }
}
Result.cs
public class Result
{
public int ErrorCode { get; set; }
public object ErrorMessage { get; set; }
public object Data { get; set; }
}
The REST Service
We already defined DTO, now we need to create a simple customer REST service transfer data in JSON format CustomerController.cs host at /customer/. To conform naming conventions between C# and JavaScript JSON object, I use Newtonsoft.Json.Serialization
with CamelCasePropertyNamesContractResolver
.
public class CustomerController : Controller
{
[AcceptVerbs(HttpVerbs.Post)]
public string Read(Guid? id)
{
}
[AcceptVerbs(HttpVerbs.Delete)]
public string Delete(Guid id)
{
}
[AcceptVerbs(HttpVerbs.Put)]
public string Update(Customer customer)
{
}
[AcceptVerbs(HttpVerbs.Post)]
public string Create(Customer customer)
{
}
}
About the data repository for customers, I create a simple implementation CrudDS.cs, storing data in the ~/App_Data/Customer.em physical file in binary format. Now all server side codes needed were done. We now focus on Ember application architecture in the next step.
The Ember Application
At a glance, we create an application object:
function getView(name) {
var template = '';
$.ajax(
{
url: '/Templates/' + name + '.htm',
async: false,
success: function (text) {
template = text;
}
});
return Ember.Handlebars.compile(template);
};
wnd.App = Ember.Application.create();
getView
: method, I define this method to synchronously getting a template via Ajax, then use Handlebars compile it to a view.
The Ember Model
We need to create a data store object at client side to interact with REST service /customer/.
App.Store = Ember.Object.extend({
update: function (customer) {
var message = null;
var xhr = $.ajax(
{
url: '/customer/update/',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(customer),
type: 'PUT',
async: false,
success: function (data) {
message = data;
}
});
if (xhr.status != 200) {
message = { errorCode: xhr.status, errorMessage: xhr.statusText };
}
return message;
},
read: function (id)
{
var message = null;
var xhr = $.ajax(
{
url: '/customer/read/',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify({ 'id': id }),
type: 'POST',
async: false,
success: function (data) {
message = data;
}
});
if (xhr.status != 200) {
message = { errorCode: xhr.status, errorMessage: xhr.statusText };
}
return message;
},
remove: function (id)
{
var message = null;
var xhr = $.ajax(
{
url: '/customer/delete/',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify({ 'id': id }),
type: 'DELETE',
async: false,
success: function (data) {
message = data;
}
});
if (xhr.status != 200) {
message = { errorCode: xhr.status, errorMessage: xhr.statusText };
}
return message;
},
create: function (customer) {
var message = null;
var xhr = $.ajax(
{
url: '/customer/create/',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(customer),
type: 'POST',
async: false,
success: function (data) {
message = data;
}
});
if (xhr.status != 200) {
message = { errorCode: xhr.status, errorMessage: xhr.statusText };
}
return message;
}
});
In the previous part, we already defined model in server side and the REST service just returns object in JSON format, in order binding it to views, we need to define it as an Ember model.
App.CustomerModel = Ember.Object.extend({
id: null,
firstName: null,
lastName: null,
email: null,
phone: null,
active: false,
quiet: false,
random: function () {
this.setProperties({ firstName: String.random(), lastName: String.random(),
email: String.random().toLowerCase() + '@gmail.com',
phone: '(097) ' + Number.random(3) + '-' + Number.random(4) });
return this;
},
plain: function () {
return this.getProperties("id", "firstName",
"lastName", "email", "phone", "active");
}
});
App.ResultModel = Ember.Object.extend({
errorCode: 0,
errorMessage: null
});
random
: Create a random customer by using two random methods in u.js. plain
: Get back JSON object before send to REST service to improve performance (no need for additional properties of Ember.Object
).
The Ember Controller
App.CustomerController = Ember.Controller.extend({
store: App.Store.create(),
currentResult: null,
currentCustomer: null,
random: function () {
var customer = App.CustomerModel.create().random();
if (this.get('currentCustomer')) {
this.get('currentCustomer')
.set('active', false)
.setProperties(this.get('currentResult').data);
}
this.set('currentCustomer', customer);
},
create: function (customer) {
this.set('currentResult', this.get('store').create(customer.plain()));
if (!this.currentResult.errorCode) {
this.set('currentCustomer', App.CustomerModel.create());
var newCustomer = App.CustomerModel.create(this.get('currentResult').data);
this.get('customers').pushObject(newCustomer);
}
},
remove: function (id) {
var customer = this.get('customers').findProperty('id', id);
if (!customer) return;
this.set('currentResult', this.store.remove(customer.id));
if (!this.currentResult.errorCode) {
if (this.get('currentCustomer').id === id) {
this.set('currentCustomer', App.CustomerModel.create());
}
this.get('customers').removeObject(customer);
}
},
read: function (id) {
this.set('currentResult', this.store.read(id));
if (!this.currentResult.errorCode) {
if (Ember.isArray(this.currentResult.data)) {
var array = Ember.ArrayController.create({ content: [] });
this.currentResult.data.forEach(function (item, index) {
array.pushObject(App.CustomerModel.create(item));
});
return array;
}
else {
var customer = this.get('customers').findProperty('id', this.currentResult.data.id)
customer && customer.setProperties(this.currentResult.data);
return customer;
}
}
else {
return id ? null : Ember.ArrayController.create({ content: [] });
}
},
update: function (customer) {
this.set('currentResult', this.store.update(customer.plain()));
if (!this.currentResult.errorCode) {
}
},
save: function (customer) {
var customer = this.get('currentCustomer');
if (!customer.id) {
this.create(customer);
}
else {
this.update(customer);
}
},
edit: function (id) {
if (this.get('currentCustomer').id != id) {
this.get('currentCustomer')
.setProperties({ active: false })
.setProperties(this.get('currentResult').data);
}
else {
return;
}
var customer = this.read(id);
this.set('currentCustomer', customer.set('active', true));
this.set('currentResult',
App.ResultModel.create({
errorMessage: 'Click Submit to save current customer.',
data: customer.getProperties("firstName",
"lastName", "email", "phone")
}));
},
customers: Ember.ArrayController.create({ content: [] }),
initialize: function () {
var array = this.read();
this.set('customers', array);
this.random();
this.set('currentResult', App.ResultModel.create({ errorMessage: 'Click Submit to create new customer.' }));
}
});
App.applicationController = App.CustomerController.create();
Our ApplicationController
controls logic when create, update or delete a customer, it stores result of each action in currentResult
property and stores editing/creating customer in currentCustomer
property. The customers array model is our customers storage, it will always be synchronized to server via App.Store
and it is used to bind to our views. We call this.read()
to retrieve all customers from server when our controller is initialized. Do you know why we must use get or set method here? That is the JavaScript way to deal with handling property changed. Actually in C#, the get/set properties compiled to get/set methods in CLR.
The Ember View
We saw the connecting between controller and model, now we dig into views. The view displays/binds values from / to model via controller anchor. I define view templates in separated htm files, load it via AJAX and use Ember.Handlebars
to compile the response text, it is easier for modifying than putting it in script
tag, we could use any HTML editor such as Microsoft Visual Studio, Notepad++...to edit view templates. Let's see create_edit_customer
template, it is defined at create_edit_customer.htm.
<div id="mainForm">
<div>
<label for="firstName">
First name</label><br />
{{view Ember.TextField valueBinding="controller.currentCustomer.firstName"}}
<a href="#" {{action "random"
on="click" target="view" }}>Random new customer</a>
</div>
<div>
<label for="lastName">
Last name</label><br />
{{view Ember.TextField valueBinding="controller.currentCustomer.lastName"}}
</div>
<div>
<label for="email">
Email</label><br />
{{view Ember.TextField valueBinding="controller.currentCustomer.email"}}
</div>
<div>
<label for="phone">
Phone</label><br />
{{view Ember.TextField valueBinding="controller.currentCustomer.phone"}}
</div>
<p>
<button id="submit" {{action "save"
on="click" target="view" }} >Submit</button>
</p>
</div>
The CreateEditCustomerView
is as follows:
App.CreateEditCustomerView = Ember.View.extend({
template: getView('create_edit_customer'),
init: function () {
this._super();
this.set('controller', App.applicationController);
},
save: function (event) {
this.get('controller').send('save');
},
random: function () {
this.get('controller').send('random');
},
name: "CreateEditCustomerView"
});
As you see in the template, there are some Handlebars syntax, The first name text box is defined as {{view Ember.TextField valueBinding="controller.currentCustomer.firstName"}}
. There are 2 actions in this view, first is random a new customer and second is save a customer, in the template, you easily found two lines {{action "random" on="click" target="view" }}
and {{action "save" on="click" target="view" }}
. Both of those actions are click event.
To display a list of customers, we need a template:
<div id="customerListHeader">
List of customers
</div>
<div id="customerListContent">
<table>
{{#unless controller.customers.length}}
<tr>
<th> First name </th>
<th> Last name </th>
<th> Email </th>
<th> Phone </th>
</tr>
<tr>
<td colspan="4" align="center">
There is no customer yet
</td>
</tr>
{{else}}
<tr>
<th> First name </th>
<th> Last name </th>
<th> Email </th>
<th> Phone </th>
<th> #Action </th>
</tr>
{{#each customer in controller.customers}}
<tr {{bindAttr class="customer.active:active:normal"}}
{{bindAttr id="customer.id"}}>
<td>
{{customer.firstName}}
</td>
<td>
{{customer.lastName}}
</td>
<td>
{{customer.email}}
</td>
<td>
{{customer.phone}}
</td>
<td align="center">
<a href="#" class="edit"
{{action edit customer target="view" }}>Edit</a>
|
<a href="#" class="delete"
{{action remove customer target="view" }}>Delete</a>
</td>
</tr>
{{/each}}
{{/unless}}
</table>
</div>
The CustomerListView
is as follows:
App.CustomerListView = Ember.View.extend({
template: getView('customer_list'),
init: function () {
this._super();
this.set('controller', App.applicationController);
},
edit: function () {
var id = customer.id;
var controller = this.get('controller').send('edit', id);
},
remove: function () {
var id = customer.id;
var controller = this.get('controller');
this.animateItem(id, function () {
controller.send('remove', id);
}, controller);
},
animateItem: function (id, callback, target) {
$('#' + id).animate({ opacity: 0 }, 200, "linear", function () {
$(this).animate({ opacity: 1 }, 200);
if (typeof callback == 'function') {
target = target | null;
callback.call(target);
}
});
},
name: "CustomerListView"
});
Each action create/edit/delete customer, we need to display message result, in MessageView
, this is message template.
{{#unless controller.currentResult.errorCode}}
<div id='message'>
{{controller.currentResult.errorMessage}}
</div>
{{else}}
<div id='error'>
{{controller.currentResult.errorMessage}}
</div>
{{/unless}}
CreateEditCustomerView
, CustomerListView
, and MessageView
are composed to ApplicationView
, template defined in main.htm.
<h3>{{view.Title}}</h3>
<div>
<div id="message">
{{view App.MessageView}}
</div>
<div id="createEditCustomer">
{{view App.CreateEditCustomerView}}
</div>
<div id="customerList">
{{view App.CustomerListView}}
</div>
</div>
<div id="footer">
</div>
App.ApplicationView = Ember.View.extend({
Title: "Example of Ember.js application",
template: getView('main'),
init: function () {
this._super();
this.set('controller', App.applicationController);
},
name: "ApplicationView"
});
The Ember Route
We already created application, controllers, views, models but there is one more thing to make our application work, a route. Route will connect application controller to application view, initialize application controller and in sequential order, application view is bound/rendered to screen. In this example, I use default IndexRoute
with path /. Now user can see, touch views and get right response from controllers.
App.IndexRoute = Ember.Route.extend({
model: function () {
return App.applicationController.initialize();
}
});
Embedding SignalR
With Visual Studio, we can add SignalR from nuget.
Install-Package Microsoft.AspNet.SignalR -pre
RegisterHubs.cs was automatically added in the folder App_Start.
using System.Web;
using System.Web.Routing;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hosting.AspNet;
[assembly: PreApplicationStartMethod(typeof(Ember.n.SignalR.RegisterHubs), "Start")]
namespace Ember.n.SignalR
{
public static class RegisterHubs
{
public static void Start()
{
RouteTable.Routes.MapHubs();
}
}
}
We use Hub
instead of PersistentConnection
to easily create a communication from server with all clients.
namespace Ember.n.SignalR.Hubs
{
using Ember.n.SignalR.DTOs;
using Microsoft.AspNet.SignalR.Hubs;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Microsoft.AspNet.SignalR;
public class CustomerHub : Hub
{
public static IHubContext Instance
{
get{
return GlobalHost.ConnectionManager.GetHubContext<customerhub>();
}
}
JsonSerializerSettings _settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
};
public void Add(Customer customer)
{
Clients.All.add(JsonConvert.SerializeObject(customer, _settings));
}
public void Update(Customer customer)
{
Clients.All.update(JsonConvert.SerializeObject(customer, _settings));
}
public void Remove(Customer customer)
{
Clients.All.remove(JsonConvert.SerializeObject(customer, _settings));
}
}
}
In order for clients to communicate with server through SignalR pipeline, we need to include the hub script and implement customerHub
in client side.
<script src="/signalr/hubs " type="text/javascript"></script>
(function (App) {
var hub = $.connection.customerHub;
function findCustomer(id) {
var c = App.applicationController.get('customers').findProperty('id', id);
return c;
}
hub.client.add = function (message) {
var customer = JSON.parse(message);
var c = findCustomer(customer.id);
!c && App.applicationController.get('customers').pushObject(App.CustomerModel.create(customer));
}
hub.client.update = function (message) {
var customer = JSON.parse(message);
var c = findCustomer(customer.id);
c && c.set('quiet', true) && c.setProperties(customer) && c.set('quiet', false);
}
hub.client.remove = function (message) {
var customer = JSON.parse(message);
var c = findCustomer(customer.id);
if (c) {
if (c.id === App.applicationController.get('currentCustomer').id) {
App.applicationController.set('currentCustomer', null);
App.applicationController.random();
}
App.applicationController.get('customers').removeObject(c);
}
}
$.connection.hub.start();
App.hub = hub;
})(window.App);
We simply manipulate models via applicationController
, Ember.js binding will take care of the rest and ensure UI reflects the modified models right.
The Ember Observers
In the previous section, after we inject SignalR, each client already gets real-time updating what's changed on the server. For more interesting, we use Ember observable to observe property changed when user typing and notify server via customerHub
, after that server will broadcast the change to all clients. Let's add the above lines of code to CustomerModel
propertyChanged: function () {
try {
if (!this.get('quiet') && this.get('id')) {
App.hub.server.update(this.plain());
}
}
catch (e) {
}
} .observes('firstName', 'lastName', 'email', 'phone', 'active'),
We just notify server when firstName
, lastName
, email
, phone
or active
property of existing customer changed. The quiet
checking prevents loop notifying from client to server and server to client, when an object is getting changed from server, it will not notify server again that it is changing.
Enjoy Our Work
Now let's build and run the web application, random a new customer and click submit. As you see on the screen, the UI changing works well without any DOM manipulation. Click edit link to edit and modify anything on first name, last name...text box, the edited row below reflects what we have just typed immediately.
To see how Ember.js interacts with SignalR, do copy the URL on the browser and paste to another browser window, arrange both windows near together, then create / edit / delete customer on the one, the other also got changed customer like a mirror. That is really cool, right.
Conclusion
Controllers, views, models and routes in Ember.js work in a very similar way with ASP.NET MVC, so those who already worked with ASP.NET MVC can easily understand and get benefit from Ember.js. This article is just an introduction about Ember.js, to make a complex and real-time web application, we must dig into Ember objects more and more. SignalR in this example is a bridge, it makes sure models in client side are synchronized with server.
References