Using WebAIM with ASP.NET 2.0 and ASP.NET AJAX, Part 2

AIM Pages

by Christian Wenz
May 15, 2007

AOL's WebAIM API provides a very comfortable means to incorporate a JavaScript-powered AIM client in a web page. With some extra effort it is also possible to control most of WebAIM's features using a server-side technology. This article shows how to access the WebAIM API with ASP.NET AJAX and how to use the ASP.NET AJAX Control Toolkit to create a buddy list. In order to get the most out of the article, you should have read part one and followed the installation and setup instructions there.

Application Layout

The WebAIM application powered by ASP.NET 2.0 will consist of the following files:

  • Default.aspx: The main file, which will also communicate with the helper files.
  • WebService.asmx: A server-side web service that will communicate with the WebAIM service and provide the results to the client code.
  • WebAIM.js: A JavaScript helper file that will call the web service and apply the data coming from it to the web page.

First of all, delete the Default.aspx file provided by the ASP.NET AJAX website template and create a new ASP.NET file (called "Web Form" by Microsoft) and give it the name Default.aspx. Let the new page use the Default theme we created in the first part of the article by changing the first line of the file to the following:

<%@ Page Language="C#" Theme="Default" %>

Then, add a JavaScript block where the WebAIM key you have created is made available to the JavaScript code. Currently, the key is stored in the Web.config file, and the following code puts it into the page:

<script type="text/javascript">
var __KEY = "<% =System.Configuration.ConfigurationManager.AppSettings["WebAIMKey"] %>";
</script>

In the next step, the main server-side control of ASP.NET AJAX is included into the page: the ScriptManager. The ASP.NET AJAX ScriptManager is a control that is responsible for loading all helper files, including the ASP.NET AJAX JavaScript libraries and custom JavaScript files. The ScriptManager can also be used to access special ASP.NET web services.

In this application, we want all three features: load the ASP.NET AJAX JavaScript libraries (this is done automatically), load our WebAIM.js helper file, and provide access to our WebService.asmx web service. This markup, if put within the <form> element on the page, fulfills the task:

<asp:ScriptManager ID="ScriptManager1" runat="server">
  <Services>
    <asp:ServiceReference Path="WebService.asmx" />
  </Services>
  <Scripts>
    <asp:ScriptReference Path="WebAIM.js" />
  </Scripts>
</asp:ScriptManager>

Two more HTML elements are required for now. First of all, we need an <iframe> element, since some aspects of the WebAIM communication cannot be handled on the server alone. Also, a <textarea> element is used solely for debugging purposes; it will provide us with important information about the communication between client and server. (But the code that sends debugging information is not used in this article, only in the code downloads--basically, the dump() helper function writes data into the text box.)

<textarea id="myTextarea" rows="5" cols="80"></textarea>
<iframe id="myIframe" style="width:640px; height: 480px;"></iframe>

Create a new .asmx (web service) file called WebService.asmx. ASP.NET AJAX cannot work with regular web services, but a tiny extension makes an ASP.NET web service compatible with the Ajax library. Just use the ScriptService attribute (which is defined in System.Web.Script.Services):

