Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Backbone.js App with WCF OData Service

DZone's Guide to

Backbone.js App with WCF OData Service

· DevOps Zone
Free Resource

The DevOps Zone is brought to you in partnership with Sonatype Nexus. The Nexus Suite helps scale your DevOps delivery with continuous component intelligence integrated into development tools, including Eclipse, IntelliJ, Jenkins, Bamboo, SonarQube and more. Schedule a demo today

Today's frontend part of web application becomes very complex. Customers expect functional high-performance apps with beautiful UI that working in the browser just like general desktop app. To fulfill these requirements we come to Single-page Applications (SPA). SPA makes us write a lot of client-side code (usually javascript). Lots of client scripts in length of time often turns into uncontrolled mess.

The solution is to structure the app using client-side frameworks. Backbone.js is one of such pills. It’s open source javascript MVC framework. Developer should describe models and views, setup events bindings, define routing and provide data service (REST-like JSON or XML service) for his app.

In this tutorial I want to show how to use Backbone.js and WCF ODATA service to create an SPA. I'm going to use Microsoft sample database Northwind. The app contains three pages: list of categories, products list from chosen category and product details with ability to change it.

The source code of the app is available on GitHub: https://github.com/tabalinas/BackboneOData

Are you ready? So let's go.

Prepare Application

Begin with creating simple ASP.NET Web Application with one Default.aspx page as a main page of SPA.

Open Visual Studio and create ASP.NET Empty Web Application


Project has following structure


  • Scripts - javascript code
  • Service - contains .edmx mapping and .svc service files
  • Styles - css styles
  • Default.aspx - main page of SPA

WCF ODATA Service

For demo purposes we could use one of public ODATA services e.g. http://services.odata.org/Northwind/Northwind.svc. But in this case same origin policy would cause some problems with data manipulation. So let’s create our own WCF ODATA Service.

Firstly create edmx file


Choose “Generate from database”, setup connection to your MS SQL Server and select Northwind as a source database. Then choose DB objects that we want to map. For our app I chose Category, Product and Supplier.


Click “Finish”. Our .edmx file is ready


Now create WCF Data service based on created mapping. Give it name "Northwind.svc"


Change the code of service as follows

public class Northwind : DataService<NORTHWNDEntities>
{
    // This method is called only once to initialize service-wide policies.
    public static void InitializeService(DataServiceConfiguration config)
    {
        // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
        // Examples:
        config.SetEntitySetAccessRule("*", EntitySetRights.All);
        config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
    }
}

Put mapping class to the DataService generic type (in our case NORTHWINDEntities). 

Grant the access to entities. For the demo I gave full access to all entities.

Service is ready to provide data.

Backbone Components

As I already mentioned Backbone is MVC framework. Backbone app has following components:

  • Model describes entities and business logic of the app.
  • Collection is a batch of entities. It’s like the other kind of Model presenting the collection of particular models.
  • View is a visual representation of particular model. Here we are free to use any template engine. Underscore templates are the most common though.
  • Router is like routing in ASP.NET MVC just on the client-side. It describes navigation of the app, the routes and related handling methods.

Less words more action. Let’s create page with list of available product categories from Northwind and show how to make all this work together.

List of Categories

Category Model

app.Models.Category = Backbone.Model.extend({

    url: function() {
        return "Service/Northwind.svc/Categories(" + this.get("CategoryID") + ")";
    },

    idAttribute: "CategoryID",

    defaults: {
        CategoryID: 0,
        CategoryName: "",
        Description: ""
    }

});

All models extend Backbone.Model. app here is our root namespace of the application. You can name it whatever you want. app.Models is namespace for all models.

Model properties:

  • url returns the url where entity can be reached on the server. In our case it’s  “Service/Northwind.svc/Categories({CategoryID})”.
  • idAttribute tells which field contains the id of the object.
  • defaults is object containing model fields with default values.

Of course we should put all business logic methods related to category here in model definition.

Category Collection

