Posting Data & AngularJS Custom Directives in an ASP.NET MVC application

Posted by: Sumit Maitra , on 4/18/2014, in Category DNC Magazine
Views: 29934
Abstract: This article demonstrates how to use AngularJS to Post Data and use AngularJS Custom Directives in an ASP.NET MVC application.

In our previous article, Getting Started with AngularJS in ASP.NET MVC - Part 1 we explored AngularJS in a plain vanilla ASP.NET MVC Twitter application. The article talked about basic data binding and retrieving of data, and introduced AngularJS, walking through some core Angular features like Directives, Modules, Services and $Resource Provider.

In this article, we will explore how to use AngularJS to Post Data and use AngularJS Custom Directives in an ASP.NET MVC application.

3rd and final part of this article - Defining AngularJS Routing in an ASP.NET MVC application

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

 

Posting Data

Before we get into Directives, let’s first add another functionality in our little Twitter Reader. The ability to send Tweets. So far we have used the $resource service’s query function. As we know, the resource function can also do GET, POST, PUT etc. Following is the map of actions to the HTTP verbs

{ 'get': {method:'GET'},

'save': {method:'POST'},

'query': {method:'GET', isArray:true},

'remove': {method:'DELETE'},

'delete': {method:'DELETE'} };

(Source: Angular JS Documentation)

So to post data, we need to simply call the save function on our resource instance. However there is a slight gotcha. The $resource object expects all the functions to post to the same URL. In MVC terms, this means posting to the same action method with different HttpGet/HttpPost/HttpPut etc attributes.

In our code, currently the HomeController uses GetTweets method to get the Data. Now Posting to a URL called GetTweets is semantically icky. So let’s update the Controller method to be called just Tweet. So we will do GET, POST, PUT etc. requests to the url /Home/Tweet.

Next we update our TwitterService to access the new URL

ngTwitter.factory("TwitterService", function ($resource)
{
    return {
        timeline: $resource("/Home/Tweet")
    }
});

Now if we run the Application, we should get the same UI where the latest 20 tweets are shown on screen.

Adding a Tweet Field

To send out Tweets, we need an input box and a Tweet button. To do this, we add the following markup to the Index.cshtml

<div>
<textarea ng-model="statusText" rows="5" />
<button class="btn btn-success" ng-click="sendStatus()">Tweet</button>
</div>

This adds a multiline textbox and a button to the top of the page. Note that we have added a view model element called statusText to contain the text or the status that we’ll be sending to Twitter, and a click handler sendStatus.

Adding the sendStatus method to the $scope

With our UI updated, we can add the sendStatus() event handler to our $scope as follows

$scope.sendStatus = function ()
{
var tweetText = $scope.statusText;
var newTimeLine = new TwitterService.timeline(
  {
   tweet: tweetText
  });
newTimeLine.$save();
}

In the method, we are fetching the text from the statusText into tweetText variable. Next we are retrieving an instance of the timeline $resource and saving it in var newTimeLine

Note we are instantiating it with a JSON object with the status text in a property called tweet.

Finally we are calling newTimeLine.$save() method that will do an HTTP Post to a Tweet method in our Home Controller.

Adding a Tweet Method to handle Post

Our client is ready to post Data. Let’s complete our controller to accept it.

[HttpPost]
public JsonResult Tweet(string tweet)
{
Authorize();
twitterCtx = new TwitterContext(auth);
try
{
  Status stat = twitterCtx.UpdateStatus(tweet);
  if (stat != null)
  {
   return Json(new { success = true });
  }
  else
  {
   return Json(new { success = false, errorMessage = "Unknown Error" });
  }
}
catch (Exception ex)
{
  return Json(new { success = false, errorMessage= ex.Message });
}
}

As seen above, the method accepts a string parameter called tweet. Note that it is same as the name of JSON property we initialized our service with.

We check if our AuthToken is still valid via the Authorize() helper method. Next we initialize the TwitterContext class provided by Linq2Twitter. Finally we call UpdateStatus method on the TwitterContext to send the Tweet.

