Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

flickr Spell Extender Control for ASP.NET Atlas

4.96/5 (15 votes)
27 May 2006CPOL12 min read 1   508  
An ASP.NET Atlas server extender control that converts text specified in a control to images from flickr.

Sample image

Introduction

Sometime back, I came across the Spell with flickr web page. You can enter some text in the web page and the text is spelled using images from the oneletter and onedigit group in flickr. This webpage uses PHP. I was experimenting with the concept of bridges in Atlas to develop a mash-up. In my actual mash-up application, I needed something similar to the Spell with flickr web site. The result is this extender which allows you to display the text inside an HTML element as random images, representing letters, from the oneletter and onedigit groups. Before we dwell into the details of the extender, let's look at the concept of Atlas extender controls.

Extending ASP.NET Server Controls

Atlas brings the concept of extender controls, which allows you to add rich features to existing ASP.NET controls. An example of an Atlas extender control is the AutoCompleteExtender which adds support for server aided AutoComplete to an ASP.NET TextBox server control. For example, you typically use an ASP.NET TextBox in an ASP.NET web page through the following declarative markup in the page:

HTML
<asp:TextBox ID="Textbox1" runat="server"></asp:TextBox>

You can extend this ASP.NET server control so that it provides AutoComplete support similar to Google suggest, by adding an Atlas AutoCompleteExtender control as shown here:

HTML
<atlas:AutoCompleteExtender ID="Extender1" runat="server">
   <atlas:AutoCompleteProperties TargetControlID="TextBox1" ... />
</atlas:AutoCompleteExtender>

There are two aspects in the extender control definition:

  1. The extender control tag: atlas:AutoCompleteExtender where you specify the runat attribute and an ID attribute.
  2. One or more extender control property elements, which specify the target control which needs to be extended and any other additional properties specific to the extender.

Extender controls use the concept of client side behaviors in Atlas. Atlas behaviors, similar in concept to DHTML behaviors, add rich functionality to existing DHTML elements, using JavaScript and DOM. Typically, a behavior is specified by using the Atlas XML-Script. The Atlas extender control on the server generates XML-Script for a behavior. Now that we have a brief idea about extender controls, let's look at how to use the flickrSpell extender control.

Using the flickrSpell Extender Control

The steps to use the flickrSpell extender control in an Atlas project are the following:

  1. Build the FlickrSpell project which generates the FlickrSpell.dll assembly.
  2. Copy FlickrSpell.dll and Microsoft.AtlasControlExtender.dll to the bin directory of your web site.
  3. In your web page, register the tag prefix to use the control, as follows:
    ASP.NET
    <%@ Register Assembly="FlickrSpell" TagPrefix="flickrSpell" 
                                        Namespace="FlickrSpell" %>
  4. Copy the FlickrBridge.asbx and FlickrBridge.asbx.cs files to your website. These are the Atlas bridge files which expose the flickr service to the JavaScript code.
  5. The control that needs to be extended should be a server control, i.e., it should have the runat attribute set to server. For example, the following snippet declares an h1 server control:
    HTML
    <h1 id="Test" runat="server">The CodeProject</h1>
  6. To extend this server control, add the extender control, as shown:

    HTML
    <flickrSpell:FlickrSpellExtender ID="FE1" runat="server" >
        <flickrSpell:FlickrSpellProperties TargetControlID="test" />
    </flickrSpell:FlickrSpellExtender>
  7. Obtain an API key from flickr. Check the Flickr Services website for more details.
  8. Once you obtain the API key, edit the web.config file of your web site and add the key to the appsettings section:
    XML
    <add key="FlickrAPIKey" value="Your API key"/>

When you browse the web page using a web browser, you will see the text "The CodeProject" replaced with images. The transition may be slow as sometimes the flickr website is quite slow. In the rest of the article, we will see how the control works. We will start by looking at the flickr web service.

Accessing Images on flickr

flickr is a popular website for sharing and storing photos. The flickr web site also allows you to tag and group photos. There are two groups in the flickr web site which are of interest in this article: oneletter and onedigit. The oneletter group in flickr has a collection of photos which depict different letters in the alphabet. The onedigit group does the same for digits. Every letter and digit is tagged by itself, with a few exceptions. For example, B is tagged with the text "b", C with the text "c", and so on. The two exceptions are the letter "A", which is tagged by the text "aa", and the letter I, which is tagged by the text "ii". Thus, searching the pool of photos in the group with the appropriate tag will result in the appropriate list of photos for a particular letter or digit.

