Click here to Skip to main content
14,580,263 members

Web Templating Using ASP.NET Web API and Handlebars

Rate this:
4.96 (13 votes)
Please Sign up or sign in to vote.
4.96 (13 votes)
8 Jun 2020CPOL
Creating applications using ASP.NET web API and Handlebars templating engine
This article will show you how to integrate three different yet vital parts of a web application, namely, Client Layer, Server Layer and Service Layer.

Introduction

Web application development trends are changing for the good every now and then. The kinds of application architectures that we used to build some years ago have now started or already become somewhat obsolete and less efficient. The article that you are about to read applies to the same changing trends.

This article sheds light on integrating three different yet vital parts of a web application; namely Client Layer, Server Layer and Service Layer. It may be wrong to call Client and Server areas of code as layers but for the sake of uniformity in naming convention, let's think of them as specific layers of a multi-layered system in which the server sits on top, then service and then the client (You may even reverse the order if you wish and that would be perfectly alright).

Intended Audience

I am going to assume that you are already familiar with using Web API and maybe Handlebars. It would be great if you have worked on a large application which could amount to enterprise scale as then it would be easier for you to relate the separation of concerns that I have tried to implement in the sample code.

What to Expect

This article in no way tries to bring out some new form of web application development pattern. Web templating has been around for a very long time, only the way we handle the content generation from the templates has been changed. We used to have server side variables and embedded code put in the HTML markup in different places to achieve data binding with the templates. Even today, so many of us would prefer and rely heavily on dynamic code generation technologies like Razor View in ASP.NET MVC which is understandable as it perfectly addresses content generation and view model binding. But if you are a simple minded developer like me who is not very comfortable in mixing up things so much, then you would understand how easy it is to maintain an application in which we properly separate everything. And besides, I always feel that it's a bad idea to put too much domain logic inside the template code.

This article aims to provide a web templating code implementation using JavaScript and Web API, so that the developers who are looking to find a basic understanding of how things move in such an application can take a reference from here.

You can easily find other such code samples on the internet if you are willing to do a quick Google search but it is always better to have references of multiple ways of doing the same thing as each one of us has different requirements.

Sample Code Overview

The code in this article is for a sample shopping cart layout. In this application, we basically have to update the information inside a predefined web page design. For that purpose, it is best to use HTML templates as we only need to combine the input data with the template and display the compiled HTML result.

Image 1

Overview

I have tried to keep things simple and my focus is mostly on Web API service calling and code generation from the compiled templates along with properly separating everything. You will not find any code for the database CRUD operations, for that purpose, I have created a very simple data mapper to use In-Memory objects. You are by all means free to code the data mapper to support additional providers as you like in the attached sample code.

The beauty of this implementation is that a specific type of data mapper is being injected into the Web API controller so that we can easily switch between development data and testing data. This approach makes it very easy for us to integrate our UI with a separate testing module. All we need to do is to use a different Web.config file to use a different data mapper. The mapper can then return data from any source like In Memory Objects, XML Stream or a test database.

This article is going to be somewhat long so I would request you to bear with me and you will find how you can properly structure your web application framework and you will have to write less code to add new features while abstracting out common stuff.

Let's first get to know about Handlebars templating engine in brief and also an introduction about what templates are all about. I am going to assume that you are already familiar with ASP.NET applications and Web API and also some basic understanding of templating. If you are not, then I would suggest to first get yourself acquainted with them before you further read this article.

Handlebars and Templating

Templates have been in use as an architectural practice for a very long time. Templates simply denote an output blueprint or wireframe in which there are tokens which we replace with actual values contained inside input data. The template is then merged with the data to generate the desired output.

For Handlebars, I am just going to use the definition given on Wikipedia as it is already very clear and I think there is no need for me to re-invent another one.

Quote:

Handlebars is a semantic web template system, started by Yehuda Katz in 2010. Handlebars.js is a superset of Mustache, and can render Mustache templates in addition to Handlebars templates. While Mustache is a logicless templating language, Handlebars adds extensibility and minimal logic, such as #if, #unless, #with, and #each helpers.

Using the Code

Starting Out

Before we start understanding the client code, we should first know how things are moving in the server code. The sample code has a separate Data layer which contains classes to get the data from whatever data source you decide to use. I have created an interface IMapper and have put it inside another layer which is shared across everywhere so that we can manage dependency injections across different parts of the application.