If all goes well, we get a non-null Status and send back a JSON with ‘success’ property set to true. If there are any exceptions, we handle the exception and send a success = false and an error message.

Updating client to notify Save Status

So far we have not used the return data after Posting the Tweet. Let’s update our client to do that. You can pass a callback method to $resource.save(…) that will retrieve the data returned as well as the HTTP headers. Using the returned data’s success property, we can determine if the Tweet was sent successfully or not. We display an alert appropriately and if successful, we update the Text area to empty.

$scope.sendStatus = function ()
{
var tweetText = $scope.statusText;
var newTimeLine = new TwitterService.timeline(
{
  Tweet: tweetText
});
newTimeLine.$save(function (data, headers)
{
  if (data.success && data.success == true)
  {
   alert("Tweet Sent Successfully!");
   $scope.statusText = "";
  }
  else
  {
   alert("ERROR: " + data.errorMessage);
  }
});
}

That wraps up the code changes. Time to take it for a spin.

Posting Data using AngularJS (Sending a Tweet)

Step 1: On launch we are redirected to the Twitter page to login.

Step 2: Once we authenticate and log in, we’ll see the top 20 tweets for the account.

Step 3: First let’s try to send a Tweet longer than 140 characters.

post-data-error

Boom! Error as expected.

Step 4: Now let’s try to send a legit tweet!

post-data-success

Nice! Successfully sent! When you click OK, the text area is cleared out.

Step 5: Hit refresh!
post-data-result

Bingo! There is our Tweet!

With that we complete this small step in Angular. We were able to use the $resource service to post data. In our case we posted it all the way up to Twitter. But instead of Twitter, we could have easily posted it to a DB if we wanted to.

Customary Note of Caution: The Twitter Authentication method used here is NOT production ready!

Custom AngularJS Directives

So far, we have seen how we could get started by building a small Twitter Client. We have explored the view model, Modules and Services in Angular JS and how to post data using the $resource Service. We have also used the ‘ng-pluralize’ directive. The ng-app attribute that we use to define the scope of our Angular App is in fact a Directive, because there are no HTML5 attributes by that name! It’s Angular who interprets the attribute at runtime. It is time to dive deep into AngularJS Directives now.

Apart from helping add custom attributes, directives can also be used to create the server side equivalent of ‘tag-libraries’ on the client. Those familiar with WebForms or JSP development will remember you could create server side components with custom Tags like <asp:GridView>…</asp:GridView> where the GridView rendering logic was encapsulated in a server component. Well, Directives allow you build such components, but on the client.

In fact, next, we’ll define a ‘Retweet’ button on our Tweet Reader that will enable us to encapsulate the function in a custom directive.

Adding the Retweet Functionality

Adding a Retweet button is rather simple to do. All you have to do is update the markup and hook it up to a function call JavaScript. We could then use the $http resource and Post it to a Retweet action method.

The markup for this would be as follows, the new bits are highlighted.

<table class="table table-striped">
<tr ng-repeat="item in tweets">
  <td>
   <img src="{{item.ImageUrl}}" />
  </td>
  <td>
   <div>
    <strong>{{item.ScreenName}}</strong>
    <br />
    {{item.Tweet}}
   </div>
  
<div>
    <button class="btn btn-mini" ng-click="retweet(item)"><i class="icon-retweet"></i> Retweet</button>
   </div>
  </td>
</tr>
</table>

Model and Controller changes

LinqToTwitter’s Retweet API uses the Status ID that Twitter generated to do the Retweet. We have not saved StatusId in our TweetViewModel.cs so first we’ll update that:

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; }
}

Next we update the Tweet() HttpGet function in the Controller to save the Id in the Json that will be sent over to the client

friendTweets =
(from tweet in twitterCtx.Status
where tweet.Type == StatusType.Home &&
tweet.ScreenName == screenName &&
tweet.IncludeEntities == true
select new TweetViewModel
{
ImageUrl = tweet.User.ProfileImageUrl,
ScreenName = tweet.User.Identifier.ScreenName,
MediaUrl = GetTweetMediaUrl(tweet),
Tweet = tweet.Text,
Id = tweet.StatusID
})
.ToList();