app.Models.CategoryList = Backbone.Collection.extend({

    url: "Service/Northwind.svc/Categories",
        
    model: app.Models.Category,
        
    load: function(callback) {
        this.fetch({
            success: callback
        });
    }

});

Collections should extend Backbone.Collection.

Collection properties:

  • url is an URL of entity collection on the server.
  • model is reference to related model class. The property determines items of which type collection contains.
  • load is custom method wrapping collection method fetch with success callback. This method encapsulates collection fetching, so we can easily modify it in the future (e.g. add filtering/sorting or whatever).

Category View

app.Views.CategoryListView = Backbone.View.extend({

    template: _.template($("#categoryListViewTemplate").html()),

    className: "category-list-view",

    render: function() {
        this.$el.html(this.template(this.model.toJSON()));
        return this;
    }

});

Each view extends Backbone.View.

View properties:

  • template is custom field (doesn’t belong to Backbone.View) referencing to the Underscore template of the view.
  • className is a name of css class adding to the root tag container of the view. The div is used by default, but we can override it specifying tagName property.
  • render is main method of the view performing actual rendering. In our case we put the result of template rendering to the root view container.

Default.aspx and Underscore View Template

Default.aspx should have following markup

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="BackboneApp.Default" %>

<!DOCTYPE html>

<html>
<head runat="server">
    <title>Backbone.js with OData Demo Application</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="Styles/bootstrap.css" rel="stylesheet" />
    <link href="Styles/style.css" rel="stylesheet" />
</head>
<body>
    <!-- app page -->
    <form id="form1" runat="server">
        <header class="container">
            <h3 id="title"></h3>
        </header>
        <div id="content" class="container">
        </div>
    </form>

    <!-- templates -->
    <script id="categoryListViewTemplate" type="text/template">
        <div class="well">
            <ul class="list">
            {% _.each(this.model.models, function(category) { %}
                <li>
                    <h4><a href="#category/{{ category.get("CategoryID") }}">{{ category.get("CategoryName") }}</a></h4>
                    <p>{{ category.get("Description") }}</p>
                </li>
            {% }); %}
            </ul>
        </div>
    </script>

    <!-- scripts -->
    <script src="Scripts/jquery-1.7.1.js"></script>
    <script src="Scripts/underscore.js"></script>
    <script src="Scripts/backbone.js"></script>
    
    <script src="Scripts/App.js"></script>

    <script src="Scripts/Models/Category.js"></script>
    <script src="Scripts/Models/CategoryList.js"></script>
    
    <script src="Scripts/Views/CategoryListView.js"></script>
</body>
</html>

The body of the Default.aspx is divided into three sections. The first is markup of the app page. The second section is templates definition. The third has links to required scripts.

App page markup is the form containing two child html elements: header with title and main div for content markup.

The template for the category list is ul with li for each category. The model for the template is the collection, so we have to loop over the model.models array to render each category. The category name is a link to list of product in the category. I used Bootstrap to make demo looking pretty, so there are some specific css classes like well.

All templates located on the Default.aspx page. To improve the app design, templates could be stored in separate files and loaded asynchronously on demand.

It’s impossible to use Underscore template with default settings on aspx page because it contains conflicting with asp syntax control tags <% %>. To solve the problem we need to execute the following code before app start.

// underscore template settings to prevent conflict with ASP tags <% %>
_.templateSettings = {
    interpolate: /\{\{(.+?)\}\}/g,      // print value: {{ value_name }}
    evaluate: /\{%([\s\S]+?)%\}/g,      // execute code: {% code_to_execute %}
    escape: /\{%-([\s\S]+?)%\}/g        // excape HTML: {%- <script> %} prints <script>s
};

In scripts section there are links to required scripts. Backbone requires jQuery (or Zepto) and Underscore (or Lo-Dash). App.js is main script of the app. Also there are scripts of our models and views.

App.js and Routing

Root application namespace and routing are defined in App.js. The method app.renderView renders Backbone view and puts the result to the page content div.

// root namespace for application
window.app = {
    Models: {},

    Views: {},

    renderView: function(view, title) {
        $("#content").html(view.render().el);
        $("#title").text(title);
    }
};

