Single Page Application with MVC and AngularJS part 2

In Part 1 we made a simple multi page application to display a list of users with simple CRUD functionality. Now to make the application behave as a single page an AngularJS application is layered on top.

Angular will need to be able to communicate asynchronously with the server so we need to implement a simple API for the application to talk to. Luckily this is easy with Web API. Thanks to our initial good practice we don't need too much duplication in our controller actions.

public class CustomerApiController : ApiController
{
    private ICustomerRepository customerRepository;

    public CustomerApiController()
    {
        this.customerRepository = new CustomerRepository(new CustomerDbContext());
    }

    public CustomerApiController(ICustomerRepository customerRepository)
    {
        this.customerRepository = customerRepository;
    }

    public IEnumerable<CustomerViewModel> Get()
    {
        return this.customerRepository.FindAll();
    }

    public Customer Get(int id)
    {
        return this.customerRepository.Get(id);
    }

    public void Post(Customer customer) 
    {
        this.customerRepository.Create(customer);
        this.customerRepository.Save();
    }

    public void Put(int id, Customer customer)
    {
        customer.Id = id;
        this.customerRepository.Update(customer); 
        this.customerRepository.Save();
    }
}

We made a point of making all of our views partials which would normally not make sense. You might be tempted to simply return a normal MVC route as your angular Template. The problem with this approach is that MVC will return the full page, including all your script references and the element that angular replaces content into and hey presto infinite recursion!

So instead we use separate routes just for angular which will return only the partial razor template and not the full page. This does mean some duplication in your controller actions, but because the business logic and view are cleanly separated the duplication becomes less cumbersome.

public ActionResult CreateTemplate()
{
    return PartialView("~/Views/Partials/_CreateCustomer.cshtml");
}

public ActionResult EditTemplate() 
{
    var model = new Customer();
    return PartialView("~/Views/Partials/_EditCustomer.cshtml", model);
}

public ActionResult IndexTemplate()
{
    var model = Enumerable.Empty<CustomerViewModel>();
    return PartialView("~/Views/Partials/_ShowCustomers.cshtml", model);
}

Remember the views are strongly typed so we need to pass in a dummy model in some actions. The overall bonus to this set-up is that your 'pages' will still render correctly with or without JavaScript and even if the user navigates directly to a particular URL. 

The next task is to implement the angular application. This tutorial is not so much about the basics of creating a AngularJS application, a good starter tutorial is available on the angular website.

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

The ngRoute module is added since we are creating a single page application and will require angular to handle routing. Next add a service to communicate with the web API, this provides a central way for the angular app to hook into the API.

app.service('CustomerService', ['$http', function ($http) {
    this.getCustomers = function () {
        return $http.get("/api/CustomerApi");
    };
    
    this.getCustomer = function (id) {
         return $http.get("/api/CustomerApi/" + id);
    };

    this.post = function (Customer) {
         var request = $http({ 
              method: "post",
              url: "/api/CustomerApi",
              data: Customer
         });

         return request;
    };

    this.put = function (id, Customer) {
         var request = $http({
             method: "put",
             url: "/api/CustomerApi/" + id,
             data: Customer
         });

         return request;
     };
}]);

At a high level the routing module does much the same job as MVC routing in other words it matches a path with a controller action. We therefore need to create controllers in our angular app to handle the different routes asynchronously using the service created earlier.

app.controller('ShowCustomersController', ['$scope', '$location', '$http', 'CustomerService', function ($scope, $location, $http, CustomerService) {

    loadData();

    function loadData() {    
       CustomerService.getCustomers().success(function (result) {
            $scope.Customers = result;
            $scope.Show = result.length > 0;
        });
    }

    $scope.edit = function (id) {
        $location.path("/Customer/Edit/" + id);
    }
}]);

app.controller('EditCustomerController', ['$scope', '$routeParams', '$location', '$http','$filter', 'CustomerService', function ($scope, $routeParams, $location, $http,$filter, CustomerService) {

    loadData();

    function loadData() {
        CustomerService.getCustomer($routeParams.id).success(function (result) {
            $scope.Customer = result;
        });
     }

     $scope.backToList = function () { 
         $location.path("/Customer/");
     }

     $scope.save = function (id) {
         if ($scope.edit_form.$valid) {
             CustomerService.put($routeParams.id, $scope.Customer).success(function (data, status) {
                  $scope.backToList();
            }).error(function (data, status) {
                  $scope.Error = "Failed to update customer";
            });
         }
     }
}]);