The flickr API provides a way for external applications to access the flickr web site. The API has many features, but the feature that we use in this article is that of getting images from the pool of images in a group. The flickr API can be invoked in three different ways: using REST, using SOAP, and using XML-RPC. We will be using REST.

The REST way to access flickr is through the URL: http://www.flickr.com/services/rest/. You can supply different querystring parameters to this URL to access different functionality. The functionality you want to access from the flickr web site is indicated by using the method parameter. The method which lists the photos in the photo pool of a group is called flickr.groups.pools.getPhotos. Here are the arguments accepted by this method (the text is taken from the flickr web site):

api_key (Required)
Your API application key.
group_id (Required)
The ID of the group whose pool you wish to get the photo list for.
tags (Optional)
A tag to filter the pool with. At the moment, only one tag at a time is supported.
user_id (Optional)
The NSID of a user. Specifying this parameter will retrieve for you only those photos that the user has contributed to the group pool.
extras (Optional)
A comma-delimited list of extra information to fetch for each returned record. Currently, the supported fields are: license, date_upload, date_taken, owner_name, icon_server, original_format, and last_update.
per_page (Optional)
Number of photos to return per page. If this argument is omitted, it defaults to 100. The maximum allowed value is 500.
page (Optional)
The page of results to return. If this argument is omitted, it defaults to 1.

The group ID for the oneletter pool is 27034531@N00 and that of the onedigit group is 54718308@N00. The HTTP GET request to access photos of the letter A from flickr will look like the following:

http://www.flickr.com/services/rest?api_key=XYZ&
  method=flickr.groups.pools.getPhotos
  &group_id=27034531@N00&tag=aa&per_page=100

The returned response is of the form:

XML
<rsp stat="ok">
    <photos page="1" pages="1" perpage="1" total="1">
    <photo id="2645" owner="12037949754@N01" title="36679_o"
    secret="a9f4a06091" server="2"
    ispublic="1" isfriend="0" isfamily="0"
    /> 
</photos>
</rsp>

The next step is to construct the URL of the photo. flickr returns images in different sizes. The URL of the image which gives a small size (75px X 75px) is of the following format: http://static.flickr.com/{server}/{photo id}_{secret}_s.jpg. The server, photo_id, and the secret are all values from the XML response. This completes the process to access an image. Let's see how we can code this process in ASP.NET Atlas.

Bridging to flickr

Introduced in the March CTP of Atlas, bridges are the way by which a client script can access web services on a domain different from that of the host web page. A problem in AJAX development is when accessing services across domains as browsers, using the default configuration, cross-domain access through the XMLHttpRequest object is not permitted. ASP.NET Atlas solves this problem by routing cross-domain web service calls from JavaScript through the host web server. From the browser point of view, the call is to the host web server, so the call is safe from a security perspective. The bridge on the host web server makes the actual call to the web service on the other domain. There are two network roundtrips in this method: one from the client to the host web server, and the other from the host web server to the server providing the web service on the other domain. Here, the performance can be improved by providing a caching mechanism on the host web server. One advantage of the cache on the web server is that it can be shared by multiple clients.

An ASP.NET Atlas bridge is an XML file with an extension of asbx. The bridge can have a code-behind file. The contents of the FlickrBridge.asbx file is shown below:

XML
<bridge 
   namespace="FlickrSpell" className="FlickrBridge" 
   partialClassFile="~/FlickrBridge.asbx.cs">
  <proxy type="Microsoft.Web.Services.BridgeRestProxy" 
          serviceUrl="% appsettings : FlickrRESTEndPoint %" />
    <method name="getLetterImages">
    <input>
      <parameter name="api_key" value="% appsettings : FlickrAPIKey %" 
                serverOnly="true"/>
      <parameter name="method" value="flickr.groups.pools.getPhotos" 
                serverOnly="true"/>
      <parameter name="group_id" value="27034531@N00" 
                 serveronly="true"/>
      <parameter name="letter" serverName="tags" />
      <parameter name="extras" serverOnly="true" value="owner_name" />
      <parameter name="per_page" value="25" />
      <parameter name="page" value="1" />
    </input>
    <caching>
      <cache type="Microsoft.Web.Services.BridgeCache" />
    </caching>
    <transforms>
      <transform type="Microsoft.Web.Services.XPathBridgeTransformer">
        <data>
          <attribute name="selector" value="/rsp/photos/photo" />
          <dictionary name="selectedNodes">
            <item name="id" value="@id" />
            <item name="owner" value="@ownername" />
            <item name="title" value="@title" />
            <item name="secret" value="@secret" />
            <item name="server" value="@server" />
          </dictionary>
        </data>
      </transform>
    </transforms>
  </method>
