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:
<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:
<atlas:AutoCompleteExtender ID="Extender1" runat="server">
<atlas:AutoCompleteProperties TargetControlID="TextBox1" ... />
</atlas:AutoCompleteExtender>
There are two aspects in the extender control definition:
- The extender control tag:
atlas:AutoCompleteExtender
where you specify the runat
attribute and an ID
attribute.
- 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.
The steps to use the flickrSpell extender control in an Atlas project are the following:
- Build the FlickrSpell project which generates the FlickrSpell.dll assembly.
- Copy FlickrSpell.dll and Microsoft.AtlasControlExtender.dll to the bin directory of your web site.
- In your web page, register the tag prefix to use the control, as follows:
<%@ Register Assembly="FlickrSpell" TagPrefix="flickrSpell"
Namespace="FlickrSpell" %>
- 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.
- 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:
<h1 id="Test" runat="server">The CodeProject</h1>
- To extend this server control, add the extender control, as shown:
<flickrSpell:FlickrSpellExtender ID="FE1" runat="server" >
<flickrSpell:FlickrSpellProperties TargetControlID="test" />
</flickrSpell:FlickrSpellExtender>
- Obtain an API key from flickr. Check the Flickr Services website for more details.
- Once you obtain the API key, edit the web.config file of your web site and add the key to the
appsettings
section:
<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:
<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:
<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.
- 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.
- 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
.
- 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.
- 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:
<appSettings>
<add key="FlickrRESTEndPoint" value="http://www.flickr.com/services/rest/"/>
...
</appSettings>
- 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.
- 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.
- 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.
- 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.
- 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.
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())
{
case "a":
replacement = "aa";
break;
case "i":
replacement = "ii";
break;
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);
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:
- 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.
- 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:
<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.
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++)
{
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:
- The text node of the element is broken into
span
elements for each character.
- For each of the
span
elements, the web service is invoked to find out the URL of the image.
- After the web service returns, an image element is created to hold the image of the letter or digit.
- The
onload
event of the image element is set to a function that adds the image element to the span
element.
- 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.