Wednesday 12 November 2014

Defining AngularJS Routing in an ASP.NET MVC application

In our previous article, Getting Started with AngularJS in ASP.NET MVC - Part 1 we talked about basic data binding and retrieving of data, and introduced some core AngularJS features. In Posting Data & AngularJS Custom Directives in an ASP.NET MVC application, we explored how to use AngularJS to Post Data and use AngularJS Custom Directives in an ASP.NET MVC application.

In this article, we will explore Routing in AngularJS as well as how to make Angular Directives self-contained. Make sure you read the previous articles in this series to make the most of this article.
This article is published from the DotNetCurry .NET Magazine – A Free High Quality Digital Magazine for .NET professionals published once every two months. Subscribe to this eMagazine for Free and get access to hundreds of free tutorials from experts
If you are familiar with MVC routing, you are wondering, err… routing is a server side thing, how does it matter to a client side framework. Well you are not alone, even I had the same thoughts when I heard of Routing in JS frameworks. But as you will see, routing is real and it works rather nicely on Angular JS.

Breaking the functionality up into ‘Views’

So far the application consists of one view with a single ng-controller. As a next step, we would like to implement a details view of the Tweet that will show us more information and if there are any embedded images, show the image as well.

Introducing the Route Provider $routeProvider

Before we introduce the status view, let’s tell Angular that we want to use the Route Provider. To do this we will take the ngTwitter application variable (in angular hello-angular.js) and add a delegate to the config function on it as follows:
// Create ngTwitter Module (roughly Module = namespace in C#)
var ngTwitter = angular.module("ngTwitter", ['ngResource','ngRoute']);
ngTwitter.config(function ($routeProvider)
{
$routeProvider.when(
  "/", {
  templateUrl: "timeline"
});
});
Let’s quickly review what we did here:
1. We added a delegate to the ngTwitter app and requested for the $routeProvider in the delegate.
2. Next we told the routeProvider that when the URL is ‘/’ that is the root URL, it should use a template called ‘timeline’.
3. Also notice we are passing ‘ngRoute’ parameter to angular.module function.
4. One more thing we have to do, is to add reference to the angular-route.js file in the _Layout.cshtml
<script src="~/Scripts/jquery-1.10.2.min.js"></script>
<script src="~/Scripts/bootstrap.min.js"></script>
<script src="~/Scripts/angular.js"></script>
<script src="~/Scripts/angular-resource.js"></script>
<script src="~/Scripts/angular-route.js"></script>
<script src="~/Scripts/hello-angular.js"></script>

Defining the Template

Above we have declared a templateUrl that’s set to the value “timeline” but we haven’t defined it yet. To define the template, we’ll wrap the existing markup inside our ng-controller with a <script> tag. There are two things special about the script tag.
1. Type is set to type=”text/ng-template”
2. The id is set to the templateUrl value we set in our route
<div ng-app="ngTwitter">
<div ng-controller="TimelineController">
  <script type="text/ng-template" id="timeline">
<!-- the existing markup -- >

  </script>
</div>
<ng-view></ng-view>
</div>
3. Final thing we added was the <ng-view> element. This is where the Template is placed when the route match happens.
If we run the app now, we’ll see that our Twitter client brings up the last 20 tweets with the Retweet button and text area to send new Tweets. So we have silently introduced routing in our existing Application.
Note: I have been holding off on clearing the query string of values returned by Twitter for a while now. Linq2Twitter saves the info in a cookie for us already so we don’t need to keep it around. I hacked around it by checking for Request.QueryString contents and re-directing to Index page if Query Strings are present after authentication. So the Index action method looks like the following, the new code is highlighted.
public ActionResult Index()
{
var unAuthorized = Authorize();
if (unAuthorized == null)
{
 
 if (Request.QueryString.Count > 0)
  {
   return RedirectToAction("Index");
  }

  return View("Index");
}
else
{
  return unAuthorized;
}
}
Removing Controller reference from the View to Route
In the markup above, we have a container that specifies the controller name for the Template. This hard-codes the Controller to the View. We can move it to the Route mechanism by updating the Route configuration as follows:
$routeProvider.when(
"/", {
  templateUrl: "timeline",
  controller: "TimelineController"
});
In the view we simply remove the <div> that had the ng-controller attribute specified.
<div ng-app="ngTwitter">
<script type="text/ng-template" id="timeline">
  <!-- the existing markup -- >
  …