</bridge>

The bridge file is converted to C# code by a custom build provider, which is specified in the config file. The C# code is compiled into an assembly by the ASP.NET runtime. Let's examine the code for the bridge file.

  1. The namespace and className attributes in the bridge element specify the namespace and the name of the type that will be created by the bridge build provider.
  2. The partialClassFile attribute specifies the name of the file where a partial implementation of the bridge type is provided. This class should derive from Microsoft.Web.Services.BridgeHandler.
  3. The type attribute of the proxy element specifies the method by which the bridge should access the web service. The Microsoft.Web.Services.BridgeRestProxy indicates that REST will be used to access the web service. If you are using SOAP, then you can specify the name of the proxy class generated by wsdl.exe as the value of this attribute.
  4. The serviceURL specifies the URL at which the web service is available. The string "% appsettings : FlickrRESTEndPoint %" indicates that the value should be obtained from the appsettings configuration section in the web.config file. Here is a snippet from the web.config file:
    XML
    <appSettings>
     <add key="FlickrRESTEndPoint" value="http://www.flickr.com/services/rest/"/>
            ...
    </appSettings>
  5. The parameter elements, which are children of the input element, specify the different querystring parameters. The name and the value attributes are self explanatory. The value true of the serverOnly attribute specifies that that the value for the particular parameter can only be specified from the server side code and cannot be specified from the JavaScript code. The value attribute specified in the XML file is used to indicate the default value of a parameter.
  6. The caching section indicates how the response needs to be cached. The type Microsoft.Web.Services.BridgeCache provides a simple caching mechanism. Any type that implements Microsoft.Web.Services.IBridgeRequestCache can be specified for caching.
  7. The transoforms section specifies how the output should be transformed before returning it to the client. The output from the bridge is returned to the client in JSON format. More than one transform can be specified. A transform is a type that implements the Microsoft.Web.Services.IBridgeResponseTransformer interface.
  8. In the code snippet provided, a transform of type Microsoft.Web.Services.XPathBridgeTransformer is used. The XPath bridge transformer extracts the items from the XML response. An XPath expression that returns a list of nodes is specified in the value attribute of the element named attribute, which has an attribute named name whose value is selector. In our code listing, the XPath expression /rsp/photos/photo selects all the photo elements in the response.
  9. For each of the nodes obtained by matching the XPath expression as explained in 8, values can be further extracted to yield a name value list. This is done in the dictionary section. The item elements specify the entries in the dictionary. The name attribute specifies the name of the entry, and the value attribute specifies the value of the entry. This dictionary is converted to a JavaScript object.

This completes the discussion of the bridge file. Now, let's examine the code-behind file of the bridge file.

C#
namespace FlickrSpell
{
    public partial class FlickrBridge : BridgeHandler
    {
        public override void TransformRequest()
        {
            string letter = this.BridgeRequest.Args["letter"] as string;

            string replacement = letter;

            switch (letter.ToLower())
            {
                //The special case for a and i where
                //the tag to be passed to flickr is different
                case "a":
                    replacement = "aa";
                    break;
                case "i":
                    replacement = "ii";
                    break;
                //If a digit is specified
                //use the onedigit group id    
                case "0":
                case "1": 
                case "2":
                case "3":
                case "4":
                case "5":
                case "6":
                case "7":
                case "8":
                case "9":
                    this.BridgeRequest.Args["group_id"] = "54718308@N00";
                    break;
            };
            
            this.BridgeRequest.Args["letter"] = replacement;
            base.TransformRequest();
        }

        public override void TransformResponse()
        {
            string responseXML = this.ServiceResponse.Response as string;
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(responseXML);
            
            //Check for errors returned by flickr
            string responseStatus = doc.SelectSingleNode("/rsp/@stat").Value;

            if (string.Compare(responseStatus, "ok", true) != 0)
            {
                throw new Exception(doc.SelectSingleNode("/rsp/err/@msg").Value);
            }

            base.TransformResponse();
        }
    }
}

The bridge asbx file and the code-behind class both define the class named FlickrBridge. The class derives from BridgeHandler which contains several overridable methods. The two methods which we override, as shown in the code listing, are TransformRequest and TranformResponse.