Next we add the Retweet Action method

[HttpPost]
public JsonResult Retweet(string id)
{
Authorize();
twitterCtx = new TwitterContext(auth);
try
{
  Status stat = twitterCtx.Retweet(id);
  if (stat != null)
  {
   return Json(new { success = true });
  }
  else
  {
   return Json(new { success = false, errorMessage = "Unknown Error" });
  }
}
catch (Exception ex)
{
  return Json(new { success = false, errorMessage = ex.Message });
}
}

Finally we add the retweet function to the $scope as follows.

$scope.retweet = function (item) {
var resultPromise = $http.post("/Home/Retweet/", item);
resultPromise.success(function (data) {
  if (data.success) {
   alert("Retweeted successfully");
  }
  else {
   alert("ERROR: Retweeted failed! " + data.errorMessage);
  }
});
};

Well if we run the application now, things will just work fine and Retweet will work. This was doing things the HTML/JavaScript way. Time to get Directives into the picture.

Creating your own Directive

The idea behind creating a directive is to encapsulate the ‘Retweet’ functionality on the client side. For this example, this may seem a little counterproductive because the ‘Retweet’ functionality is rather simple and re-using it, involves only a bit of markup copy-paste.

But imagine for more involved UI Component like say a Tabbed UI functionality. Having a compact directive that renders the tabbed interface would really be a welcome relief.

In our case, we’ll simply create a new element called <retweet-button>…</retweet-button>.

Defining the Directive

To do this, we first copy the current markup from the Index.cshtml and replace it with the following:

<retweet-button></retweet-button>

Next we simply add the following snippet to our hello-angular.js file

ngTwitter.directive("retweetButton", function ()
{
return {
  restrict : "E",
  template : "<button class='btn btn-mini' ng-click='retweet(item)'><i class='icon-retweet'></i> Retweet</button>"
}
});

There are a few key things to note in the above snippet and how it ‘links’ to the custom element we used in the html markup.

1. The ngTwitter.directive method’s first parameter is the name of your directive. Note the naming convention, the ‘-‘ in retweet-button was removed and the next character ‘b’ was made upper case (camel case) and the markup attribute ‘retweet-button’ became the directive name ‘retweetButton’. Remember this convention else your directive will not be hooked up correctly and you’ll end up with invalid HTML.

2. Next, the directive method takes an anonymous function as parameter which returns an object. In our case the object has two properties

a. The ‘restrict’ property tells Angular what type of directive it is. In our case it’s an element, hence the “E”. If you omit it, default is attribute and things will break.

b. The ‘template’ property contains a piece of the DOM (not string). As we can see, we have simply copy pasted the button element from our cshtml file. This works, but it has the action method, the text both hard coded and as we know, hard-coding makes components ‘brittle’ and less extensible.

Before we go further you can run the application at this point and things will still work.

‘Transclusion’ and generalizing the Button’s label

First thing we want to do is to be able to have Text that we want, instead of hard-coding it in our Directive. To do this we can add the text in our markup as follows

<retweet-button>RT</retweet-button>

Then we’ll add a ‘transclude’ property in our directive’s return object and set it to true. Finally we’ll add the ng-transclude attribute in the template as shown below.

ngTwitter.directive("retweetButton", function ()
{
return {
  restrict: "E",
  transclude: true,
  template : "<button class='btn btn-mini' ng-click='retweet(item)' ng-transclude><i class='icon-retweet'></i> </button>"
}
});

If we use our browser tools, we’ll see that the actual button has been placed inside the <retweet-button> element. This is a little icky and can be easily fixed by using the ‘replace’ property in our directive’s return object.

ngTwitter.directive("retweetButton", function ()
{
return {
  restrict: "E",
 
replace: true,
  transclude: true,
  template : "<button class='btn btn-mini' ng-click='retweet(item)' ng-transclude><i class='icon-retweet'></i> </button>"
}
});

This tells angular to replace the directive with the Template.