</script>
<ng-view></ng-view>
</div>

Adding a new Route, ng-Controller and a new View

Adding a new Route is easy, because the $routeProvider’s when method returns a $routeProvider so we can simply chain another when like this
$routeProvider.when(
"/", {
  templateUrl: "timeline",
  controller: "TimelineController"
})
.when(
  "/status/:id", {
  templateUrl: "status",
  controller: "StatusController"
});
What the new route is telling us is, the path /status comes with a parameter called id and the template to use is name ‘status’ and it should use the StatusController to get relevant details.
Next in the Index.cshtml, we’ll add an anchor tag and style it like a bootstrap button. We’ll also set the href to point to the status page and pass the status ID to it. This is accomplished with the following markup:
<a class="btn btn-primary btn-mini" href="#/status/{{item.Id}}">Details</a>
Note the #/ notation for URL. This is a part of HTML spec where URL starting with # doesn’t cause a postback, instead the browser looks for the anchor in the same page. Running the App will give us a result as shown below.
routing-details-button
Setting up the new ng-Controller
Now let’s setup the StatusController in the hello-angular.js file.
ngTwitter.controller("StatusController", function ($scope, $http, $routeParams, 
TwitterService)
{
var resultPromise = $http.get("/Home/Status/"+ $routeParams.id);
resultPromise.success(function (data)
{
  $scope.status = data;
});
});
As we can see in the delegate, we have a new parameter getting injected called $routeParams. Thanks to our Route definition which specified the parameter as id and the URL in the View that sets up the id value, Angular sets up $routeParams as follows:
{ id: 1234567898 }
In the controller we setup a status object to be used as the view Model in $scope. Value of $scope.status is populated by our Server’s Status action method. We are revisiting $http service here so we have to use the promise to wait for Success callback to be called before we can set the value to $scope.status.
Adding new action method in HomeController to get Status from Twitter
In the HomeController we’ll add a new Status Action method. But before we do that, we’ll add a couple of properties to our TweetViewModel class. We’ll add FavoritedCount, RetweetedCount and HasMedia (a Boolean).
public class TweetViewModel
{
public string ImageUrl { get; set; }
public string ScreenName { get; set; }
public string MediaUrl { get; set; }
public string Tweet { get; set; }
public string Id { get; set; }
public string FavoriteCount { get; set; }
public string RetweetCount { get; set; }
public bool HasMedia { get; set; }

}
Next we refactor the translation of Status object into TweetViewModel from the Linq query to a helper method GetTweetViewModel
private TweetViewModel GetTweetViewModel(Status tweet)
{
var tvm = new TweetViewModel
{
  ImageUrl = tweet.User.ProfileImageUrl,
  ScreenName = tweet.User.Identifier.ScreenName,
  MediaUrl = GetTweetMediaUrl(tweet),
  Tweet = tweet.Text,
  Id = tweet.StatusID,
  FavoriteCount = tweet.FavoriteCount.ToString(),
  RetweetCount = tweet.RetweetCount.ToString(),
};
tvm.HasMedia = !string.IsNullOrEmpty(tvm.MediaUrl);
return tvm;
}
Finally we add the Status action method to call Twitter using Linq2Twitter
[HttpGet]
public JsonResult Status(string id)
{
Authorize();
string screenName = ViewBag.User;
IEnumerable<TweetViewModel> friendTweets = new List<TweetViewModel>();
if (string.IsNullOrEmpty(screenName))
{
  return Json(friendTweets, JsonRequestBehavior.AllowGet);
}
twitterCtx = new TwitterContext(auth);
friendTweets =
  (from tweet in twitterCtx.Status
   where tweet.Type == StatusType.Show &&
    tweet.ID == id
   select GetTweetViewModel(tweet))
    .ToList();
   if (friendTweets.Count() > 0)
    return Json(friendTweets.ElementAt(0), JsonRequestBehavior.AllowGet);
   else
    return Json(new TweetViewModel { Tweet = "Requested Status Not Found" }, 
     JsonRequestBehavior.AllowGet);
}
Adding the ‘status’ View
In the Index.cshtml, we add the following markup that will constitute the Status view.
<script type="text/ng-template" id="status">
<table class="table table-striped">
  <tr>
   <td>
    <img src="{{status.ImageUrl}}" />
   </td>
   <td>
    <div>
     <strong>{{status.ScreenName}}</strong>
     <br />
     {{status.Tweet}}
    </div>
   </td>
  </tr>
  <tr>
   <td>
    <div>
     <retweet-button text="Retweet" clickevent="retweet(status)"></retweet-button>
    </div>
   </td>
   <td>
    <span class="label label-info" >Retweeted </span> <span class="badge">{{status.RetweetCount}}</span>
    <span class="label label-info">Favorited </span> <span class="badge">{{status.FavoriteCount}}</span>
   </td>
  </tr>