Step 1

We will start by creating an empty ASP.NET Web Forms Application in Visual Studio 2012 (You can download one from here). This empty application does not have any pre-generated code related to navigation or authentication and that is exactly what we need for this sample application.

Image 2

Here is a snapshot of the project structure. I am going to explain all of the modules in it one by one:

Image 3

Add a new .aspx file and name it Default.aspx. Also, add a master page and name it Master.master. As of now, we do not need to add any code to the master page, we will later add references to the scripts and stylesheets that we will need.

Replace the code inside Default.aspx file that we just added with the following:

%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" 
   Inherits="TestCart.Default" MasterPageFile="~/Master.Master" %>

<asp:Content ID="index" runat="server" ContentPlaceHolderID="MainPlaceHolder">
    <div id="divGrid">
    </div>
    <script>      
    </script>
</asp:Content>

The default page will contain the grid of all the available products.

Add another aspx page and name it Product.aspx. We will only have two aspx pages in this sample application and they will be enough to convey the understanding of doing templating in ASP.NET using Handlebars. Add the following code to the Product.aspx page.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Product.aspx.cs" 

    Inherits="TestCart.Product"  MasterPageFile="~/Master.Master" %>

<asp:Content ID="product" runat="server" ContentPlaceHolderID="MainPlaceHolder">
    <div id="divProduct">
    </div>
    <script>        
    </script>
</asp:Content>

Before we dive further into the client side, let's first get to know about the server code structure. The client will simply call the web Api to get the data and will then combine the data with the templates. Now we can follow one of the two paths:

  1. First one is to write our domain logic directly into the Web Api class, initialize the data access module in Global.asax file and tie it to the session. Web Api class will use that data access module in the session to get the data. This approach has a lot of drawbacks like there should not be any business logic inside the service layer (or it should be bare minimum) and we are also tightly coupling the services with the data access module making it extremely difficult to integrate a test module or some other production data provider.
  2. Second one is when we are injecting the appropriate data access module dependency into the Web Api and not putting any domain logic code inside it.

Now obviously you guys have figured out which approach is better so let's start to implement the second one.

Add a new class library project and name it Data. Add a class in the Data project and also name it Data. (See the Solution Explorer image above).

In the Data class, we will first take care of our in memory product repository. The repository will be used to store static product objects in the memory. For that purpose, add the following code into Data.cs file:

#region Static Data

/// <summary>
/// Product repository class that contains the static product information
/// </summary>
public class ProductRepository
{
    private List<Product> _products;

    /// <summary>
    /// Get all products
    /// </summary>
    public List<Product> Products { get { return _products; } }

    public ProductRepository()
    {
        Product product;
        _products = new List<Product>();

        product = new Product(1, "iPhone 6", 38999, "iphone.jpg",
            "Apple iPhone 6 exudes elegance and excellence at its best. With iOS 8,
            the world’s most advanced mobile operating system, and a larger and
            superior 4.7 inches high definition Retina display screen,
            this device surpasses any expectations you might have with a mobile phone. ",
            "This new generation device comes with improved Wi-Fi speed to enhance
            your surfing experience. Its sleek body and seamless design along with
            a perfect unison between its software and hardware system gives you
            marvellous navigation results. Just double tap the home screen
            and the entire screen shifts down to your thumb for easy operation.
            In terms of security, this iPhone is a class apart from its predecessors
            as it allows you finger print lock. The multitude of features in
            Apple iPhone 6 makes it powerful, efficient, smooth and super easy to use.
            With this phone in your hands, you can manage your world with
            just a single touch!",
            true);
        product.Reviews = new List<Review>();
        product.Attributes = new List<ProductAttribute>();

        product.Reviews.Add(new Review("John", "Very good phone!!", 4));
        product.Attributes.Add(new ProductAttribute("Brand", "Apple"));

        _products.Add(product);
    }

    /// <summary>
    /// Get product by id
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public Product GetById(Int32 id)
    {
        return _products.Find(p => p.Id == id);
    }
}

#endregion Static Data

The above is a very ugly way to add in-memory objects but that will do for this sample application. In the ProductRepository class constructor, we are adding product information to the list Products. The attached code has a lot more products being added to the list. The view model classes for Product, Reviews and Attributes are contained in a Shared module.

The product list can be accessed directly from the read-only Products property. There is a method GetById which will search a product in the product list using the integer type Id and will return the product object if found.

