Marius Schulz
Marius Schulz
Front End Engineer

Inlining Angular View Templates with ASP.NET MVC and Razor

In this post, I want to show you a technique I've successfully used in multiple projects to increase the performance of Angular applications that use client-side routing and view templates.

#A Simple Demo Application

Let's assume we want to build a very simple Angular application that displays various JavaScript transpilers. On the home page, we'd like to see a list of all transpilers, like this:

List of transpilers

When one of the list items is clicked, the application navigates to a details view that shows some information specific to the selected transpiler. That could roughly look as follows:

Details view: The TypeScript transpiler

Note that the URLs in both screenshots show the routes used by the application. The client-side routing is achieved using the angular-route module. The routes and the application module itself are defined within an app.js file similar to the following:

(function () {
  "use strict";

  var app = angular.module("inlinedTemplates", ["ngRoute"]);

  app.config([
    "$routeProvider",
    function ($routeProvider) {
      $routeProvider
        .when("/transpilers", {
          templateUrl: "templates/TranspilerList",
          controller: "TranspilerListController",
        })
        .when("/transpilers/:transpiler", {
          templateUrl: "templates/TranspilerDetails",
          controller: "TranspilerDetailsController",
        })
        .otherwise({
          redirectTo: "/transpilers",
        });
    },
  ]);
})();

As you can see, the list of transpilers on the home page is managed by the TranspilerListController whereas the details page uses the TranspilerDetailsController. Both views have their own HTML templates, which are specified using the templateUrl property of the route. Before we go on, let's take a look at how Angular loads view templates.

#Loading View Templates from a URL

There are two properties that we can use to specify the view template for a given route. The first one, template, defines the template HTML directly. However, since we don't want to write a large view template within our app.js file, the second property, templateUrl, is a much better solution. Instead of passing it the view HTML directly, we can specify a URL to an HTML template. Here's the template for the transpiler list that should be returned with a content type of text/html from the specified URL:

<h2>Transpilers</h2>
<ul>
  <li ng-repeat="transpiler in transpilers">
    <a href="#/transpilers/" ng-bind="transpiler"></a>
  </li>
</ul>

When the user navigates to a route, Angular will look up the corresponding HTML template. If the template has not been loaded before, Angular will asynchronously download it from the given URL and continue once the HTTP response comes back. Depending on network latency, that HTTP request can take a while to complete. The user has to wait for the download to finish, which can easily take a second or two. Therefore, there's a noticeable delay.

Once a view template has been loaded, though, Angular puts it in a local cached for quicker retrieval. The next time the user navigates to the same route, the template is already there and doesn't need to be fetched again. In this case, the view will be rendered almost instantaneously because the template HTML is already on the client.

#Prefilling the Template Cache

What if we could prefill our application's template cache? If the templates were already preloaded, Angular wouldn't have to go fetch them when the user navigates to a different route. They would already be present and we could avoid the annoying delay caused by the latency of an additional AJAX request.

It turns out that we can do exactly that by providing <script> tags with a type of text/ng-template. Their id property is used to communicate to Angular for which route the given view template should be used. Note that the <script> tag has to be a descendant node of the application's root element (usually the element with the ng-app attribute); otherwise, Angular won't find the template.

Here's an example of a view template for our transpiler list view:

<script type="text/ng-template" id="templates/TranspilerList">
  <h2>Transpilers</h2>
  <ul>
      <li ng-repeat="transpiler in transpilers">
          <a href="#/transpilers/" ng-bind="transpiler"></a>
      </li>
  </ul>
</script>

Similarly, we can defined the template for the details view:

<script type="text/ng-template" id="templates/TranspilerDetails">
  <h2 ng-bind="transpiler.name"></h2>
  <p ng-bind="transpiler.description"></p>
  <p>
      <a href="#/transpilers">&larr; Back</a>
  </p>
</script>

Notice that browsers don't know about the text/ng-template type. Therefore, they will not interpret the contents of the <script> tag as JavaScript code. In fact, they won't even output the contents. They will treat the <script> tag as a regular DOM element containing some plain text, as is indicated by the missing syntax highlighting:

Missing syntax highlighting

A word of caution, though: You shouldn't blindly inline all view templates into your HTML this way. If you have dozens or hundreds of large views, you're increasing the size of the initial HTML tremendously. Also, the user might never visit many of the views for which you preloaded the template. However, if your application has a bunch of views the user is likely to visit, performance can be increased notably using this approach.

#Inlining View Templates with ASP.NET MVC

So how do you inline all those view templates within an ASP.NET MVC application? You don't want to have one big Razor view containing all Angular templates. It would be a lot nicer if every template were defined within its own Razor view to keep your code base clean and organized. Let's use partial views so that our templates are organized like this:

Angular template views in Solution Explorer

As you can see in the above screenshot of the Solution Explorer, every template resides in a separate file. The name of the template is given by the file name. Here's the AngularTemplatesController that looks up the names of all files within the Templates directory:

public class AngularTemplatesController : Controller
{
    [ChildActionOnly]
    public ActionResult Inline()
    {
        IEnumerable<string> templateNames = Directory
            .GetFiles(Server.MapPath("~/Views/AngularTemplates/Templates/"))
            .Select(Path.GetFileNameWithoutExtension);

        return View(templateNames);
    }
}

The benefit of this approach is that you can simply add a new Angular template by creating a new Razor view within the Templates directory. There's no need to explicitly add it to a list of partial views to include because the Inline action will pick up on that view automatically. Neat!

Finally, here's the corresponding Inline.cshtml view that inlines all templates into the HTML by rendering all partial views:

@model IEnumerable<string>

@foreach (string templateName in Model)
{
    <script type="text/ng-template" id="templates/@templateName">
        @Html.Partial("~/Views/AngularTemplates/Templates/" + templateName + ".cshtml")
    </script>
}

The Inline action can now be called as a child action within the layout view (or any other Razor file):

<body ng-app="inlinedTemplates">
    <div class="container">
        <h1>Inlined Angular Templates</h1>
        <hr />
        <div ng-view></div>
    </div>

    @Html.Action("Inline", "AngularTemplates")

    <script src="~/Client/scripts/vendor/angular.js"></script>
    <script src="~/Client/scripts/vendor/angular-route.js"></script>
    <!-- ... -->
</body>

In a real-world application, you'd use bundling and minification for the JavaScript files, of course. In this example, I've simply listed all JavaScript files for the sake of simplicity. Make sure to read my post on bundling and minifying Angular applications with ASP.NET MVC for more information.

If you run the application now, Angular will load the given view templates into its template cache. So are we done? Almost.

#Providing URLs to the Angular View Templates

There's one more thing to do. In our app.js file, we specified templates/TranspilerList and templates/TranspilerDetails as the URLs to our view templates. Because we have inlined those templates, Angular won't request the given URLs anymore. However, it doesn't feel right to give out fake URLs to resources that can't be requested. We should therefore add an action to our AngularTemplatesController that returns the requested Angular template:

public ActionResult Template(string name)
{
    if (name == null || !Regex.IsMatch(name, @"^[-\w]+$"))
        throw new ArgumentException("Illegal template name", "name");

    string relativeViewPath = string.Format("~/Views/AngularTemplates/Templates/{0}.cshtml", name);

    return View(relativeViewPath);
}

Finally, to make the routing work with our template naming scheme, we have to add the following route definition to our application's RouteConfig:

routes.MapRoute("AngularTemplates", "templates/{name}",
    new { controller = "AngularTemplates", action = "Template" });

And this is it! Now, our Angular view templates can be requested individually.

#Summary

To avoid latency when loading view templates via additional AJAX request, Angular allows us to prefill its template cache. By inlining our various view templates into the page HTML, we can speed up our applications noticeably.

In my blog-post-samples repository on GitHub, I have added a demo application that includes all the code shown in this blog post.

If you want to go one step further with optimizing performance, check out my post on bootstrapping Angular applications with server-side data using ASP.NET MVC and Razor.