Defining AngularJS Routing in an ASP.NET MVC application

Posted by: Sumit Maitra , on 5/26/2014, in Category ASP.NET MVC
Views: 96990
Abstract: This article discusses how to use AngularJS Routing and Directive specific Controllers 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







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




3. Final thing we added was the 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

that had the ng-controller attribute specified.




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:

{{item.Id}}">Details

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 friendTweets = new List();
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.


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: ""
};
});

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: ""
};
});

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: ""
};
});

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 status object 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



 


 


   {{item.ScreenName}}
  

   {{item.Tweet}}
 

 

   Retweet
   Details
 



Note we are using the ‘item’ instance from the tweets collection. If we rename the item instance to status 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

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.

advanced-directives-retweet

Click on Retweet for something you would like to share and it should use the new Directive Controller’s click event handler

retweet-breakpoint

Now navigate to the Details page of a Tweet

retweet-status-page_mini

Click retweet, you should see the same breakpoint being hit again. Voila!

retweet-status-page-breakpoint

And that’s finally a wrap for now!

Conclusion

That was a not-so-quick introduction to some fundamental concepts of Angular JS. We saw how to do Data Binding, fetch and submit Data, create Custom Directives and refactor to some best practices.

We built a Twitter reader over the course of the article, highlighting how well AngularJS was suited to interact with HTTP APIs. Combined with its routing capabilities, AngularJS provides a nice platform for the category of web applications that is now termed as SPAs or Single Page Applications.

It is worth mentioning that Visual Studio 2013 has first class support for Angular JS and it recognizes Angular Directives in your markup and treat them accordingly.

You can also subscribe to our Free .NET Magazine and read the entire article (Download Issue 10 – Jan/Feb 2014).

Download the entire source code of the app (Github)

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Sumit is a .NET consultant and has been working on Microsoft Technologies since his college days. He edits, he codes and he manages content when at work. C# is his first love, but he is often seen flirting with Java and Objective C. You can follow him on twitter at @sumitkm or email him at sumitkm [at] gmail


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!
Comment posted by D. Chamberland on Sunday, June 1, 2014 8:44 AM
Clean and concise AngularJS demo, now that's what I'm talking about. Nice job. Keep them coming...      
Comment posted by test on Friday, February 20, 2015 5:03 AM
<script>alert('Hello');</script>
Comment posted by Slaptas on Monday, March 30, 2015 1:50 AM
I like It! :)
Comment posted by excellent on Monday, May 25, 2015 2:09 PM
This is the first best example which has shown me the integration of angular js with mvc practically. Thank you Sumit