ASP.NET MVC6 Angular Tutorial

Basic ASP.NET 5 MVC6 Angular tutorial with easy gulp tooling.

View on GitHub

Part 5: Resolving, routing, and multiple pages

This continues on from the work done in part 4, if you haven't completed that, it's best to head over there. Alternatively, you can switch to the branch "Part04Final" (Team Explorer - Branches - right click "Part04Final" inside remotes/origin and select "New local branch from..." and give it a local name, tick Checkout branch and untick Track remote branch) to use our version of part 4.

Resolving data before loading a page

In our home page controller, the first thing we do is load our list of items from the server. This is simple, but where you have data that is a pre-requisite for showing a page, it's not the best way to do things. The page router has "resolving" functionality that allows data to be loaded as a pre-requisite for displaying a particular page, then passed in as a dependency much like a service. Let's convert our thingList home page to resolve the current list of things instead of loading it after page load.

Open up the routes.js file, and update the route we specified earlier as follows:

$stateProvider.state('home', {
    url: '/',                             
    templateUrl: 'js/src/home/home.html', 
    controller: 'Home as home',           
    resolve: {
        thingList: ['thingListService', function(thingListService) {
            return thingListService.getThings();
        }]
    }     
});

There's a few things going on here. The "resolve" property defines a set of items which must be resolved before the page is displayed. In this case, we've defined a single item, thingList. The value of the item can be one of three things - a hard-coded value, e.g. thingList: null, a function that returns a value directly, e.g. thingList: function() { return "horse"; }, or a function that returns a promise, which will be resolved before the page is displayed - as per the example above.

The other thing to note here is the "inline dependency" syntax. This is an equivalent to the $inject property we've used elsewhere - the dependencies are listed in the array as per $inject, but the final item of the array is the function that uses the dependencies.

Now we've updated the route, let's update our controller to use the data supplied to it. In Home.controller.js, you'll need to add "thingList" to the $inject line, and to the parameters to the function. Then, simply set vm.list = thingList; and remove the call to refreshList() in the initialisation section - so it should all look something like this:

Home.$inject = ["thingListService", "thingList"];
function Home(thingListService, thingList) {
    var vm = this;
    // "Public" properties
    vm.list = thingList;
    ...
    // Initialisation
    // refreshList(); -- no longer required, we're resolving it before
    //                   we display the view.    
    ...

Run it up, add a couple of items then refresh the page - you should see the items in the list when refreshing - and you should not see the page load without the items present (with the previous version, you may have noticed a slight lag between the page loading and the list being populated).

Multiple pages!

So far, we've only created a single page in our application. Let's add a second page to display some details! This will give us the opportunity to try out a few of the things we've covered so far, and to cover a couple of new areas too.

Update the WebAPI

We've decided that we want to store a more complex representation of our Things than the current Web API allows, so the first job is to update our Web API. Let's start by updating the Thing model class to add in a bit of detail about that thing:

public class Thing
{
    public string Value { get; set; }
    public string Detail { get; set; }
}

We also need to update our ThingListController to store these in a static dictionary of Thing objects, instead of a list of strings, and update our endpoints. It should look something like the below, noting the following key changes:

  • Returning IActionResult - instead of plain objects, we're now returning IActionResults from most of our methods. This allows us to return HTTP status codes as well as plain objects (wrapped in new ObjectResult()) - take a look to see how we're doing both of those.
  • Get collection - our basic Get() method for the entire collection is now returning the collection of Keys from our dictionary, rather than the previous string list.
  • Get detail - we've added a second Get(string thing) method which takes a URL including an identifier of a specific thing and returns the full representation of that single thing.
  • Put action - an HTTP PUT means "replace the representation at this URL with what I've supplied in the body of this request" - i.e. update an existing item. This completes our basic set of HTTP actions - GET retrieves (either a collection /api/thinglist or a specific item /api/thinglist/sheep), POST creates (always to the collection address /api/thinglist), PUT updates (always to a specific item /api/thinglist/sheep) and DELETE deletes (also to a specific item /api/thinglist/sheep).
private static Dictionary<string, Thing> s_thingDetails 
    = new Dictionary<string, Thing>();

// Get the IDs of all things
[HttpGet]
public IEnumerable<string> Get()
{
    return s_thingDetails.Keys;
}

// Create a new thing
[HttpPost]
public IActionResult Post([FromBody]Thing thing)
{
    // If this thing already exists, return a 409 Conflict.
    if (s_thingDetails.ContainsKey(thing.Value))
    {
        return new HttpStatusCodeResult((int)HttpStatusCode.Conflict);
    }
    s_thingDetails[thing.Value] = thing;
    // Just return an empty OK to say we've done what was asked.
    return new HttpOkResult();
}

// Get the full detail of an existing thing.
[HttpGet("{thing}")]
public IActionResult Get(string thing)
{
    // If we don't know about thing, return a not found.
    if (!s_thingDetails.ContainsKey(thing)) return HttpNotFound();
    // Return the full object.
    return new ObjectResult(s_thingDetails[thing]);
}

// Update an existing thing.
[HttpPut("{thing}")]
public IActionResult Put(string thing, 
    [FromBody] Thing thingWithDetail)
{
    // If we don't know about thing, return a not found.
    if (!s_thingDetails.ContainsKey(thing)) return HttpNotFound();
    
    // Update the details using the thing we've had sent in.
    s_thingDetails[thing] = thingWithDetail;

    // Return a representation of what we've updated.
    return new ObjectResult(s_thingDetails[thing]);
}

// Delete an existing thing.
[HttpDelete("{thing}")]
public IActionResult Delete(string thing)
{
    // If we don't know about thing, return a not found.
    if (!s_thingDetails.ContainsKey(thing)) return HttpNotFound();

    s_thingDetails.Remove(thing);

    // Just return an empty OK to say we've done what was asked.
    return new HttpOkResult();
}

For reference, here is the HTTP endpoints that this API now provides:

URL Verb Meaning
/api/thinglist GET Return the names of things in our list
Returns:
HTTP 200 OK, body e.g. ["Thing1", "Thing2"]
/api/thinglist POST Add a thing to the list.
Request body: { "Value": "Sheep", "Detail": "Sheep's detail!" }
Returns:
HTTP 409 Conflict if value (e.g. "Sheep" in this case) already exists HTTP 200 OK if fine, no body.
/api/thinglist/{thing} GET Returns detail of {thing}.
Returns:
HTTP 404 Not Found if {thing} doesn't exist.
HTTP 200 OK, body e.g.: { "Value": "Sheep", Detail: "Sheep's detail!" }
/api/thinglist/{thing} PUT Updates detailed representation of {thing}.
Request body: { "Value": "Sheep", "Detail": "Sheep's detail!" }
Returns:
HTTP 404 Not Found if {thing} doesn't exist.
HTTP 200 OK if updated OK, body e.g. { "Value": "Sheep", "Detail": "Sheep's detail!" }
/api/thinglist/{thing} DELETE Remove a particular thing from the list
Returns:
HTTP 404 Not Found if {thing} doesn't exist.
HTTP 200 OK if removed OK, no body.

At this point, our API is ready. Try it out from Postman to check that it works as expected.

Let's quickly update our thingList.serivce.js service to cover this updated API - this should all look fairly familiar, the only major changes are that we're now doing some error handling for the specific HTTP status codes that we have decided to return from our Web API, and we've added in our first use of the $http.put() function:

var service = {
    getThings: getThings,
    addThing: addThing,
    getThing: getThing,
    updateThing: updateThing,
    removeThing: removeThing
};

// Return the interface.
return service;

// Define the functions referenced by the service interface
// above here:
function getThings() {
    return $http.get("/api/thinglist").then(function (response) {
        return response.data;
    });
}

function addThing(thing) {
    return $http.post("/api/thinglist", { "Value": thing }).then(
        function (response) {
            return response.data;
        },
        function (errResponse) {
            if (errReponse.status === 409) {
                throw new Error("AlreadyExists");
            } else {
                // Re-throw for default handling.
                throw errResponse;
            }
        }
    );
}

function getThing(thing) {
    return $http.get("/api/thinglist/" + thing).then(
        function (response) {
            return response.data;
        },
        function (errResponse) {
            // If this was a not found, simply return null.
            if (errReponse.status === 404) {
                return null;
            } else {
                // Re-throw for default handling.
                throw errResponse;
            }
        }
    );
}

function updateThing(thing, thingWithDetails) {
    return $http.put("/api/thinglist/" + thing, thingWithDetails).then(
        function (response) {
            return response.data;
        },
        function (errResponse) {
            // If this was a not found, throw a specific error.
            if (errReponse.status === 404) {
                throw new Error("DoesntExist");
            } else {
                // Re-throw for default handling.
                throw errResponse;
            }
        }
    );
}

function removeThing(thing) {
    return $http.delete("/api/thinglist/" + thing).then(
        function (response) {
            return response.data;
        }, 
        function (errReponse) {
            // If this was a not found, throw a specific error.
            if (errReponse.status === 404) {
                throw new Error("DoesntExist");
            } else {
                // Re-throw for default handling.
                throw errResponse;
            }
        }
    );
}

We're now ready to add our second page.

Adding a second page to our application

We now want to add a page which will show the details of a single thing. It'll have its own controller and template, and the route will resolve the thing in question from our new Get endpoint.

Let's start by defining the new route in routes.js - this will look fairly similar to the existing route for "home", but will have a parameter in its URL to identify which "thing" we're interested in - this gets appended to the end of the existing state:

$stateProvider.state('home', {
... 
}).state('detail', {
    url: '/things/:thing',
    templateUrl: 'js/src/thingdetail/thingdetail.html',
    controller: 'ThingDetail as thingDetail',
    resolve: {
        thing: ["thingListService", "$stateParams", 
            function (thingListService, $stateParams) {
                // The injected "$stateParams" gets its parameters 
                // from the URL pattern defined above.
                return thingListService.getThing(
                    $stateParams.thing);
            }
        ]
    }
});

This looks very similar to our route for the "home" state, but with two notable differences - the URL has a parameter within it (identified by :thing) and the resolve function has an additional injection - $stateParams - which has properties on it relating to any parameters specified in the URL - in this case, a .thing property. We use that to load the relevant data from the thingListService.

This is an important point to understand, so we'll work through an example of how this operates.

  1. The user visits http://localhost:12345/things/chicken (by typing it in or by following a link inside the application)
  2. The URL will be analysed - Angular will identify our "detail" route as matching, and create a stateParams with $stateParams.thing = "chicken".
  3. The specified resolve will be executed with this stateParams, which calls thingListService.getThing("chicken").
  4. The service will do an HTTP GET on /api/thinglist/chicken.
  5. Once that has all completed, the resolve is complete and the requested detail page will be loaded.

Now we've got the route, we need to create the controller and template. Create a new folder in wwwroot/js/src named thingdetail, and put a new controller ThingDetail.controller.js and HTML template thingdetail.html. Put the controller boilerplate structure as per part 1, remembering to change the names to ThingDetail instead of Home.

In our new controller, we'll need to inject "thing" to receive the resolved data, and pop that in a new public property (e.g. vm.thing) - so overall it should look something like this:

(function () {
    'use strict';
    angular.module('app').controller('ThingDetail', ThingDetail);

    ThingDetail.$inject = ["thing"];

    function ThingDetail(thing) {
        var vm = this;
        // "Public" properties
        vm.thing = thing;

        // "Public" functions

        // "Private" properties

        // Initialisation

        // "Public" function definitions

        // "Private" function definitions

        // Event Subscriptions
    }
})();

In our new thingdetail.html template, let's show the name and detail from the Thing we've been passed - remember to use the alias for the controller that we defined in the route, and that the object passed in to the controller will be structured as per our Thing class on the server-side.

Once you've done that, you should end up with something like the below:

<table>
    <tr>
        <th>Name:</th>
        <td>{{thingDetail.thing.Value}}</td>
    </tr>
    <tr>
        <th>Detail:</th>
        <td>{{thingDetail.thing.Detail}}</td>
    </tr>
</table>

<p><a href="/">Back to Thing List</a></p>

I've added a link back to our "home" page at the bottom there, now we should update our thing list to link to these detail pages to complete the navigation around our app. In the agsThingList.html template, update the {{item}} line to wrap it in a link to our new detail page:

<ul>
    <li ng-repeat="item in agsThingList.list">
        <a ng-href="/things/{{item}}">{{item}}</a>
        <span ng-hide="agsThingList.removing">
            (<a href="#" ng-click="agsThingList.removeItem({item: item})">remove</a>)
        </span>    
    </li>
</ul>

Here, we're using ng-href instead of a plain href, this is so Angular can replace {{item}} correctly to form a valid URL.

Run this up, add an item or two, and you should be able to click on the item name and be taken to the detail page, showing the name you've entered on the previous page, and an empty detail. You should also be able to click on the Back to Thing List link and navigate back to our home page. If you look closely, you'll notice that the page isn't loading fully when you click these links - Angular is intercepting the clicks and loading the relevant states of the app internally, without an additional round-trip to the server - all that is actually being updated is the content inside the <ui-view> tag in Views/Home/Index.cshtml. The address bar URL is being updated as you move around, though, so if the user refreshes the page or sends the URL to a friend, they'll get the right "state" of the app when they load the URL.

Aside: Nested states

It is worth being aware that you can nest states inside each other. In this scenario, a top-level template would have another <ui-view> inside it, and child states which derive from that state would put their content there, and so forth. This is very powerful for building up a modular app.

To very quickly try this out, you need only to make a couple of changes to your routes.js file:

  • Change the name of your 'detail' state to home.detail - this defines it as a child of home.
  • Remove the leading / from the URL for that state, i.e. it becomes url: 'things/:thing' - this is because it inherits the URL of its parent, so you are defining the URL relative to that.

Then you just need to pop <ui-view></ui-view> into your home.html template where you want the detail to be injected (I'd suggest under the Add section). Now, when you run your app, the detail will be shown on the same page as the list!

Undo this or leave it as is for the next section - either will work!

Making it editable

Now we've updated our API and created our new form for viewing the details, we should make it editable to use our Put function and allow us to specify details for a thing. To do this, let's start by changing our thingdetail.html to provide editing functionality:

<table>
    <tr>
        <th>Name:</th>
        <td>{{thingDetail.thing.Value}}</td>
    </tr>
    <tr>
        <th>Detail:</th>
        <td>
            <input type="text" ng-model="thingDetail.thing.Detail"/> 
            <input type="button" ng-click="thingDetail.save()" 
                value="Save" ng-disabled="thingDetail.saving"/>
        </td>
    </tr>
</table>

<p ng-show="thingDetail.errMsg">{{thingDetail.errMsg}}</p>

<p><a href="/">Back to Thing List</a></p>

There shouldn't be anything too surprising there! We've used "ng-disabled", which can disable form elements based on a boolean, to stop Save being clicked while the save is processing. We've also defined a ng-click handler function - save() - so add that to our ThingDetail controller and make it call our thingListService.updateThing function. You should end up with something like this:

ThingDetail.$inject = ["thing", "thingListService"];
function ThingDetail(thing, thingListService) {
  var vm = this;
  // "Public" properties
  vm.thing = thing;
  vm.saving = false;
  vm.errMsg = null;
  
  // "Public" functions
  vm.save = save;
  
  // "Private" properties
  
  // Initialisation
  
  // "Public" function definitions
  function save() {
      vm.saving = true;
      thingListService.updateThing(vm.thing.Value, vm.thing).then(
          function (updatedThing) {
              // In case the server has changed anything, 
              // update our local thing (e.g. if we had an "updatedTime"
              // property managed by the server).
              vm.thing = updatedThing;
              vm.saving = false;
          },
          // Catch any errors and display a message.
          function (err) {
              vm.saving = false;
              vm.errMsg = err.message;
          }
      );
  }
  ...
}

Note that what we're sending back to the server is what the server originally sent to us - vm.thing - which has been updated by being bound to the input in our template. This means that no matter how many properties of this object we want to edit in our form, our code here and in the service remains identical - the only work is to add the inputs in our template and bind them to the relevant properties.

We've also included a bit of rudimentary error handling in there too, should the update fail - nothing flashy at the moment! To see this in operation:

  • Open your app and add an item
  • Navigate to the details page for that item
  • Open a second tab and go to your home page
  • Remove the item using the (remove) link
  • Go back to the first tab, and attempt to Save the details - you should see a DoesntExist message.

That's the end of this part, so if you're having trouble following any of the above, you can see our complete working solution in the Part05Final branch.