// app router
app.ApplicationRouter = Backbone.Router.extend({

    routes: {
        "": "categoryList"
    },

    categoryList: function() {
        var categoryList = new app.Models.CategoryList(),
            categoryListView = new app.Views.CategoryListView({ model: categoryList });

        categoryList.load(function() {
            app.renderView(categoryListView, "Categories List");
        });
    }

});

App router extends Backbone.Router. Object routes is a map between url patterns and methods handling requests to these urls. In our case we say that empty url refers to categoryList method. Method categoryList creates the model and accordant view. Then it fetches collection from the server and renders the view.

At the end we should create instance of router and start the app. It should happen on document ready event.

// start app
$(function() {
    app.router = new app.ApplicationRouter();

    Backbone.history.start();
});

OData Related Tuning

Oops! We start the app and see nothing. Because our request to OData service is failed. We need to do some tuning to make it work.

Backbone uses jQuery.ajax function to send ajax request to the server. Add special ajax prefilter.

// prepare request according to ODATA
$.ajaxPrefilter(function(options, originalOptions, xhr) {
    // set required HTTP headers
    options.contentType = "application/json; odata=verbose";
    options.headers = $.extend(options.headers || {}, {
        Accept: "application/json; odata=verbose"
    });

    // add default error handler
    var originalError = originalOptions.error;
    options.error = function(xhr, textStatus, errorThrown) {
        alert("Error during processing the request!");
        if(originalError) {
            originalError.apply(this, arguments);
        }
    };
        
    // patch success handler to retrieve data from ODATA json response
    var originalSuccess = options.success;
    if(originalSuccess && originalOptions.type === "GET") {
        options.success = function(data) {
            originalSuccess(data["d"]);
        };
    }
});

Prefilter adds contentType and accept header to request, wraps error handler and patches success handler to get the json result from WCF OData service response (field d of response json).

Now we can start the app and see the following result


Products List

Now let’s create second view with list of products from selected category.

Product Model

app.Models.Product = Backbone.Model.extend({

    url: function() {
        return "Service/Northwind.svc/Products(" + this.get("ProductID") + ")";
    },

    idAttribute: "ProductID",

    initialize: function(attributes) {
        this.set({ UnitPrice: parseFloat(attributes.UnitPrice) });
    },

    defaults: {
        ProductID: 0,
        ProductName: "",
        UnitPrice: 0.0
    }

});

The method initialize is a constructor of the model instance. The input parameter is an object with attributes. In the constructor we parse the value of the product unit price that comes from the server as a string.

Product Collection

app.Models.ProductList = Backbone.Collection.extend({

    url: "Service/Northwind.svc/Products",

    model: app.Models.Product,
        
    loadByCategory: function(categoryId, callback) {
        this.fetch({
            data: { "$filter" : "CategoryID eq " + categoryId },
            success: callback
        });
    }

});

The list of products is fetched by category. The method loadByCategory adds new $filter parameter to the request so we get products from the particular category.

Product List View

app.Views.ProductListView = Backbone.View.extend({

    template: _.template($("#productListViewTemplate").html()),

    className: "product-list-view",
        
    render: function() {
        this.$el.html(this.template(this.model.toJSON()));
        return this;
    },
        
    events: {
        "click #backToCategories": "backToCategories"
    },

    backToCategories: function() {
        app.router.navigate("/", { trigger: true });
    }
});

