Part 3: Creating and consuming a service
This continues on from the work done in part 2, if you haven't completed that, it's best to head over there. Alternatively, you can switch to the branch "Part02Final" (Team Explorer - Branches - right click "Part02Final" 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 2.
Creating our thingList service
In wwwroot/js/src, create a folder named services, and in that, a Javascript file named thingList.service.js. In that file, paste the following boilerplate starting point for our service (which, by convention, will be named with a lower-case leading letter to indicate that only a single instance of this will ever exist):
(function () { 'use strict'; // Register our service on our module. angular.module('app').factory('thingListService', thingListService); // Inject the dependencies - we'll use the angular-provided $http // service to interact with the server. thingListService.$inject = ['$http'] // Define our service - note that Angular will ensure that whatever // we've specified in our $inject clause above will get injected as // parameters here, allowing us to use that within our service. function thingListService($http) { // Define the public interface for the service here var service = { }; // Return the interface. return service; // Define the functions referenced by the service interface // above here: } })();
Here, we've registered a new, empty, service called thingListService with Angular, and declared that it requires the Angular-provided $http service injected into it. Now, let's use that to get our list of things from the server. Update the public interface to include a new function:
var service = { getThings: getThings };
... then create the getThings function below the "return service;" line:
function getThings() { // $http.get returns a promise - when this promise resolves, we want to // unwrap the data from the response and return that to anything waiting // on this. return $http.get("/api/thinglist").then( function (response) { return response.data; } ); }
This basic function has touched on an important concept - a "promise" - so before we continue, we'll have a quick introduction to promises and how they work. We'll get back to our service shortly!
A quick word on "Promises"
In Javascript there is only a single thread of execution, therefore anything which takes time to complete (such as HTTP calls to a server) can't simply wait, as the web browser would freeze until it returns. In such circumstances callback functions are used, which provide code to be executed when an asynchronous action completes.
"Promises" are a way of structuring callbacks in a way that allows the code to be written sequentially, chaining callbacks together one after another, instead of nesting callbacks inside each other which quickly becomes unmanagable. It also provides a standard way that calling code can access the final result of an asynchronous operation.
A promise returns an object which has a function .then(fulfilledCallback, [rejectedCallback]) on it - into this function, you can pass one or two callback functions - and the "promise" ensures that one of these (and only one) will be called exactly once at some point in future - called "resolving" the promise.
The fulfilledCallback will be executed should the promise complete successfully, and will be passed the result of the operation. The rejectedCallback (if specified) will be executed should the promise fail (this can be for technical errors such as a timeout or functional failures such as validation problems), and is passed a "reason" describing the failure (which will often be a Javascript Error instance of some kind).
The .then() function itself returns another promise, allowing .thens to be chained together one after another, to create a logical sequence of asynchronous operations.
Within a fulfilledCallback or rejectedCallback, you can return a value or another promise. If you return a value, any further ".then" callback which has been chained on will be immediately fulfilled with the value you have returned. If you return another promise, that will itself be waited upon, and once that resolves, any further ".then" callback which has been chained on will be fulfilled (or rejected) with the result of that promise.
To show the power of this, here's an admittedly unrealistic example showing multiple HTTP calls being chained together, each using the response of the last:
function doOurHorseyThing() { var promise = $http.get("/api/refdata").then( function (refDataResponse) { // For some reason, we're interested in the first horse returned, // so we return another promise here which will resolve with data // about that horse. var ourHorse = refDataResponse.data.horses[0]; console.log("We've found a horse with ID " + ourHorse.id); return $http.get("/api/horses/" + ourHorse.id); } ).then( function (horseDataResponse) { // Now we've got the data for that first horse, let's create a // race with his name included, and return a promise for creating // that race. var horseName = horseDataResponse.data.name; console.log("We've loaded the horses info and found its name is " + horseName); return $http.post("/api/races", { "horseName": horseName }); } ).then( function (raceCreatedResponse) { // This will be resolved once the race has been created, so let's // return the response to the POST to anyone waiting on this whole // promise chain to complete. console.log("We've successfully made our race!") return raceCreatedResponse.data; }, function (err) { // Errors bubble along the promise chain until they find a then // with a failureHandler specified - therefore ANY // error thrown by ANY of the above asynchronous actions will // find its way here. If this doesn't throw an error itself, // anything chained onto the end of this will be resolved // *successfully*, so you need to re-throw or fully deal with // the error here. console.log("Failed doing our horsey thing - " + err.message); // Re-throw the error. throw err; } ); // ********** // NOTE: The next line *IS GUARANTEED* to be executed BEFORE ANY of // the callbacks specified inside the .thens above! Remember it's // all asynchronous inside the .then()'s, and the Promise spec // REQUIRES that, even if the promise is already resolved, callbacks // passed to .then() are not executed until the current execution // stack completes. // ********** console.log("Request sent!"); // Return the promise, so whoever called this can get access to the // result of these promises once they finish resolving, or add its // own chain of .thens on and keep the party going! return promise; }
Notice how, irrespective of how many actions we're chaining up, we're not getting any further "nested" as we go on - just adding .thens on to the end repeatedly. In real-world usage, you're unlikely to chain them all together like this in one place, more likely you'll have one or two chained together then return that promise to somewhere else, where you might add another "then" on the end, and so on! By using promises everywhere, buildling up sequences of asynchronous operations becomes relatively straightforward.
To compare, if you were using "standard" callback nesting, that same code would look like this - note how we're getting further and further nested as we go on (and just imagine if we added a couple more actions on the end!). Also note how we're having to provide multiple error handlers for the different levels of nesting:
For the purposes of the below, assume $http.get and $http.post take success and failure callbacks as parameters; this is not the case so this code would not actually work.function doOurHorseyThing(callbackWhenFinished, callbackIfFailed) { $http.get("/api/refdata", function (refDataResponse) { // For some reason, we're interested in the first horse returned, // so load the data about that horse. var ourHorse = refDataResponse.data.horses[0]; console.log("We've found a horse with ID " + ourHorse.id); $http.get("/api/horses/" + ourHorse.id, function (horseDataResponse) { // Now we've got the data for that first horse, let's create a // race with his name included var horseName = horseDataResponse.data.name; console.log("We've loaded the horses info and found his name is " + horseName); $http.post("/api/races", { "horseName": horseName }, function (raceCreatedResponse) { // This will be called once the race has been created, so let's // call the callback we were passed with the response for // creating the race. console.log("We've successfully made our race!") callbackWhenFinished(raceCreatedResponse.data); }, function (raceCreatedError) { // Do any specific error handling, or simply // call the failure callback. callbackIfFailed(raceCreatedError); } ); }, function (horseDataError) { // Do any specific error handling, or simply // call the failure callback. callbackIfFailed(horseDataError); } ); }, function (refDataError) { // Do any specific error handling, or simply // call the failure callback. callbackIfFailed(refDataError); } ); // ********** // NOTE: The next line will *LIKELY* be executed BEFORE ANY of // the callbacks specified above! Remember it's all asynchronous // inside the callbacks. However, unlike in the promise case, // this is not 100% guaranteed. // ********** console.log("Request sent!"); }
We'll be using promises while creating our services, so you can play with them more as we go - they can be confusing at first but once you get your head around them, they become second nature... nearly! :)
The promise is documented in the fabulously brief Promises/A+ open standard, and they will become a native part of Javascript in future versions of the language. You can read more about the guaranteed order of execution of then callbacks on the great pair of answers to this StackOverflow question
Now, back to our new service...
Wiring our service to our controller
Now we've got a basic service with one function on it, let's use that from our controller. Open up Home.controller.js and ensure our new service gets injected by adding it to the $inject array (remembering that you specify the name of the service by a string, which must equal that used when we registered our service with the module), and as a parameter to the top-level controller function.
Home.$inject = ["thingListService"]; function Home(thingListService) { ...
Using the service to load our list of things
Now, we'll create a private function for refreshing our list from the service. In the "Private" function definitions section of the controller, add something like the following:
function refreshList() { // Return a promise, so we can use this to do things once the // refresh has completed. return thingListService.getThings().then( function(things) { // Clear the array (setting an array's length to zero is // the simplest way to do this in javascript) vm.list.length = 0; // Add all returned items to our array. for (var x =0; x < things.length; x++) vm.list.push(things[x]); } ); }
Interesting things to note in this function:
- We're returning a promise - so we can easily wait for the refresh to complete when we use this function.
- We're updating our existing vm.list array, rather than replacing it with the value from the thingListService - this is important, because the ng-repeat directive we're using in the template watches the collection that it is bound to when the page is loaded - if we overwrite vm.list with a new value instead of editing the existing array, ng-repeat would simply continue watching the old (now de-referenced) array, and not show our items.
Before we try this out, we need to call our new refreshList function when initialising the controller - so add a call to refreshList() in the Initialisation section of your controller, then run it up!
As we're using a static variable, each time you restart the web application, the list will be cleared, so use Postman again to add an item, then refresh your page in the web browser, and you should see your item loaded into the list!
Adding items to the list
Using Postman isn't a very user-friendly way for our users to add things to the list, so let's update our addItem implementation to send the item to the server. To do this, we need to add an addThing function to the service, which will do a POST and handle the response:
function addThing(thing) { return $http.post("/api/thinglist", { "Value": thing }).then( function (response) { return response.data; } ); }
Here we're using $http.post instead of $http.get - it's very similar, but takes a second parameter of the data to send as the body of the request. We're constructing an object there that the server should be able to convert an instance of our Thing model that we created in part 2.
Remembering to add addThing to the service = {} section, we then head back to our controller. In the controller, we already have a addItem function, so we'll update that to use our service instead of updating the array locally:
function addItem() { // We don't want to add empty entries - we will change this to // show an error later. if (vm.newItem.length == 0) return; // Use the thingListService to submit our new item vm.adding = true; thingListService.addThing(vm.newItem).then( function (response) { // We've added it, so clear the textbox. vm.newItem = ""; // Refresh the list. return refreshList(); } ).then( function () { // The list has been refreshed now and we've finished // adding our item. vm.adding = false; } ); }
Note that we've added a new property - vm.adding - which you should add to your "Public" properties section and default to false.
Interesting things to note here:
- We're setting a boolean flag - vm.adding - to true while the add and subsequent refresh of the list is happening. We'll use that in our template to display an "Adding..." message and hide the Add link.
- We're re-using our refreshList function - and waiting on it to complete via the then() function, so we can re-set our flag to false once everything is complete.
Let's make a couple of small updates to our template to use our new "adding" property:
<a href="#" ng-hide="home.adding" ng-click="home.addItem()">Add!</a> <span ng-show="home.adding">Adding...</span>
Give it all a go! Now you should be able to add items to your list, and each time you submit an item, the "Add" link should temporarily be disabled, and the list should be refreshed to show your new item.
To prove it is working against the server, after adding a couple of items, refresh your page - it should reload the current list from the server, so you should see whatever you've added.
Removing items from the list
As a final exercise in this section:
- Update your new service to have a function to ask the server to remove an item from the list (remember, the name of the item being deleted should go in the URL to the server, to match the way we set our route up in the Web API).
- Update the removeItem function in your controller to use this instead of locally updating the array.
- Hide the "remove" links in your template while the remove is happening.
Don't forget you'll need to refresh your list after removing an item.
You can check your solution against the branch Part03Final which shows a final working solution for all the steps discussed in this part of the tutorial.
Onwards to Part 4 for a look at Directives.