Tuesday, April 7, 2015

Angular JS with MVC templates

I was recently working on an ASP.MVC project and I was thinking about using angular on the project.  There wasn't going to be a ton of dynamic things that required angular, but I still thought it would be a good fit for the project.  I knew that the typical way to use angular + ASP.MVC is to pretty much use ASP.MVC as a rest service (WebAPI), but I wanted a hybrid approach where I could use the Razor templating in places that had static data and angular templating in places I wanted it to be more dynamic.

With your powers combined...

The solution that we used allowed us to do both which is what I wanted.  We would use MVC templates instead of just html ones.  Angular would reference those templates in the routing and it could contain the server side information when it came back with the template.


So to get started I created a basic MVC project and opened up NuGet (.net package manager) to remove the jQuery packages and add AngularJS Route package.

Next I modified App_Start/BundleConfig.cs to bundle the new files.  I removeed the jQuery ones and added the angular.js ones.

 
  public static void RegisterBundles(BundleCollection bundles){

      // angular.js libraries
      bundles.Add(new ScriptBundle("~/bundles/angular").Include(
            "~/Scripts/angular.js",
            "~/Scripts/angular-route.js"
            ));
      //Application code
      bundles.Add(new ScriptBundle("~/bundles/app").Include(
            "~/Scripts/app.js",
            "~/Scripts/controllers/*.js",
            "~/Scripts/services/*.js"
             ));

      // Use the development version of Modernizr to develop with and learn from. Then, when you're
      // ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
      bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                    "~/Scripts/modernizr-*"));

      bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));
}

Next let's update the Views/Shared/_Layout.cshtml to use the new bundles.  We will also add the ng-app  and ng-view directives here as well.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body ng-app="mvcApp">
    @RenderBody()

    <div id="content" ng-view></div>
    
    @Scripts.Render("~/bundles/angular")
    @Scripts.Render("~/bundles/app")
</body>
</html>

For this app I am going to have a home page with 3 links to a view and passing in the id in the following format '<controller>/<action>/<id>'. 

Notice in my page I have the ng-view directive, but I also have @RenderBody() which MVC uses to render the pages. What I am going to do is create a default page that just renders as an empty page.  This is going to be hit on the initial load, but that is it.  After that the MVC views will be used as angular templates.

So let's first add the mvc pages & services.

For my first view and control I will name it Home just to keep things simple.  I am going to implement an Index function and just return the view.

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

And here is the corresponding view:

@{
    ViewBag.Title = "View1";
    Layout = null;
}

<h2>Home View</h2>

<p>{{hello}}</p>

<nav>
    <ul>
        <li><a href="#dynamic/1">Dynamic Page with Id 1</a></li>
        <li><a href="#dynamic/2">Dynamic Page with Id 2</a></li>
        <li><a href="#dynamic/3">Dynamic Page with Id 3</a></li>
    </ul>
</nav>

For all my links I will be going through the angular routing.  Likewise I can use angular in my views. Here is the dynamic view which is a bit more interesting.

The dynamic view accepts an id as a parameter and returns data based on it.  First we create the controller which will return  an object with the controller.  Then we will set up the view to use both data retrieved from an angular service call and the MVC template.  Finally we will create a service for our angular code to call.

The controller:
public class DynamicObject
{
   public string Name { get; set; }
}

public class DynamicController : Controller
{
    public ActionResult Index(string id)
    {
        return View(new DynamicObject { Name = "Dynamic Object Name " + id });
    }
}

The view:
@model MVCApp.Controllers.DynamicObject

@{
    Layout = null;
}

<h2>Dynamic View</h2>

<h3>
    @Model.Name
</h3>

<p>{{hello}}</p>

<ul>
    <li ng-repeat="item in dynamicList">{{item}}</li>
</ul>

The Service:
public class DynamicController : ApiController
{
   private Dictionary<string, string[]> Data;
   public DynamicController()
   {

       Data = new Dictionary<string, string[]>();
       Data.Add("1", new string[] { "Mercury", "Venus", "Earth" });
       Data.Add("2", new string[] { "Mars", "Jupiter", "Saturn" });
       Data.Add("3", new string[] { "Uranus", "Neptune", "Pluto" });
    }

    [AcceptVerbs("GET")]
    public string[] Index(string id)
    {
        return Data[id];
    }

}

Notice in the view we are using the value from the model Name and using ng-repeat to iterate through all of the items in the dynamicList on our angular $Scope object.

Now that we have our server code all set up (you should be able to actually hit each of these endpoints/views).  We can get started on the angular side.

First we will create our app.js file in the Scripts folder to define our app and routing.

'use strict';
angular
  .module('mvcApp', ['ngRoute']).config(['$routeProvider',  function ($routeProvider) {

      $routeProvider.when('/', {
          templateUrl: 'home',
          controller:'mainCtrl'
      })
      .when('/dynamic/:id', {
          templateUrl: function(params){
              return 'dynamic/index/' + params.id;
          },
          controller: 'dynamicCtrl'
      });
}]);


One interesting thing to note here is that instead of returning a url for the dynamic page, I return a function which is then used to get the URL for the view.  I do this so I can pass the Id from the angular.js routing to the MVC routing.  An alternative could be query string passing, but I wanted to stick with the nicer looking urls.

Next we will define our two controllers.  One for the dynamic view and one for the home view,  The home view one is pretty simple, so let's just skip to the dynamic view one.

angular.module('mvcApp').controller('dynamicCtrl', ['$scope',
'$routeParams', 'dynamicService', 

function ($scope, $routeParams, dynamicService) {
    $scope.hello = "Hello World from dynamic.js";
    dynamicService.getDynamicList($routeParams.id).then(function (data) {
        $scope.dynamicList = data.data;
    });
}]);

So in this controller we are setting data directly on the scope property hello and putting data from a service call onto the dynamicList property of the scope.  We use the id from the $routeParams to pass onto the service.

So let's define the dynamicService.js file that will call out to the MVC.WebApi Conroller that we created earlier.

angular.module('mvcApp').factory('dynamicService',['$http',  function ($http) {
    function getDynamicList(id) {
        return $http.get('api/dynamic/' + id);
    }

    return {
        getDynamicList: getDynamicList
    };

}]);


Now we have a service that calls the rest endpoint that we created earlier.  So now if we run the app we should get what we were looking for.  MVC views with an angular.js front-end!  The source code is on github here if you want to check it out and run it yourself or use it as a reference.

Cheers!

Dynamic Page 1

Dynamic Page 2

Dynamic Page 3