[WebService(Namespace = "http://www.hauser-wenz.de/AspNetAJAX/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class WebService : System.Web.Services.WebService
{
  // ...
}

Finally let's prepare the WebAIM.js file. The ASP.NET AJAX ScriptManager supports a loading queue for including external JavaScript files. Due to the nature of JavaScript, it cannot detect whether an external file has been fully loaded without some extra help. This extra help is in the following code at the end of the JavaScript file, which basically notifies the ScriptManager that the file has been completely transmitted:

if (typeof(Sys) != "undefined") {
  Sys.Application.notifyScriptLoaded();
}

Application Flow

Working with the WebAIM API requires calling API methods from the client. For that, the REST principle (REpresentational State Transfer) is used: the browser sends HTTP GET requests to the server and gets XML or JSON back. This is then used from the JavaScript code. The API reference contains a complete list of all supported operations (just the list of error codes is unfortunately currently incomplete), but the following scenario is very common:

  1. Call the getToken() method to get a valid WebAIM token.
  2. On the first attempt, you will most probably receive the message "Authentication required," along with a URL of a login page that prompts the user to authenticate himself or herself with the AIM service.
  3. After successful authentication, call the getToken() method again to retrieve a token.
  4. Call the startSession() method to start a WebAIM session, providing the token you have previously received from the service.
  5. On the first attempt, you will most probably receive the message "Rights denied," along with a URL for a page that prompts the user to allow the script to access the API with his token.
  6. After successfully demanding the appropriate rights from the user, call the startSession() method again to retrieve an AIM session ID (called AIMSID).
  7. Once you have your AIMSID, you can fully access the WebAIM API, including retrieving the buddy list, subscribing to WebAIM events like buddies signing on/off, incoming messages, and much more.

So you see that the process to actually get into the WebAIM service is quite long. Even worse: some of this process cannot be fully mimicked on the server side. In particular, steps 2 and 5 must be done on the client: the user must log in to the WebAIM service, and the user must also acknowledge that the WebAIM API accesses and uses the current AIM account.

For the other steps, we can create a proxy on the server that takes care of communicating with the WebAIM server, calling the APIs and providing the data from the web service calls to the client. Doing so on the server allows us to use the advantages of a server-side language over JavaScript: better tool and language support. There is a cost, though: sometimes the communication may look quite hacky.

Calling Web Services with ASP.NET AJAX

The technical basis for our application is the ASP.NET AJAX web services support. When an ASP.NET web service is using the [ScriptService] attribute, ASP.NET AJAX can call the service. Two prerequisites must be met: the web service must be loaded in the ScriptManager control, and the service must also reside on the same server as the ASP.NET application--a requirement due to the security-wise same-domain policy of JavaScript.

If these requirements are met, the JavaScript method <Classname>.<Methodname> is automatically created. You have to provide a list of arguments to that method. First of all, all arguments that the web methods requires; additionally, you can (and should!) provide callback functions that are called when the web service returns data--or an error. Here is a sample call from JavaScript:

WebService.myMethod(param1, param2, successCallback, errorCallback);

The successCallback() function receives the data returned from the web service and can, for instance, output it:

function successCallback(data) {
  alert("Data from the server: " + data);
}

The error handling function receives any exception thrown on the server, and can access exception information like the actual exception message:

function errorCallback(data) {
  alert("Error from the server: " + data.get_message());
}

The ASP.NET AJAX web services support not only works with scalar data types, but also with complex data like arrays and objects. These data are usually converted into JSON (JavaScript Object Notation, json.org) on the server side and then automatically converted back into a JavaScript object on the client, allowing the exchange of non-trivial data, as well. Of course there are no miracles to expect: the JSON compatibility stops where the language features of JavaScript end.

Retrieving a Token

The first step consists of getting a valid token from the WebAIM server. For this, an HTTP request must be sent to the URL https://api.screenname.aol.com/auth/getToken?f=json&k=***, where "***" obviously denotes the WebAIM key. However, there is a catch: the WebAIM service checks the HTTP referrer to find out where the request came from. This is where our BaseURL configuration setting comes in: the URL of our script is sent in the Referrer HTTP header field. The following server-side code loads the remote URL and returns the data from the server:

[WebMethod]
public string getToken()
{
  string myURL = String.Format(
    "https://api.screenname.aol.com/auth/getToken?f=json&k={0}", 
    HttpUtility.UrlEncode(
      System.Configuration.ConfigurationManager.AppSettings["WebAIMKey"]));

  HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(myURL);
  myRequest.Referer = System.Configuration.ConfigurationManager.AppSettings["BaseURL"];

  HttpWebResponse myResponse = (HttpWebResponse)myRequest.GetResponse();

  if (myResponse.StatusCode != HttpStatusCode.OK) {
    throw new Exception("Status: " + myResponse.StatusCode.ToString());
  }

  Stream s = myResponse.GetResponseStream();
  StreamReader rs = new StreamReader(s, Encoding.UTF8);
  return rs.ReadToEnd();
}

Calling this from client side code is easy--the pageLoad() JavaScript function is automatically called from ASP.NET AJAX once all JavaScript libraries have been fully loaded:

function getToken() {
  WebService.getToken(getTokenCallback, myError);
}

However, evaluating the result proves as a bit tricky. First of all the data must be deserialized from a JSON string into a JavaScript object. This can be done using JavaScript's eval() function, or a helper method defined in ASP.NET AJAX, which also checks the data to be evaluated:

function getTokenCallback(result) {
  if (typeof(result) != "object") {
    var data = Sys.Serialization.JavaScriptSerializer.deserialize(result);
  }
  // ...
}

As aforementioned: the first time getToken() is called, the user is usually not authenticated. Therefore, the service returns the status code 401 and the message "Authentication required." In that case, the user must log into the system. The URL of the login page has been sent from the server in the redirectURL property--the URL is not fixed! Therefore, the page from redirectURL must be displayed on the page, in the <iframe> element already residing on the page:

if (data.response && data.response.statusText &&
    data.response.statusText == "Authentication required" && 
    data.response.data && data.response.data.redirectURL) {
  $get("myIframe").src = data.response.data.redirectURL + "?k=" + __KEY;
  _intervalID = setInterval("checkURL()", 500);
}

As you can see, the script then calls the checkURL() JavaScript function twice per second. The reason is simple: as soon as the user is authenticated with the AIM system, the hash of the URL (the data after the # character) changes to #AUTHDONE. In that case, we are ready to proceed and can call the getToken() WebAIM method again.

Figure 1
Figure 1. The user is prompted to sign in

However, there is a new catch: after signing into the system, the WebAIM service has sent a cookie to the web browser. Without this cookie, getToken() will fail (again). Therefore, the client must call the getToken() method via JavaScript; the server cannot help us with this task.

So the JavaScript code must call the http://api.screenname.aol.com/auth/getToken URL by dynamically adding a <script> tag to the page's DOM. The following code in checkURL() does the trick:

var lh = location.hash;
if (lh == "AUTHDONE" || lh == "#AUTHDONE") {
  location.hash = "#";
  clearInterval(_intervalID);
  var s = document.createElement("script");
  s.setAttribute(
    "type",
    "text/javascript");
  s.setAttribute(
    "src", 
    "https://api.screenname.aol.com/auth/getToken?f=json&k=" + __KEY + "&c=getTokenCallback");
  document.body.appendChild(s);
}

Note that the URL has been extended with the URL parameter c=getTokenCallback. This prompts the WebAIM server to return JavaScript code, which in turn calls a local function, getTokenCallback(). This function already exists; we will now expand it to save the token in a global variable and then proceed with starting the WebAIM session:

function getTokenCallback(result) {
  if (typeof(result) != "object") {
    var data = Sys.Serialization.JavaScriptSerializer.deserialize(result);
  } else {  //we get an object back, not JSON
    var data = result;
  }
  if (data.response && data.response.data && data.response.data.token) {
    _token = data.response.data.token;
    startSession();
  }
  // ...
}

The _token variable now holds the token information. Actually, _token itself is an object; _token.a contains the piece of data we now have to send to the startSession() method--in the third and final article in this series.

References

 


Enable the Subscriptions block here!