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

A Sample Real-time Web Application using Ember.js, REST API, and SignalR

4.82/5 (36 votes)
9 Jul 2013MIT6 min read 229.1K   3.9K  
A sample real-time web application using Ember.js, REST API, and SignalR.

Sample Image - maximum width is 600 pixels

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.

Image 2

This is Ember.js's MVC implementation, client side part.

Image 3

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.

XML
<!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:

XML
@{
    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
C#
[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
C#
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.

C#
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:

JavaScript
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/.

JavaScript
// Data store
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) { // error
            message = { errorCode: xhr.status, errorMessage: xhr.statusText };
        }

        return message;
    },
    read: function (id) // !id read all
    {
        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) { // error
            message = { errorCode: xhr.status, errorMessage: xhr.statusText };
        }

        return message;
    },
    remove: function (id) // !id delete all
    {
        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) { // error
            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) { // error
            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.

JavaScript
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

JavaScript
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)) { // Read all
                var array = Ember.ArrayController.create({ content: [] });
                this.currentResult.data.forEach(function (item, index) {
                    array.pushObject(App.CustomerModel.create(item));
                });
                return array;
            }
            else { // An object
                var customer = this.get('customers').findProperty('id', this.currentResult.data.id)
                customer && customer.setProperties(this.currentResult.data);
                return customer;
            }
        }
        else { // Empty result
            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) { // create
            this.create(customer);
        }
        else { // edit
            this.update(customer);
        }
    },
    edit: function (id) {
        if (this.get('currentCustomer').id != id) { // Rollback
            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") // Keep copy
            }));
    },
    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.

XML
<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:

JavaScript
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:

XML
<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:

JavaScript
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.

JavaScript
{{#unless controller.currentResult.errorCode}}
<div id='message'>
    {{controller.currentResult.errorMessage}} &nbsp;
</div>
{{else}}
<div id='error'>
    {{controller.currentResult.errorMessage}} &nbsp;
</div>
{{/unless}}

CreateEditCustomerView, CustomerListView, and MessageView are composed to ApplicationView, template defined in main.htm.

XML
<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.

JavaScript
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.

C#
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()
        {
            // Register the default hubs route: ~/signalr/hubs
            RouteTable.Routes.MapHubs();
        }
    }
}

We use Hub instead of PersistentConnection to easily create a communication from server with all clients.

C#
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>

JavaScript
/// <reference path="_references.js" />

(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

JavaScript
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.

Image 4

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

License

This article, along with any associated source code and files, is licensed under The MIT License