The TransformRequest function transforms the request sent by the JavaScript code on the client before sending it to the web service; typically, the implementation of TransformRequest adds or modifies the parameters to be sent to the web service. This is one place where values for the server-only parameters can be supplied. In the code listing above, we are doing two important things:

  1. The letter parameter in the bridge method request is the flickr photo tag. We have seen earlier that the tag for photos representing "A" is actually "aa" and "I" is actually "ii"; therefore, we modify the request parameter value to the appropriate tag value. This is done to simplify the client script.
  2. If the request is for a digit, the value of the group ID, whose default is specified in the asbx file, is changed to that of the onedigit group.

We have seen how the request is modified, now let's see what we do in the TransformResponse method. In the TransformResponse method, we place the common code that checks for errors returned from flickr. flickr indicates success by setting the value of the stat attribute to "ok"; any other value indicates a failure. If an error occurs, then the XML returned by flickr has an element err within the root rsp element. The two attributes of the err element, msg and code, give more information about the error. In the TransformResponse method, we check for an error and create an Exception out of the flickr error. This completes the details of the server-side code, now let's examine the client-side code.

The Client Code

Consider the following HTML:

HTML
<h1>The CodeProject</h1>

This simple HTML has a DOM element node for h1 which has a DOM text node containing the text "The CodeProject". We can break the text node into img elements for each character. The other option is to break the text into span elements and set the background image of each element. The problem with the first approach is that the text will be lost. The problem with the second approach is that the images cannot be resized. We will use a hybrid approach. You can use the AtlasControlToolkit to generate a starter code for an extender control. The AtlasControlToolkit has a wizard to generate an extender control project. You can download the AtlasControlToolkit from this link.

JavaScript
FlickrSpellExtender.FlickrSpellExtenderBehavior = function() {
  FlickrSpellExtender.FlickrSpellExtenderBehavior.initializeBase(this);
       
  this.initialize = function() {
    FlickrSpellExtender.FlickrSpellExtenderBehavior.callBaseMethod(this, 
                           'initialize');
        
    if (!this.control)
       return;
        
    Sys.Net.WebRequestManager.set_enableBatching(true);
        
    var children = this.control.element.childNodes;
      
    for(var i = 0; i < children.length; i++)
    {
        if(children[i].nodeType == 3)
        {
           replaceTextNode(children[i]);
        }
    }
        
    Sys.Net.WebRequestManager.set_enableBatching(false);
  }
    
  function replaceTextNode(node)
  {
     var text = node.data.trim();
     var list = document.createElement("span");
     list.className = "flickr-text";
     list.title = text;
        
     var currentWord = null;
        
     for(var i = 0; i < text.length; i++)
     {
         //TODO: Allow for word breaks            
         var listElem = document.createElement("span");
         listElem.innerText = text.charAt(i);
         list.appendChild(listElem);
         listElemToFlickrImage(listElem);    
     }
        
     node.parentNode.replaceChild(list, node);
  }
    
  function listElemToFlickrImage(listElem)
  {
     var letter = listElem.innerText.charAt(0).toUpperCase();
     
     if (letter >= "A" && letter <="Z" || 
        (letter >= "0" && letter <= "9"))
     {
         FlickrSpellExtender.FlickrBridge.getLetterImages({"letter" : letter}, 
             Function.createDelegate(this, onWebServiceCompleted), 
             null, 
             null, 
             null, 
             listElem);
      }
   }
       
   function onWebServiceCompleted(results, reponse, listElem)
   {
      if (results.length < 1)
         return;
        
      var img = document.createElement("img");
      var i = Math.round(Math.random()*(results.length - 1));

      img.onload = function()
      {
         listElem.insertBefore(img, listElem.firstChild);
         img.title = results[i].title + "\nOwner: " + results[i].owner;
      }

      img.src = String.format("http://static.flickr.com/{0}/{1}_{2}_s.jpg", 
               results[i].server, 
                  results[i].id, 
                  results[i].secret);
   }

Here is how the client-side code works:

  1. The text node of the element is broken into span elements for each character.
  2. For each of the span elements, the web service is invoked to find out the URL of the image.
  3. After the web service returns, an image element is created to hold the image of the letter or digit.
  4. The onload event of the image element is set to a function that adds the image element to the span element.
  5. The src attribute of the image is set to trigger asynchronous loading. Note that we do it after we set the onload event handler. Sometimes, the image gets loaded as soon as the src attribute. This happens when the image is already in the cache.

One other point to note is the feature of batching. We batch all the calls to the web service to reduce the network roundtrips.

Conclusion and Future Enhancements

I am using this control as a part of a bigger application. I will release the application once it is ready. Please post any suggestions on how to improve this control or add features to it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)