Now we need to add abstract and concrete mapper classes to get the data. Before we do that, we need to add another logical layer in our solution for the shared data. Add a new class library project and name it Shared. Add three classes in the Shared layer and name them Models, SharedInterfaces and Types.

Let's add the code for our view model classes as follows in the Models class:

/// <summary>
/// Product class
/// </summary>
public class Product
{
    public Int32 Id { get; set; }
    public String Name { get; set; }
    public Decimal Price { get; set; }
    public String Image { get; set; }
    public String SmallDescription { get; set; }
    public String LargeDescription { get; set; }
    public List<Review> Reviews { get; set; }
    public List<ProductAttribute> Attributes { get; set; }
    public Boolean InStock { get; set; }

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="name"></param>
    /// <param name="image"></param>
    /// <param name="smallDescription"></param>
    /// <param name="largeDescription"></param>
    /// <param name="inStock"></param>
    public Product(Int32 id, String name, Decimal price, String image,
                   String smallDescription,
                   String largeDescription, Boolean inStock)
    {
        Id = id;
        Name = name;
        Price = price;
        Image = image;
        SmallDescription = smallDescription;
        LargeDescription = largeDescription;
        InStock = inStock;
    }
}

/// <summary>
/// Product review class
/// </summary>
public class Review
{
    public String ReviewedBy { get; set; }
    public String ReviewText { get; set; }
    public Int32 Rating { get; set; }

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="reviewedBy"></param>
    /// <param name="reviewText"></param>
    /// <param name="rating"></param>
    public Review(String reviewedBy, String reviewText, Int32 rating)
    {
        ReviewedBy = reviewedBy;
        ReviewText = reviewText;
        Rating = rating;
    }
}

/// <summary>
/// Product attribute class
/// </summary>
public class ProductAttribute
{
    public String Name { get; set; }
    public String Value { get; set; }

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="name"></param>
    /// <param name="value"></param>
    public ProductAttribute(String name, String value)
    {
        Name = name;
        Value = value;
    }
}

The above code is self explanatory, we are just adding simple classes with public properties to hold the product data. These view models will be returned to the JavaScript code and then will be merged with our templates.

We are going to need interface contracts to be shared across the application to perform dependency injections. So it makes sense to put them in a common place. Add the following code to the SharedInterfaces class:

/// <summary>
/// Mapper interface
/// </summary>
public interface IMapper
{
    Object Get(DataType type);
    void SetId(Int32 id);
}

/// <summary>
/// Product Mapper interface
/// </summary>
public interface IProductMapper
{
    List<Product> GetAllProducts();
    Product GetProductById(Int32 id);
}

IMapper is the interface for all the data mappers. Get method will return the data based on the type. SetId method is to set the id of record that we need. DataType is an enum which will direct the Get method to fetch the specific information that we need.

There is another interface IProductMapper which is specific to the products. The IMapper will be injected into the IProductMapper constructor. IProductMapper method implementations will then call IMapper methods internally to perform crud operations. GetAllProducts will get all the available products and GetProductById will return a single product based on the id provided. When we use a database, then the concrete product mapper method implementations will need to be wrapped inside transactional scopes.

Only thing left in Shared is to add the DataType enum implementation:

/// <summary>
/// Data type enum
/// </summary>
public enum DataType
{
    AllProducts,
    IndividualProduct
}

As of now, we only need two types; AllProducts and IndividualProducts. The current mapper implementation needs to handle all the types to return the type of data being requested. You are free to add more as per your convenience.

With the shared code in place, now we can move to implement the concrete mapper classes. We will be having three mapper classes: AbstractMapper, InMemoryMapper and ProductMapper.

AbstractMapper will contain the code common to all the different concrete mappers. Add the following code to Data.cs class implement this:

#region Mappers

/// <summary>
/// Abstract data mapper.
/// </summary>
public abstract class AbstractMapper : IMapper
{
    protected Int32 _id;
    public abstract Object Get(DataType type);
    public void SetId(Int32 id)
    {
        _id = id;
    }
}

In the above code, we have declared the Get method as abstract so that the concrete classes inheriting the abstract mapper can do the implementation. The SetId method is just setting the value of the protected _id variable.

InMemoryMapper will do the job of getting the data from the in-memory data that we will create by instantiating the ProductRepository class. The following code is for this mapper class:

/// <summary>
/// Product mapper to get data from the objects in the memory.
/// </summary>
public class InMemoryMapper : AbstractMapper
{
    /// <summary>
    /// Get the data
    /// </summary>
    /// <returns></returns>
    public override Object Get(DataType type)
    {
        Object retVal = null;

        switch (type)
        {
            case DataType.AllProducts:
                retVal = new ProductRepository().Products;
                break;

            case DataType.IndividualProduct:
                retVal = new ProductRepository().GetById(_id);
                break;
        }

        return retVal;
    }
}

Above is the implementation of the Get method. Based on the type of data being requested, the code initializes a new instance of the ProductRepository and then gets the data. One thing to note is that you can set the in-memory data as static to avoid creating new objects every time; this is an exercise I leave for you guys to work on.

ProductMapper is the specific mapper for the products. It will use the IMapper interface to execute different code instructions to get the data that the users need. Add the following code to the Data.cs file:

/// <summary>
/// Concrete mapper class
/// </summary>
public class ProductMapper : IProductMapper
{
    private IMapper _mapper = null;

    public IMapper Mapper
    {
        get { return _mapper; }
        set { _mapper = value; }
    }

    public ProductMapper(IMapper mapper)
    {
        _mapper = mapper;
    }

    public List<Product> GetAllProducts()
    {
        return _mapper.Get(DataType.AllProducts) as List<Product>;
    }

    public Product GetProductById(Int32 id)
    {
        _mapper.SetId(id);
        return _mapper.Get(DataType.IndividualProduct) as Product;
    }
}

The above code is straightforward if you are familiar with how the simplest dependency injections work. If you are not, then I would strongly suggest to read about it.

What we are doing is just injecting a specific implementation of the IMapper interface into the ProductMapper constructor. The ProductMapper only needs to know about the members of the IMapper and not the type of implementation. We will then inject this implementation of ProductMapper in our Web Api controller.

We have created all the required classes to get the product data, now is the time to move on to the Web Api. Add a new Web Api controller and name it ProductController. It will come with empty GET/PUT/POST/DELETE methods. We will only need the GET methods to get the data from the server. Add the following code into the ProductController for a constructor function which accepts a mapper of type IProductMapper as an argument. We also need to add a private field to hold the mapper reference and modified signatures for the GET methods:

public class ProductController : ApiController
{
    private readonly IProductMapper _mapper;

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="mapper">Mapper dependency</param>
    public ProductController(IProductMapper mapper)
    {
        _mapper = mapper;
    }

    // GET api/<controller>
    public List<Shared.Product> Get()
    {
        return _mapper.GetAllProducts();
    }

    // GET api/<controller>/5
    public Shared.Product Get(int id)
    {
        return _mapper.GetProductById(id);
    }

    // POST api/<controller>
    public void Post([FromBody]string value)
    {
    }

    // PUT api/<controller>/5
    public void Put(int id, [FromBody]string value)
    {
    }

    // DELETE api/<controller>/5
    public void Delete(int id)
    {
    }
}

The Api controller class will use the private _mapper to call the IMapper type implementation of the concrete mapper class. The methods don't need to worry about what type of mapper is being used and so the UI is completely unaware and un-plugged from the data provider. This makes it very easy to integrate custom test suits with our application.

Now the question arises that how are we going to actually inject the mapper in the web Api controller class. We are going to do this by creating an implementation of the IDependencyResolver interface. IDependencyResolver is used to resolve the Web Api service dependency and we can return the service based on the requested service type. This is illustrated in the following class. Add a new folder App_Code and a class MyDependencyResolver and add this code into the class file:

public class MyDependencyResolver : IDependencyResolver
{
    private static readonly IProductMapper Mapper =
            new ProductMapper(DataHelper.CreateMapper(WebConfigurationManager.AppSettings
            ["MapperType"].ToString()));

    public Object GetService(Type serviceType)
    {
        return serviceType == typeof(ProductController) ?
                              new ProductController(Mapper) : null;
    }

    public IEnumerable<Object> GetServices(Type serviceType)
    {
        return new List<Object>();
    }

    public IDependencyScope BeginScope()
    {
        return this;
    }

    public void Dispose()
    {

    }
}

The GetService method implementation will be used to return the appropriate Api Controller based on the type of service requested. When the requested type is ProductController, then we are returning a new instance of ProductController class by injecting the proper mapper dependency based on the mapper type stored in the config file.