</table>
<div ng-show="status.HasMedia">
  <img src="{{status.MediaUrl}}" />
</div>
</script>
Most of the markup is easy to understand. We are using the type=”text/ng-template” directive to declare this snippet as a template and tying it up with our route using the id=status.
If you remember in the Client Side controller we had added the data to $scope.status hence status is the name of our view model object, so while binding values, we use status.*
Towards the end we have a div with an ng-show directive with the value set to ‘status.HasMedia’. This is a directive we are using to dynamically show/hide any attached image that a tweet may have. Point to note is that value of ng-show is not escaped using {{ … }} impliying ng-show needs the binding expression from which to get the value and not the value itself.
All done.
Run the application now and click on the details button to navigate to the status page. As we can see below we have an amusing image shared by the user Fascinatingpics.
routing-attachment
Broken Retweet functionality
The hawk-eyed will note that the Retweet functionality is broken. This is because, while trying to show how to use a second controller, I left out the retweet method in our TimelineController. Actually the status functionality doesn’t need a separate controller of its own. To fix this we do the following:
1. Update status route to point to TimelineController
"/status/:id", {
    templateUrl: "status",
    controller: "TimelineController"
})
2. Update our TwitterService with a new status function, this will also require us to use the $http service that we’ll simply request Angular to inject. The status function will do the $http.get to retrieve the Status
ngTwitter.factory("TwitterService", function ($resource, $http)
{
return {
  timeline: $resource("/Home/Tweet"),
  status: function (id)
  {
   return $http.get("/Home/Status/" + id);
  }
}
});
3. Next in the TimelineController, we’ll request for the $routeParams service and put an if condition to check whether $routeParams has the id property and if so, call the status method on the service and wait for the promise to return successfully.
ngTwitter.controller("TimelineController", function ($scope, $http, $routeParams, TwitterService)
{
if ($routeParams.id)
{
  var statusPromise = TwitterService.status($routeParams.id);
  statusPromise.success(function (data)
  {
   $scope.status = data;
  });
}
else
{
  $scope.tweets = TwitterService.timeline.query({}, isArray = true);
}
// rest of the code remains the same
… 
}
That pretty much covers it and now our status view also uses the TimelineController. Thus now the retweet function will work perfectly!
routing-retweet
Super sweet, we just verified a use-case for custom directives! We added the Retweet button in a new view and things just worked!

Making Directives self-contained

In our previous section, we abandoned the StatusController and stuffed everything back into TimelineController. That was an ugly hack. After all we want the Retweet functionality to be self-contained and reusable and Directives are meant to help create reusable components. Let’s see how we can do this.
Fortunately there is one concept about Directives that we haven’t visited yet and that is Directive specific controllers. Yup, directives can have their own controllers as well, so we’ll use this feature to further modularize our controller.
Custom Controller Directives
Currently our Retweet Directive is defined as follows:
ngTwitter.directive("retweetButton", function () 

return { 
  restrict: "E", 
  replace: true, 
  scope: { 
   text: "@", 
   clickevent: "&" 
  }, 
template: "<button class='btn btn-xs' ng-click='clickevent()'><span class='glyphicon glyphicon-retweet'></span> {{text}}</button>"
}; 
});
It uses a local scope that is limited to the properties defined in here (that is text and clickevent properties). This scope overrides the global $scope. This is a key point to keep in mind when using Controllers for Directives.
We can update the above Directive as follows to have its own Controller
ngTwitter.directive("retweetButton", function ($http, $routeParams)
{
return {
  restrict: "E",
  replace: true,
  transclude: true,
  controller: function ($scope, $element)
  {
    // do what it takes to retweet
  },
  template: "<button class='btn btn-mini' ng-transclude><i class='icon-retweet'></i></button>"
};
});
Notice a few significant things:
1. We have let go of the custom scope, because we want access to the $scope.
2. As a result of the above, we have given up on the {{ text }} template in our HTML template for the button.
3. We’ve also removed the ng-click attribute from the button template and put the ng-transclude attribute back.
4. In the controller function we have $element which is an instance of the button from the template.
Moving the Retweet functionality into the Directive’s Controller
Well, we’ll need to move the Retweet functionality from the TimelineController into the retweetButton directive’s own controller. We add the highlighted code below to our Directive.
ngTwitter.directive("retweetButton", function ($http, $routeParams)
{
return {
  restrict: "E",
  replace: true,
  transclude: true,
  controller: function ($scope, $element)
  {
   $element.on("click", function ()
   {
    var resultPromise = $http.post("/Home/Retweet/", $scope.status);
    resultPromise.success(function (data)
    {
     if (data.success)
     {
       alert("Retweeted successfully");
     }
     else
     {
      alert("ERROR: Retweeted failed! " + data.errorMessage);
     }
    });
   });
  },
  template: "<button class='btn btn-mini' ng-transclude><i class='icon-retweet'></i></button>"
};
});
Let’s see what this does line by line:
1. We assign a click event handler to the $element which is essentially the button defined in the template.
2. When the click event fires, we use the $http object to do an http post and pass it the $scope.status object to this.
3. What does this $scope.status contain? Well that depends on the controller. As things are defined now, if we are in the Status Controller it provides us with the status object that is the current tweet as we can see from the controller below.
ngTwitter.controller("StatusController", function ($scope, $http, $routeParams,
TwitterService)
{
var resultPromise = $http.get("/Home/Status/" + $routeParams.id);
resultPromise.success(function (data)
{
  $scope.status = data;
});
});
4. However if we are in the TimelineController, this will return undefined because there is no statusobject in the TimelineController. Instead we have the tweets object. When looping through the tweet object, we use the following markup and template in the index.cshtml
<tr ng-repeat="item in tweets">
<td>
  <img src="{{item.ImageUrl}}" />
</td>
<td>
  <div>
   <strong>{{item.ScreenName}}</strong>
   <br />
   {{item.Tweet}}
  </div>
  <div>
   <retweet-button text="Retweet">Retweet</retweet-button>
   <a class="btn btn-primary btn-mini" href="#/status/{{status.Id}}">Details</a>
  </div>
</td>
</tr>
Note we are using the ‘item’ instance from the tweets collection. If we rename the item instance tostatus we are done and the $scope.status in the Directive’s controller will be valid.
1. Once we have the $scope.status object sorted out, rest of the code is about handling the return value from the server and displaying a success or failure notice.
We can now remove the $scope.retweet function from the TimelineController.
If we run the application now, we’ll see that the ‘Retweet’ text has gone missing. This is because we are now transcluding text from the Directive markup into the Template. So we’ll have to update the Directive markup as follows in two places (the timeline and status templates)
<retweet-button text="Retweet">Retweet</retweet-button>
With this change done we are good to go.

The Final Demo Time

Run the application and put a breakpoint in the click event handler of the directive’s controller.

1 comment:

Angular Tutorial (Update to Angular 7)

As Angular 7 has just been released a few days ago. This tutorial is updated to show you how to create an Angular 7 project and the new fe...