Introduction
A few days ago, one of my clients asked for an Image tagging control which looks like the Facebook Photo Tagging application. That one was built on PHP but my application was based on ASP.NET, so I needed to find some solution with ASP.NET and Ajax. The result is this ImageTag
Extender that allows to tag an image with respective name tags and that can be stored through Ajax enabled Web service. Before we jump in-depth to understand the extender, we will discuss the basics of Ajax Control Toolkit Extenders.
ASP.NET Ajax Extender
Atlas brought the concept of extenders to provide client side behaviors to controls. Extenders attach JavaScript, DHTML and stylesheets to the control they takes as the target control.
The ImageTag Extender
My ImageTag
Extender takes an ASP Image control as the target Control. It also takes few properties such as another image to act as a Save button, a text box which takes the tag name, and a Container
class name for a ASP Panel
control which will contain the TextBox
and the Button
and a <div>
which will target the tag area and be visible when the user clicks on a point to tag.
The ImageTagExtender
declaration will be as follows:
<cc1:ImageTagExtender ID="ImageTagExtender1" runat="server"
TargetButtonID="Image2"
TargetControlID="Image1" TargetTextBox="TextBox1" TextContainerClass="Float">
</cc1:ImageTagExtender>
The code for the Container Panel
is given below:
<asp:Panel ID="Panel1" runat="server" CssClass="Float">
<Div style="border-style:dashed;border-width:medium;
height:50px;width:50px;border-color:Black;z-index:10">
</Div>
<asp:Image ID="Image2" runat="server" Height="32px" ImageUrl="~/BlueHills.jpg"
Width="46px" />
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
</asp:Panel>
The idea of the tag extender is to grab the point where the user clicks on the picture and save that point along with the tag name through a Web service method.
And while loading the image in view mode, load the tagged points through another Web method and place transparent <div>
objects there and also please tag names which will be visible when the user moves the mouse over the <div>
area.
I created the extender using Visual Studio 2008 ASP.NET Ajax Extender Project template because it prepares your web.config with the required declarations. You can also create the extender in Visual Studio 2005.
My Extender
class expects three properties as I mentioned before and it takes an image object as its target, so you have to declare the properties in the ImageTagExtender
class and also define the type of designer and the behavior file for the extender.
[Designer(typeof(ImageTagExtenderDesigner))]
[ClientScriptResource("ImageTagExtender.ImageTagExtenderBehavior",
"ImageTagExtender.ImageTagExtenderBehavior.js")]
[TargetControlType(typeof(Image))]
public class ImageTagExtender : ExtenderControlBase {
The declaration for my properties is as follows:
[ExtenderControlProperty]
[DefaultValue("")]
[IDReferenceProperty(typeof(Image))]
public string TargetButtonID
{
get
{
return GetPropertyValue("TargetButtonID", "");
}
set
{
SetPropertyValue("TargetButtonID", value);
}
}
[ExtenderControlProperty]
[DefaultValue("")]
[IDReferenceProperty(typeof(TextBox))]
public string TargetTextBox
{
get
{
return GetPropertyValue("TargetTextBox", "");
}
set
{
SetPropertyValue("TargetTextBox", value);
}
}
[ExtenderControlProperty]
public string TextContainerClass
{
get
{
return GetPropertyValue("TextContainerClass", "");
}
set
{
SetPropertyValue("TextContainerClass", value);
}
}
The Behavior Class
In the ASP.NET Ajax library, you can define the namespace
for your script class with the registerNamespace
method.
Type.registerNamespace('ImageTagExtender');
The behavior file contains all the client side methods and properties.
The first method of the behavior file is the initializer which intializes all JavaScript objects.
ImageTagExtender.ImageTagExtenderBehavior = function(element) {
ImageTagExtender.ImageTagExtenderBehavior.initializeBase(this, [element]);
this._targetButtonIDValue = null;
this._targetTextBox = null;
this._textContainerClass = null;
}
ImageTagExtender.ImageTagExtenderBehavior.prototype = {
initialize : function() {
ImageTagExtender.ImageTagExtenderBehavior.callBaseMethod(this, 'initialize');
$addHandler(this.get_element(), 'click',
Function.createDelegate(this, this._onclick));
var sButton = $get(this._targetButtonIDValue) ;
$addHandler(sButton,'click',Function.createDelegate(this, this._onbuttonclick));
},
dispose : function() {
ImageTagExtender.ImageTagExtenderBehavior.callBaseMethod(this, 'dispose');
},
Here $addHandler
is a JavaScript shortcut to add handlers to objects dynamically. It takes the object, event name and a handler as parameters.
Next, you have to declare the properties in your script. Here one thing is mandatory, that is to strictly follow the property convention as follows:
(get + _ + PropertyName)
Here are my property declarations:
get_TargetButtonID : function() {
return this._targetButtonIDValue;
},
set_TargetButtonID : function(value) {
this._targetButtonIDValue = value;
},
get_TargetTextBox : function() {
return this._targetTextBox;
},
set_TargetTextBox : function(value) {
this._targetTextBox = value;
},
get_TextContainerClass: function() {
return this._textContainerClass;
},
set_TextContainerClass : function(value) {
this._textContainerClass = value;
},
The onclick
function places the container panel on the point clicked and gets the tag name for that point.
Here is the code for the _onclick
function:
_onclick : function() {
var panel = this._findChildByClass(document,this._textContainerClass);
var e = $get(this._targetButtonIDValue);
var ev = window.event;
var pos = Sys.UI.DomElement.getLocation(panel);
var scrollX = document.body.scrollLeft ||
document.documentElement.scrollLeft;
var scrollY = document.body.scrollTop ||
document.documentElement.scrollTop;
var clickX = window.event.x-pos.x+scrollX;
var clickY = window.event.y- pos.y+scrollY;
Sys.UI.DomElement.setLocation(panel,clickX+pos.x-25,clickY+pos.y-25);
}
Here the function _findChildByClass
returns all the objects with a specified class name. I got this idea of grabbing objects from Omar Al Zabir's Drop Things project.
_findChildByClass : function(item, className)
{
var child = item.firstChild;
while( child != null )
{
if( child.className == className ) return child;
child = child.nextSibling;
}
child = item.firstChild;
while( child != null )
{
var found = this._findChildByClass( child, className );
if( found != null ) return found;
child = child.nextSibling;
}
},
Once I grabbed the floating panel, then I captured the mouse click event for the clicked points. I also deducted the scrollX
and scrollY
in order to be precise about the original point clicked on the image. Currently, this code only supports Internet Explorer 4+, I am working on cross browser aspects. Then I just placed the floating panel on the clicked point.
Saving the Tag
Once the floating panel is placed, then the user provides a tag name in the text box and clicks the Save button. Now my _onbuttonclick
function gets fired to call a Web service to save the positions and the tag name.
_onbuttonclick : function() {
var panel = this._findChildByClass(document,this._textContainerClass);
var pos = Sys.UI.DomElement.getLocation(panel);
var text = $get(this._targetTextBox).value;
var args = new Array(pos.x.toString() ,pos.y.toString(),text);
TaggReciever.TaggReciever.SetTag(args,this._setPosition,this._onerror);
},
Here I grab the position for the floating panel and the text value of the text box and pass them to the Web service method TaggReciever.TaggReciever.SetTag
.
The Ajax library takes a fully qualified name for the service method (i.e. Namespace.ClassName.MethodName
). It takes three parameters:
- Method Parameters array
- Function Succeeded callback
- Function Failed Callback
If the Web method takes a single parameter, then it can be directly passed but if it consumes multiple parameters, then those have to be passed in the form of an array. Next, the Function Succeeded callback and Function Failed callback are the function names that are called at successful and failed asynchronous callbacks respectively.
Here, in success callback function, you will get the return value of the service method. In this example, our service returns back a string
with a description of the tag.
Here is the code for my success callback function _setPosition
:
_setPosition : function(result){
alert(result);
},
I just put an alert with the result value:
Here is the code for the Web method:
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
[System.Web.Script.Services.ScriptService]
[Serializable]
public class TaggReciever : System.Web.Services.WebService
{
[WebMethod]
public string SetTag(string[] args)
{
return string.Format("x:{0},y:{1},Text:{2}", args[0], args[1], args[2]);
}
One important thing over here is that you have to tag the service consumable through Ajax calls by adding a [System.Web.Script.Services.ScriptService]
attribute to the Web service class. Here you can see that the set tag method is just returning the arguments. You can save the values or do whatever you want.
In the failed call back function, you can handle the error returned from the service.
_onerror : function(error){
alert('error');
},
Conclusion
In this article, I discussed about the extender and how to save the tags through service calls. In the next part of this article, I'll discuss about an extender for showing the tags and few more tips about parsing Web service returns from XML or JSON serializer.
History
- 2nd June, 2008: Initial release