The value of mapper type needs to be stored in the web.config file:

<appSettings>
    <add key="MapperType" value="InMemoryMapper" />
</appSettings>

Now we need to wire-up the dependency resolver with the application configuration, this can be done in the Global.asax file as follows:

public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        RouteTable.Routes.MapHttpRoute(
                    name: "DefaultApi",
                    routeTemplate: "api/{controller}/{id}",
                    defaults: new { id = System.Web.Http.RouteParameter.Optional }
                    );
        GlobalConfiguration.Configuration.DependencyResolver = new MyDependencyResolver();
    }
}

Oh and we will also need to map the api controller route template with the route table when we are explicitly adding Web Api controller to a web forms application.

We are still missing a vital piece of code which will be used to create the mapper type instance using the String value stored in the config. Add this code in the Data.cs file:

#region Helper

public static class DataHelper
{
    /// <summary>
    /// Creates the mapper object from the given type.
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static IMapper CreateMapper(String type)
    {
        return Activator.CreateInstance(Type.GetType
                         (String.Format("Data.{0}", type))) as IMapper;
    }
}

#endregion Helper

Above, we are using reflection to create the mapper and type and then type casting it into IMapper before returning the object. This mapper object will be then passed into concrete product mapper class constructor.

This was all about the server code, now we are at liberty to move on to the client code. You must have already added the master and aspx pages and we have to now add the supporting JavaScript code. We should do this by creating an application structure in the JavaScript which should be easily accessible from everywhere. To do this, I am just going to attach an object named App to the window. From our App object, we can do different things like calling the Web Api, using Handlebars templates, Event Binding, etc.

Image 4

I am going to add separate JavaScript files associated with different tasks so as to make future code changes easier. To load the App object into the window, I am just going to use a simple loader function. Similarly, I will use separate loader functions for each module.

Add a new JavaScript file into Content\Scripts folder and name it App.js:

;
(function (w, undefined)
{
    function loadApp()
    {
        var app =
            {
                'Events': new w['AppEventsLoader'](),
                'Service': new w['AppServiceLoader'](),
                'Constants': new w['AppConstantsLoader'](),
                'Templates': new w['AppTemplatesLoader']()
            };

        w['App'] = app;

        //There is no going back now!!
        if (w['LoadApp']) w['LoadApp'] = undefined;
        if (w['AppEventLoader']) w['AppEventLoader'] = undefined;
        if (w['AppServiceLoader']) w['AppServiceLoader'] = undefined;
        if (w['AppConstantsLoader']) w['AppConstantsLoader'] = undefined;
        if (w['AppTemplatesLoader']) w['AppTemplatesLoader'] = undefined;
    }
    
    w['LoadApp'] = loadApp;
})(window);

After the loaders are done doing their jobs, we are going to remove them from the memory as we are not going to use them anymore. At this point, you must be thinking how we are going to call the LoadApp function, you will find this out when we will get to the master page implementation below.

Let's add the different modules of our App object. Add a new JavaScript file and name it Service.js. We are going to use it to call the Web Api using a single point of entry.

;
(function (w, $)
{
    var serviceLoader = function ()
    {
        this.Call = function (type, args)
        {
            var url = w.App.Constants.ServiceMap[type];

            if (!args) return $.getJSON(url);
            else return $.getJSON(url, args);
        };
    };
    
    w['AppServiceLoader'] = serviceLoader;
})(window, jQuery);

In the above code, type is used to determine the Web Api url. If args is not null, then we will send it with the service call. I have stored the metadata information of service type and template url in the code which will get loaded at the time of App initialization. Constants.js is used for this purpose:

;
(function (w)
{
    var constantsLoader = function ()
    {
        this.ServiceMap =
        {
            'GetAllProducts': '/api/Product/',
            'GetProduct': '/api/Product/'
        };

        this.TemplateMap =
        {
            'ProductGrid': 'Content/Templates/ProductGrid.html',
            'ProductItem': 'Content/Templates/ProductItem.html'
        };
    };

    w['AppConstantsLoader'] = constantsLoader;

})(window);