Now that we have generalized the text, what if we wanted to pass in the action method name too? Transcluding doesn’t help there.

Using ‘Link Function’ in our Directive

As mentioned earlier, hardcoding the click event to an action method reduces the modularity and makes our ‘component’ brittle. One way to ‘not’ hardcode is to use the Linking function while creating our Directive. The linking function is defined via the Link attribute.

We update our directive definition as follows:

ngTwitter.directive("retweetButton", function ()
{
return {
  restrict: "E",
  replace: true,
  transclude: true,
  template: "<button class='btn btn-xs' ng-transclude></button>",
 
link: function (scope, el, atts)
  {
   el.on("click", function ()
   {
    scope.retweet(scope.item);
   });
  }
}
});

If we run the app now,we’ll still get routed to the retweet method. How is this working? Well the scope parameter of the input function passes the current $scope instance, the el parameter passes a jQuery object of the html element and atts element contains an array of attributes that we may have added to our directive markup. Currently we haven’t used atts but we’ll see it shortly. However the el element is a disaster waiting to happen. Given a jQuery object, we can simply go back to doing things the jQuery way like injecting the template etc. etc. In this case we’ve attached the click handler.

Well this works, but we can better this too and ‘angularize’ it further.

The ‘scope’ Definition for a Directive

We can add another property in the return object for Directive definition. This is the scope property. It is different from $scope as in, scope simply helps configure shortcuts that can be used in our template. For example, remember Angular uses the {{ … }} syntax for templating fields. So instead of using transclude, we can use the {{ … }} expression. But what will be the template property to bind to? We can define it in scope as follows (don’t forget to remove the transclude: true property and the ng-transclude attribute).

ngTwitter.directive("retweetButton", function ()
{
return {
  restrict: "E",
  replace: true,
 
scope: {
   text: "@"
  },

  template: <button class='btn btn-xs' ng-click='clickevent()'><span class='glyphicon glyphicon-retweet'></span> {{text}}</button>"
}
});

So what’s happening here? We’ve defined a scope property that has an object with the property text whose value is set to @. This is actually a ‘shorthand’ telling Angular to pick up the ‘text’ attribute from the markup’s list of attributes. Next, we have used the {{ text }} templating notation to tell angular that it should replace the value passed in via the scope->text in the template. The corresponding change in the markup is as follows:

<retweet-button text="Retweet"></retweet-button>

Okay, so this generalized the text in yet another way. How do we generalize the function call? Well we’ll use another shortcut in the scope for that. We update the directive script as follows first:

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

Yes, we have used another magic symbol & for getting hold of a ‘function pointer’ called clickevent. In our template, we are invoking the function that this pointer is pointing to. But where did we assign the function? In our view of course

<retweet-button text="Retweet" clickevent="retweet(item)"></retweet-button>

Finally we have extracted the action method we are supposed to call on $scope out from the directive into the markup making the directive pretty generic.

In hindsight, it seems like a lot of work to reduce

<button class="btn btn-mini" ng-click="retweet(item)"><i class="icon-retweet"></i> Retweet</button>

TO

<retweet-button text="Retweet" clickevent="retweet(item)"></retweet-button>

But as I said, we could potentially encapsulate the functionality of say sending a tweet or rendering a timeline etc., using the same directives functionality.

So Directives in Angular JS, kind of, teaches HTML to do new tricks. We were able to define a custom tag that did a custom action and ended up with a component that could be reused at other places in the App. Checkout the Tab component on AngularJS’ documentation for more examples.

If you are eager to read the last part of this article right now, you can subscribe to our Free .NET Magazine and read the entire article (Download Issue 10 – Jan/Feb 2014) . You can also download the entire source code of the app.

Give a +1 to this article if you think it was well written. Thanks!
Recommended Articles
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


User Feedback
Comment posted by Ben on Thursday, June 19, 2014 9:02 AM
Why this step is needed ? : Step 5: Hit refresh!

I thought angularjs could update the view automatically ?

thanks nice tutorial !

Post your comment
Name:  
E-mail: (Will not be displayed)
Comment:
Insert Cancel