This view contains button that brings us back to the category list page. To add such a logic to the view I use events object, that maps events to handling methods. The event has name (click) and selector where to attach handler (#backToCategories is button with id backToCategories). Our event handler cause router to navigate to the root url (the page with categories).

Product List View Template

<script id="productListViewTemplate" type="text/template">
    <p><input id="backToCategories" type="button" value="< Back to Categories" class="btn" /></p>
    <div class="well">
        <ul class="list">
        {% _.each(this.model.models, function(product) { %}
            <li>
                <h4><a href="#product/{{ product.get("ProductID") }}">{{ product.get("ProductName") }}</a></h4>
                <p>Price: ${{ product.get("UnitPrice").toFixed(2) }}</p>
            </li>
        {% }); %}
        </ul>
    </div>
</script>

Here we loop over the list of products and render them with formated unit price.

Product List Routing

routes: {
    "": "categoryList",
    "category/:id": "productList"
},
...
productList: function(id) {
    var productList = new app.Models.ProductList(),
        productListView = new app.Views.ProductListView({ model: productList });

    productList.loadByCategory(id, function() {
        app.renderView(productListView, "Products List");
    });
},

The route for product list is added. There is a parameter :id in the url pattern. The value of parameter will be passed to the handling function.

If we start the app and open a category we’ll see a list of products.


Product Details

The last page is product details where user can change the name and price of a product.

Product Model Modifying

app.Models.Product = Backbone.Model.extend({

    url: function() {
        return "Service/Northwind.svc/Products(" + this.get("ProductID") + ")";
    },

    idAttribute: "ProductID",

    initialize: function(attributes) {
        this.set({ UnitPrice: parseFloat(attributes.UnitPrice) });
    },

    defaults: {
        ProductID: 0,
        ProductName: "",
        UnitPrice: 0.0
    },
        
    load: function(callback) {
        this.fetch({
            success: callback
        });
    }

});

Added new method load to encapsulate product fetching.

Product Details View

app.Views.ProductDetailsView = Backbone.View.extend({

    template: _.template($("#productDetailsViewTemplate").html()),

    className: "product-details-view",
                
    render: function() {
        this.$el.html(this.template(this.model.toJSON()));
        return this;
    },

    events: {
        "click #backToProducts": "backToProducts",
        "click #saveProduct": "saveProduct",
    },

    backToProducts: function() {
        window.history.back();
    },

    saveProduct: function() {
        var message = this.$el.find("#message").empty();

        this.model.save({
            ProductName: $('#ProductName').val(),
            UnitPrice: $('#UnitPrice').val(),
        }, {
            success: function() {
                message.append($("<div />").addClass("alert alert-success").text("Product info successfully saved!"));
            },
            error: function() {
                message.append($("<div />").addClass("alert alert-error").text("Error occurred while saving product info!"));
            }
        });
    }

});

The most interesting here is method saveProduct. It takes data from the inputs and calls save method of the model. Also it shows the status message of operation result.

Product Details View Template

Product details view template is a from with product fields.

<script id="productDetailsViewTemplate" type="text/template">
    <p><input id="backToProducts" type="button" value="< Back to Products" class="btn" /></p>
    <div class="well">
        <fieldset>
            <label>Product ID:</label>
            <input type="text" id="ProductID" disabled value="{{ ProductID }}">

            <label>Product Name:</label>
            <input type="text" id="ProductName" value="{{ ProductName }}">

            <label>Unit Price:</label>
            <input type="text" id="UnitPrice" value="{{ UnitPrice }}">
        </fieldset>
        <div id="message"></div>
        <input id="saveProduct" type="button" value="Save" class="btn-primary btn" />
    </div>
</script>

Product Details Routing

The route to product details is similar to product list route.

routes: {
    "": "categoryList",
    "category/:id": "productList",
    "product/:id": "productDetails"
},
...
productDetails: function(id) {
    var product = new app.Models.Product({ ProductID: id }),
        productDetailsView = new app.Views.ProductDetailsView({ model: product });

    product.load(function() {
        app.renderView(productDetailsView, "Product \"" + product.get("ProductName") + "\"");
    });
}

Now if we open the product from the list, edit name and click “Save”, we’ll get the following result


The demo app is pretty simple but it demonstrates how we can build architecture of client side code with Backbone.js. Also we saw how to create WCF OData Service and make it work with Backbone.

The DevOps Zone is brought to you in partnership with Sonatype Nexus. Use the Nexus Suite to automate your software supply chain and ensure you're using the highest quality open source components at every step of the development lifecycle. Get Nexus today

Topics:

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}