Friday 24 May 2013

AngularJS Tutorial and .Net Web API (part 4)

In this post I'll get step 8 of the AngularJS tutorial http://docs.angularjs.org/tutorial/step_08 running on top of the .Net Web API.  This tutorial uses more data, so we'll be plumbing in further ApiController actions to connect the Angular code to the data source (JSON text files).  I'll also take a look at some of the under-the-covers stuff about how the ApiController works when resolving requests to actions.

The code for the Web API can be got by using git checkout -f step8.

As per the Angular tutorial, use git to checkout out step 8 of the original tutorial, then copy all the phone model json files from app/phones (i.e. everything apart from phones.json, which you should have edited previously and don't want to overwrite).  Unfortunately all of these files contain a reference to an image file that won't work with our website directory structure, so you'll need to amend the paths in the 'images' section of the JSON files so that the paths point to "Content/img/phones" rather than "img/phones".

Following along with the original tutorial we're going to update the Angular controller (Scripts/AngularControllers/controller.js) so that it makes an http get request for the data the phone-detail view requires.  However it's going to call an action on our Phones controller, rather than make a direct call to a JSON file.  So the function call should look like this:
function PhoneDetailCtrl($scope, $routeParams, $http) {
    $http.get('api/phones/' + $routeParams.phoneId).success(function (data) {
        $scope.phone = data;
    });
}
Once again the path is to 'api/phones' so we'll be calling the Phones controller.  Note that we're also passing the phoneId, which in this case is a string.

Now update the AngularPartials/phone-detail.html file so that it matches the one from the AngularJS tutorial (app/partials/phone-detail.html).

In order for this to work we will need to update the PhonesController class so that it has a method that can respond to this call.
public PhoneDetail GetPhoneBy(string id)
{
 return this.phoneRepo.GetBy(id);
} 
There are a couple of things to note here.  First and most obviously, there's no 'PhoneDetail' class or GetBy(id) method on the repository - we'll add those in shortly.  More subtly, it's also worth thinking about what the ApiController does when it receives a Get request.  The base ApiController class uses a convention whereby it will try to match requests to method names that contain the the request type and the route parameters, (in this case 'Get' and a string).  What this means is that you cannot have two methods names that contain the same request verb (in this case 'get') that have the same signature in your ApiController.  It also implies that it doesn't matter what your methods are called from the application's point of view, those method names are really just for your benefit. 

If you do have two methods with the request type in their name and the same signature, the framework will throw a System.InvalidOperationException which will be caught and transformed into a 500 error response. For further detail download the code from http://aspnetwebstack.codeplex.com/ and take a look at System.Web.Http.Controllers.ActionSelectorCacheItem.SelectAction. Look for:
throw Error.InvalidOperation(SRResources.ApiControllerActionSelector_AmbiguousMatch, ambiguityList);
and
System.Web.Http.Dispatcher.SendAsync
Ultimately this means that Angular will return an empty template and you'll have to track down what the 500 error means.

Having said that, our controller has a single 'Get' method that takes a string as a parameter, so no issues there. It then calls a non-existent method on the repository class.  So, let's update the repository interface and implementation accordingly.
public interface IPhoneRepository
{
 IEnumerable<Phone> GetAll();
 PhoneDetail GetBy(string id);
}

public class FileDrivenPhoneRepository : IPhoneRepository
{
 public IEnumerable<Phone> GetAll()
 {
  string dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory").ToString();
  var phonesText = System.IO.File.ReadAllText(dataDirectory +"/phones.json");
  return JsonConvert.DeserializeObject<List<Phone>>(phonesText);
 }

 public PhoneDetail GetBy(string id)
 {
  string dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory").ToString();
  var phoneText = System.IO.File.ReadAllText(dataDirectory + "/" + id + ".json");
  return JsonConvert.DeserializeObject<PhoneDetail>(phoneText);
 }
}
Finally I'll create a new PhoneDetail Model (or ViewModel if you prefer).  Add a class 'PhoneDetail' to the Models folder:
public class PhoneDetail
{
 public string additionalFeatures { get; set; }
 public AndroidDetail android { get; set; }
 public string[] availability { get; set; }
 public BatteryDetail battery { get; set; }
 public CameraDetail camera { get; set; }
 public ConnectivityDetail connectivity { get; set; }
 public string description { get; set; }
 public DisplayDetail display { get; set; }
 public HardwareDetail hardware { get; set; }
 public string id { get; set; }
 public string[] images { get; set; }
 public string name { get; set; }
 public SizeAndWeightDetail sizeAndWeight { get; set; }
 public StorageDetail storage { get; set; }

 public class AndroidDetail
 {
  public string os { get; set; }
  public string ui { get; set; }
 }

 public class BatteryDetail
 {
  public string type { get; set; }
  public string talkTime { get; set; }
  public string standbyTime { get; set; }
 }

 public class CameraDetail
 {
  public string[] features { get; set; }
  public string primary { get; set; }
 }

 public class ConnectivityDetail
 {
  public string bluetooth { get; set; }
  public string cell { get; set; }
  public bool gps { get; set; }
  public bool infrared { get; set; }
  public string wifi { get; set; }
 }

 public class DisplayDetail
 {
  public string screenResolution { get; set; }
  public string screenSize { get; set; }
  public bool touchScreen { get; set; }
 }

 public class HardwareDetail
 {
  public bool accelerometer { get; set; }
  public string audioJack { get; set; }
  public string cpu { get; set; }
  public bool fmRadio { get; set; }
  public bool physicalKeyboard { get; set; }
  public string usb { get; set; }
 }

 public class SizeAndWeightDetail
 {
  public string[] dimensions { get; set; }
  public string weight { get; set; }
 }

 public class StorageDetail
 {
  public string flash { get; set; }
  public string ram { get; set; }
 }
}
Once again that I'm going with the convention of Camel case in my C# class to reduce the amount of work I have to do with the copied code for the Angular templates.  Also, I'm struck again by how clever newtonsoft json library is when it comes to transforming a fairly intricate object structure to and from JSON.  I guess that's  why MS use it under the hood.

Finally you'll need to update the CSS in Content/app.css to match what's in app/css/app.css in the Angular tutorial.

Hopefully you now have a working version of step8 of the AngularJS tutorial.

No comments:

Post a Comment