app.controller('CreateCustomerController', ['$scope', '$location', '$http', 'CustomerService', function ($scope, $location, $http, CustomerService) {
    $scope.backToList = function () {
        $location.path("/Customer");
    }

    $scope.save = function () {
         if ($scope.create_form.$valid) {
              var Customer = {
                    FirstName: $scope.FirstName,
                    Surname: $scope.Surname,
                    Telephone: $scope.Telephone
               };

        CustomerService.post(Customer).success(function (data, status) {
             $scope.backToList();
        }).error(function (data, status) {
             $scope.Error = "Failed to save customer";
        });
    }
}
}]);

The angular route names are important and how data will be retrieved by either AngularJS or MVC or in some cases both. When a user first goes to your application be it the root URL or some other section then obviously at that point you are requesting the page from the server using MVC.

Once the page has been loaded for the first time from the server and hence MVC angular will take over and begin taking care of everything assuming the user’s client has JavaScript enabled.

Ordinarily Angular uses routing in conjunction with flat html templates. We ideally want to re-use our .cshtml razor templates to avoid repeating front end mark-up in two places. It is actually possible to do this so long as you separate your views correctly.

app.config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) { 

    $routeProvider.when('/Customer', {
        templateUrl: '/Customer/IndexTemplate',
        controller: 'ShowCustomersController'
    });

    $routeProvider.when('/Customer/Edit/:id',
    {
        templateUrl: '/Customer/EditTemplate',
        controller: 'EditCustomerController'
    });

    $routeProvider.when('/Customer/Create',
    {
        templateUrl: '/Customer/CreateTemplate',
        controller: 'CreateCustomerController'
    });

    $routeProvider.otherwise(
    {
        redirectTo: '/'
    });

    $locationProvider.html5Mode(true);
    $locationProvider.hashPrefix = '!';
}]);

Now we modify the existing Partial Views to add in the angular directives to bind the controls to the model, this means we are reducing duplication of front end markup. Again the Create and Edit views are more or less identical so only the create is shown here.

@model Customer

<form name="create_form" method="post" data-ng-submit="save()">
    <div class="form-horizontal">
        <h4>Customer</h4>
            <div class="form-group">
                @Html.LabelFor(model => model.FirstName, "First Name")
                @Html.EditorFor(model => model.FirstName, new { htmlAttributes = new { data_ng_model = "FirstName" } })
            </div>
            <div class="form-group">
                @Html.LabelFor(model => model.Surname, "Last Name")
                @Html.EditorFor(model => model.Surname, new { htmlAttributes = new { data_ng_model = "Surname" } })
            </div>
            <div class="form-group">
                @Html.LabelFor(model => model.Telephone, "Phone Number")
                @Html.EditorFor(model => model.Telephone, new { htmlAttributes = new { data_ng_model = "Telephone" } })
            </div>
            <div class="form-group">
                <input type="submit" value="Create" />
            </div>
        </div>
    </form>
<div>
@Html.ActionLink("Back to List", "Index", null, new { data_ng_href = "/Customer" })
</div>   

The table of entries does contain some duplication unfortunately this is unavoidable because we still need the server side repeat to run should the page be loaded there first. The angular repeater is hidden then until required.

<p>@Html.ActionLink("Create New", "Create", null, new { data_ng_href = "/Create" })
</p>
<table class="table">
    <tr>
        <th>First Name</th>
        <th>Surname</th>
        <th>Telephone</th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>@item.FirstName</td>
        <td>@item.Surname</td>
        <td>@item.Telephone</td>
        <td>@Html.ActionLink("Edit", "Edit", new { id = item.Id })</td>
    </tr>
}

<tr class="ng-hide" data-ng-show="Show" data-ng-repeat="item in Customers">
    <td data-ng-bind="item.FirstName"></td>
    <td data-ng-bind="item.Surname"></td>
    <td data-ng-bind="item.Telephone"></td>
    <td><a href="" data-ng-click="edit(item.Id)">Edit</a></td>
</tr>
</table>

Finally add the view directive to the main page layout so angular routes knows what to replace when processing a particular route.

<body ng-app="customerModule">
    <div class="container">
        <div class="row">
            <div class="col-sm-12">
                <div ng-view>
                    @RenderBody()
                </div>
            </div>
         </div>
    </div>
    <!--[if gt IE 8]><!-->
        <script src="~/Scripts/angular.min.js"></script>
        <script src="~/Scripts/angular-route.min.js"></script>
        <script src="~/Scripts/app/app.js"></script>
        <!--<![endif]-->
</body>

 The full code project solution can be found on github.