There is one last JavaScript module which we will use to compile the Handlebars templates. Now before we try to implement the code, we should make sure how we are going to put everything together; so here it goes. We need:

  1. input data
  2. handlebars template which is nothing but HTML string with tokens in the form of {{value}} here and there, and
  3. lastly, we need to combine the two to get our final HTML that will be shown to the user. It means that the output HTML will always be based on the user's requested information.

For example, if we have input data as follows in the form of Json:

var data = {'FirstName': 'Mark', 'LastName': 'Twain'};

and we have the following handlebars template:

<div id='placeholder'></div>
<script id="my-template" type="text/x-handlebars-template">
    <div>First Name: {{FirstName}}</div>
    <div>Last Name: {{LastName}}</div>
</script>

then we will write the following JavaScript code to combine the template with the data:

var myTemplate = Handlebars.compile($('#my-template').html());
var myHtml = myTemplate(data);
$('#placeholder').html(myHtml);

We can also bind json object arrays with the templates, for that purpose, we need to use {{#each}} token as given in the following example:

var contact = {'Name': 'Jason', 'Phones':[{'Number': 111111}, {'Number': 222222}]};

<script id="my-template" type="text/x-handlebars-template">
    <div>Name: {{Name}}</div>
    Phone Numbers: <br />
    {{#each Phones}}
        <div>Last Name: {{Number}}</div>
    {{/each}}     
</script>

It's simple as that. However, we are not going code things this simple in our sample app. We will load the template into the browser cache whenever we are going to need it and then will use the input data to get the output HTML.

Add a new JavaScript file and name it Templates.js and paste the following in it:

;
(function (w, $, h)
{
    var templatesLoader = function ()
    {
        this.GetTemplateHtmlAjax = function (name, jsonData, secondCallback)
        {
            var source;
            var template;
            var html;

            $.ajax({
                url: App.Constants.TemplateMap[name],
                cache: true  
            }).then(function (data)
            {
                source = data;
                template = h.compile(source);
                html = template(jsonData);
                secondCallback(html);
            });
        }        
    };

    w['AppTemplatesLoader'] = templatesLoader;
})(window, jQuery, Handlebars);

GetTemplateHtmlAjax function accepts the template name, jsonData and the callback as arguments. The template name is used to resolve the template file path, jsonData will be provided as the source for the loaded and compiled template and the secondCallback function will be called along with passing the output HTML.

So when we want to bind input data with a template, then we simply have to call the above GetTemplateHtmlAjax and will put the HTML writing code inside the callback function which will be executed once we have the HTML response available.

Our small client side framework is almost complete, all we need to do now is to define our template HTML and put everything together. Add two HTML files to the Templates folder (see the solution explorer image above) and name them ProductGrid.html and ProductItem.html.

ProductGrid.html

<div>
    {{#each Products}}
        <div class="ProductGridItem">
            <div>
                <div style="display:block;overflow:hidden;margin:0 auto;">
                    <a href="Product.aspx?id={{Id}}">
                    <img src="../../Content/Images/{{Image}}" /></a>
                </div>
            </div>
            <hr />
            <div><a href="Product.aspx?id={{Id}}"><b>{{Name}}</b></a></div>            
            <hr />
            <div style="text-align:center;"><b>Rs. {{Price}}</b></div>    
            <hr />      
            <div><a class="AddToCartGridButton" href="#">Add To Cart</a></div>  
        </div>
    {{/each}}
</div>

ProductItem.html

<div style="overflow:hidden;">
    <div class="ProductItemLeftDiv">
        <img src="../../Content/Images/{{Image}}" />
    </div>
    <div class="ProductItemRightDiv">
        <div><h2>{{Name}}</h2></div>
        <hr />
        <b>Price</b> {{Price}}
        <hr />
        {{SmallDescription}}
        <hr />
    </div>
</div>
<div id="divLargeDescription">
    <h3>Description</h3>
    <hr />
    {{LargeDescription}}
</div>
<div id="divAttributes">
    <h3>Attributes</h3>
    <hr />
    <table>
        {{#each Attributes}}
            <tr>
                <td><b>{{Name}}</b></td>
                <td>{{Value}}</td>
            </tr>    
        {{/each}}
    </table>
</div>
<div id="divReviews ">
    <h3>Reviews</h3>
    <hr />
    {{#each Reviews}}
        <span class="ReviewedBySpan"><b>{{ReviewedBy}}</b></span>
        <span class="RatingSpan">Rating: {{Rating}}</span>
        <div>{{ReviewText}}</div>
    {{/each}}
</div>

We are going to need Handlebars and jQuery library files. Although jQuery is not mandatory, most of us end up using jQuery at some point to avoid handling every code scenario for various browsers.

Download the latest Handlebars and jQuery library scripts and add their reference in the master page (alternatively, you can use cdn links as they might have already been cached by the browser). We also need to add references of all the JavaScript files, and a separate script block to initialize our client App object.

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Master.master.cs" 
    Inherits="TestCart.Master" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>

    <link href="Content/Style/Style.css" rel="stylesheet"/>

    <script src="Content/Scripts/jquery-1.11.3.min.js"></script>
    <script src="Content/Scripts/handlebars-v4.0.2.js"></script>
    <script src="Content/Scripts/App.js"></script>
    <script src="Content/Scripts/Constants.js"></script>
    <script src="Content/Scripts/Events.js"></script>
    <script src="Content/Scripts/Service.js"></script>
    <script src="Content/Scripts/Templates.js"></script>

    <script>
        $(document).ready(function ()
        {
            if(!window.App) window.LoadApp();
        });
    </script>

    <asp:ContentPlaceHolder ID="head" runat="server">
    </asp:ContentPlaceHolder>

</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:ContentPlaceHolder ID="MainPlaceHolder" runat="server">
        
        </asp:ContentPlaceHolder>
    </div>
    </form>    
</body>
</html>

In the above code, we are calling LoadApp function to create the App object if it is not already there in the browser memory.

Add the following script block to Default.aspx to load the product grid:

<script>
        $(document).ready(function ()
        {
            var app = window.App;
            if (app)
            {
                app.Service.Call('GetAllProducts').then(function (data)
                {
                    $divGrid = $('#divGrid');

                    //Our callback
                    function setHtml(html)
                    {
                        $divGrid.html(html);
                    }

                    app.Templates.GetTemplateHtmlAjax('ProductGrid'
                    , { 'Products': data }, setHtml.bind(this));
                })
            }
        });
    </script>

The above code should be easy to understand. We are calling the GetAllProducts web Api service and in the success callback, we are calling the GetTemplateHtmlAjax function from the Templates module to get the output html string. The setHtml is the callback function which is passed as an argument to GetTemplateHtmlAjax and gets called after a HTML response is available. The path of web Api service will be resolved from the metadata information that we coded into the Constants.js file.

One thing to note here is that we are using bind on setHtml function to preserve the function scope. If you are unaware of what that means, then you should learn more about scopes in JavaScript as this is outside the scope of this article (no pun intended!).

Similarly, add the following script block to the Product.aspx file:

<script>
    $(document).ready(function ()
    {
        var app = window.App;
        if (app)
        {
            app.Service.Call('GetProduct', { 'Id': getParameterByName('id') }).then
            (function (data)
            {
                $divProduct = $('#divProduct');

                //Our callback
                function setHtml(html)
                {
                    $divProduct.html(html);
                }

                app.Templates.GetTemplateHtmlAjax('ProductItem', data, setHtml.bind(this));
            })
        }

        //To read id from the url
        //http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
        function getParameterByName(name)
        {
            name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
            var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
                results = regex.exec(location.search);
            return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
        }
    });
</script>

getParameterByName is there to read the value of any query string parameter in the url. We are reading the value of id from the url which is then passed as an argument to the web api service call.

So that was it, now all you need to do is download the sample code and run it if you haven't already and relate all the steps given here with the sample code to get a much better understanding of how everything is working. I have not gone into much detail about Handlebars or Web Api in general as my intention was to give a working sample on which you can base your code or maybe you can get a better view from a different perspective on how to design your application when implementing HTML templates.

If you think that I have missed something or I need to explain anything differently, then feel free to drop your comment and I will be more than willing to fix it.

History

  • 12th November, 2015: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Nitij
Software Developer (Senior)
India India
Just a regular guy interesting in programming, gaming and a lot of other stuff Smile | :)

Please take a moment to visit my YouTube Channel and subscribe to it if you like its contents!
My YouTube Channel

Don't be a stranger! Say Hi!!

Cheers!

Comments and Discussions

 
GeneralMy vote of 5 Pin
Santhakumar M13-Nov-15 4:24
professionalSanthakumar M13-Nov-15 4:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 12 Nov 2015

Stats

24.2K views
435 downloads
23 bookmarked