Sunday, April 3, 2011

ToolTip AJAX Client Control in Asp.net


Introduction

Many of you might have seen a tooltip as shown in above screenshot. The purpose of this article is to show how we can leverage the Microsoft ASP.NET AJAX framework to develop a general purpose AJAX ToolTip Client Control. You can associate this control to any of the commonly used controls such as textboxes, images etc., to display appropriate tooltips.

Background

Recently, I was exploring the ASP.NET AJAX features, and came across an excellent book: ASP.NET In Action, by Alessandro Gallo. I feel it is an excellent reference for all those who want to learn and know in depth about ASP.NET AJAX. After reading the chapter on developing ASP.NET AJAX Client Controls, I decided to build a general purpose ToolTip control which I had seen on many websites and was curious to build. The rest of the document will explain the actual implementation.
The tooltip control is built using two simple DOM elements <div> and <p>. In the sample presented here, I have associated it to a simple textbox control.

Using the code

The constructor of the control is shown below:
Collapse
// Register namespace.
Type.registerNamespace("AjaxClientComponent");
// Constructor
AjaxClientComponent.ToolTipTextBox = function(element) {
    AjaxClientComponent.ToolTipTextBox.initializeBase(this, [element]);
    // define private variables
    this._container=null;
    this._divElement=null;
    this._pElement=null;
    this._toolTip =null;
    this._cssClass =null;
}
If you look at above JavaScript code, the _container variable is used to initialize it with the container element which contains all the controls with which we can associate the appropriate tooltip. _divElement and _pElement variables are initialised to assign a dynamic <div> tag and a <p> tag created as part of the initialization process. The _toolTip variable is used to initialise the tooltip text to be displayed, and _cssClass is the CSS class which has styles defined for tooltip elements to display and hide, as well as to give the look and feel as shown above.
All the associated properties, methods, and event handlers of this class are initialized using a prototype object of ToolTipTextBox:
Collapse
// Define Methods,properties and event handlers here
AjaxClientComponent.ToolTipTextBox.prototype = { initialize: function() 
{
    AjaxClientComponent.ToolTipTextBox.callBaseMethod(this, 'initialize'); 
    // Add custom initialization here

    //create div and p tag dynamically
    this._divElement = document.createElement('div');
    this._pElement = document.createElement('p'); 
    this._pElement.innerHTML =this._toolTip;

    // assign css class the  tag
    Sys.UI.DomElement.addCssClass(this._pElement,'tipText');

    // Hide the tooltip
     this._cssClass ="hidenotes";
    Sys.UI.DomElement.addCssClass(this._divElement,this._cssClass);
        
     this._divElement.appendChild(this._pElement);
    // add the 
element to the container $get(this._container).appendChild(this._divElement); // add event handlers here $addHandlers(this.get_element(), { mousemove:this._onMouseover, mouseout:this._onMouseout }, this); }, // Dispose method dispose: function() { //Add custom dispose actions here $clearHandlers(this.get_element()); AjaxClientComponent.ToolTipTextBox.callBaseMethod(this, 'dispose'); }, // Define all event handlers here // MouseMovw Event handler _onMouseover:function(evt){ if(this._divElement!=null){ // Show the tooltip by changing the class of div element. if(this._cssClass=='hidenotes'){ Sys.UI.DomElement.removeCssClass(this._divElement,this._cssClass); this._cssClass='notes'; Sys.UI.DomElement.addCssClass(this._divElement,this._cssClass); } this._divElement.style.left=evt.clientX; this._divElement.style.top=evt.clientY+5; }, // Mouseout Event handler _onMouseout:function(evt){ if(this._divElement!=null) { if(this._cssClass!=null) { Sys.UI.DomElement.removeCssClass(this._divElement,this._cssClass); this._cssClass='hidenotes'; Sys.UI.DomElement.addCssClass(this._divElement,this._cssClass); } } }, //properties // Define set and get methods for initialising tooltip text get_toolTip: function() { return this._toolTip; }, set_toolTip: function(value) { this._toolTip=value; }, // get and set methods for container element get_container:function() { return this._container; }, set_container:function(value) { this._container=value; } }
If you look at above code snippet, the initialize() method creates the <div> and <p> tags dynamically, and assigns the CSS class to them, and then adds them as children of the container element. It adds the event handlers for the mousemove and mouseout events of the element with which the tooltip will be associated with. Two CSS classes notes and hidenotes show and hide the tooltip, respectively.
The _onMousemove() method shows the tooltip by assigning the notes CSS class to _divElement, and it also initializes the left and top properties based on the mouse client coordinates passed as event argument to the evt object.
The _onMouseout() method hides the tooltip by assigning the hidenotes CSS class to _divElement.
Rest of the properties are self explanatory.
Now, let us see the code from the default.aspx file to see how the above control is invoked.
Collapse

Credit Card Validator control for ASP.NET

Sample Image

Introduction

A while ago I started working on converting an eCommerce payment gateway's (DataCash) COM server to a native .NET assembly using their XML API. Once I had got a basic version working I decided to produce a simple web form to test it out, and so opened it up for all comers (and received some very generous donations from CP members -- thanks guys :). As part of this web form I wanted to include support to check that users had entered a card number, expiration date etc., and then wanted to extend it further to include support for checking that the card number was valid before issuing a request to the payment gateway's server. This is the result, a drop-in replacement for any of the other validation controls.
Incidentally, you can see a demo of the validator in use (as well as the card payment gateway assembly) at the following address: https://ssl500.securepod.com/oobaloo/DataCash/, besides this you may also be interested in the everything you ever wanted to know about CC's guide.
Before getting into any of the implementation details here is a simple UML class diagram to show the rough layout of the Control.
The diagram is missing information about parameter types since its not essential to understanding the model. For those who are not familiar with UML, it shows a specialisation relationship between the BaseValidator and CreditCardValidator classes - an is a relationship - demonstrating inheritance from BaseValidator to the more specialised CreditCardValidator class. New with the third incarnation of the control is the AcceptedCardTypes property which is used to specify what types of card should pass the validation using the CardType enumeration.
The control includes support for validating card numbers in two ways. Firstly, through checking the card number using Luhn's formula, the details of which are included in the next part of the article. Secondly, the card type itself is examined, and the length is checked. The card type can be determined through a prefix and each type has a specified length, by examining these an additional level of control can be added - the types of card to accept. The method that implements this is IsValidCardType, and whether this is used during the validation is set by the ValidateCardType property.
The main way the card number is going to be validated is through Luhn's formula, so firstly a little bit of background information and a demo of how the validation is performed.

Luhn's Formula

The CreditCardValidator control will perform a check on the contents of the textbox using Luhn's formula which is used to validate card numbers. It can be used to check against a number of cards, including the following:
  • MasterCard
  • VISA
  • American Express
  • Diners Club/Carte Blanche
  • enRoute
  • Discover
  • JCB
  • Solo*
  • Switch*
* These are UK only cards from my recollection, but have been tested by myself and work.
Information on the history of the formula can be found on WebOpedia but so you don't have to read that here is a summary of how it is performed:
  1. Double the value of alternating digits
    The first step is to double each of the alternating digits in the number. But the trick is to start with the second digit from the right and work backwards. Say we have a credit card number 1234 5678 1234 5670. We'll start with the rightmost number 7, double it, and then do the same for every other digit. 1234 5678 1234 5670

    This will give us the following values.

    7 x 2 = 14
    5 x 2 = 10
    3 x 2 = 6
    .
    .
    etc.
  2. Add the separate digits of all the products
    Now we'll the separate digits of all the products, and come up with a final sum. (1 + 4) + (1 + 0) + 6 + 2 + (1 + 4) + (1 + 0) + 6 + 2 = 28
    Be sure to add the digits, not just the number.
  3. Add the unaffected digits
    Now we'll go back to the original number and add all the digits that we didn't double. We'll still start from the right, but this time we'll start from the rightmost number. 1234 5678 1234 5670
    0 + 6 + 4 + 2 + 8 + 6 + 4 + 2 = 32
  4. Add the results and divide by 10
    Finally, we'll add both the results and divide the answer by 10.
    28 + 32 = 60

    60 is evenly divided by 10, so the credit card number is well formed and ready for further processing.
This will be converted into a method which will perform all of the steps listed above on the contents of the specified textbox. By deriving the new validator control from BaseValidator it's possible to produce a control which behaves exactly as any other validator for the easiest deployment.

Luhn's implementation

The code for Luhn's formula is in the ValidateCardNumber method which is implemented as follows:
Collapse
private static bool ValidateCardNumber( string cardNumber )
{
    try 
    {
        // Array to contain individual numbers
        System.Collections.ArrayList CheckNumbers = new ArrayList();
        // So, get length of card
        int CardLength = cardNumber.Length;
    
        // Double the value of alternate digits, starting with the second digit
        // from the right, i.e. back to front.
        // Loop through starting at the end
        for (int i = CardLength-2; i >= 0; i = i - 2)
        {
            // Now read the contents at each index, this
            // can then be stored as an array of integers

            // Double the number returned
            CheckNumbers.Add( Int32.Parse(cardNumber[i].ToString())*2 );
        }

        int CheckSum = 0;    // Will hold the total sum of all checksum digits
            
        // Second stage, add separate digits of all products
        for (int iCount = 0; iCount <= CheckNumbers.Count-1; iCount++)
        {
            int _count = 0;    // will hold the sum of the digits

            // determine if current number has more than one digit
            if ((int)CheckNumbers[iCount] > 9)
            {
                int _numLength = ((int)CheckNumbers[iCount]).ToString().Length;
                // add count to each digit
                for (int x = 0; x < _numLength; x++)
                {
                    _count = _count + Int32.Parse( 
                          ((int)CheckNumbers[iCount]).ToString()[x].ToString() );
                }
            }
            else
            {
                // single digit, just add it by itself
                _count = (int)CheckNumbers[iCount];   
            }
            CheckSum = CheckSum + _count;    // add sum to the total sum
        }
        // Stage 3, add the unaffected digits
        // Add all the digits that we didn't double still starting from the
        // right but this time we'll start from the rightmost number with 
        // alternating digits
        int OriginalSum = 0;
        for (int y = CardLength-1; y >= 0; y = y - 2)
        {
            OriginalSum = OriginalSum + Int32.Parse(cardNumber[y].ToString());
        }

        // Perform the final calculation, if the sum Mod 10 results in 0 then
        // it's valid, otherwise its false.
        return (((OriginalSum+CheckSum)%10)==0);
    }
    catch
    {
        return false;
    }
}
The code includes comments that explains how it works, however, here is a summary.
  1. Build an ArrayList which will contain the alternating digits taken in step 1. This is so that the original values can be used again in step 2 but without looping back through the number. This was primarily done for readability.
  2. Once the list has been created, a sum is created of the individual digits if the number is greater than 9 (i.e. has more than one digit).
  3. The original digits that were untouched are added together, these create the OriginalSum variable. This is then added to the number created as a result of steps 1 and 2, and the value is divided by 10 and the result tested against 0 which provides the return value for the function.
If an exception is thrown throughout the code, then false is returned.

Card Type Validation

Each of the card types mentioned above can be tested for a given length based upon the numerical prefix. The prefixes and lengths are in the table below:
Card Type Prefix Number Length
MasterCard 51-55 16
VISA 4 13 or 16
American Express 34 or 37 15
Diners Club/Carte Blanche 300-305,36,38 14
enRoute 2014,2149 15
Discover 6011 16
JCB 3 16
JCB 2131,1800 15
These types can be put into an enumeration. This will allow us to include a property that users can set specifying which types to accept, and then test the property during the validation to determine which types should be accepted.
Collapse
[Flags, Serializable]
    public enum CardType
    {
        MasterCard    = 0x0001,
        VISA        = 0x0002,
        Amex        = 0x0004,
        DinersClub    = 0x0008,
        enRoute        = 0x0010,
        Discover    = 0x0020,
        JCB            = 0x0040,
        Unknown        = 0x0080,
        All            = CardType.Amex | CardType.DinersClub | 
                         CardType.Discover | CardType.Discover |
                         CardType.enRoute | CardType.JCB | 
                         CardType.MasterCard | CardType.VISA
    }
The CardType (which is an instance of an Int32-based enumerated type) will be used as a set of bit flags - each bit reflects a single card type. So, 0...0001 is MasterCard, 0...0010 is VISA. By using a set of bit flags it will be possible to set a variable to more than one card type, and be able to determine which ones are to be supported.
This card type check is to be performed alongside the length check (ensuring that the card number matches the card type's expected length) and for this check we will use a Regular Expression using the .NET Framework's Regex class. Regular Expressions let you perform pattern matches, and can be extremely powerful. For more details on Regular Expressions take a look at .NET Framework Regular Expressions on MSDN, and if you only want to include this kind of validation you can use the Regular Expression validation control.
The card type check also includes support for the end user to specify which card types should pass the validation, this is set through the AcceptedCardTypes property (and then stored in the _cardTypes member variable). The code looks like this:
Collapse
public bool IsValidCardType( string cardNumber )
    {    
    // AMEX -- 34 or 37 -- 15 length
    if ( (Regex.IsMatch(cardNumber,"^(34|37)")) 
         && ((_cardTypes & CardType.Amex)!=0) )
        return (15==cardNumber.Length);
            
    // MasterCard -- 51 through 55 -- 16 length
    else if ( (Regex.IsMatch(cardNumber,"^(51|52|53|54|55)")) && 
              ((_cardTypes & CardType.MasterCard)!=0) )
        return (16==cardNumber.Length);

    // VISA -- 4 -- 13 and 16 length
    else if ( (Regex.IsMatch(cardNumber,"^(4)")) && 
              ((_cardTypes & CardType.VISA)!=0) )
        return (13==cardNumber.Length||16==cardNumber.Length);

    // Diners Club -- 300-305, 36 or 38 -- 14 length
    else if ( (Regex.IsMatch(cardNumber,"^(300|301|302|303|304|305|36|38)")) && 
              ((_cardTypes & CardType.DinersClub)!=0) )
        return (14==cardNumber.Length);

    // enRoute -- 2014,2149 -- 15 length
    else if ( (Regex.IsMatch(cardNumber,"^(2014|2149)")) && 
              ((_cardTypes & CardType.DinersClub)!=0) )
        return (15==cardNumber.Length);

    // Discover -- 6011 -- 16 length
    else if ( (Regex.IsMatch(cardNumber,"^(6011)")) &&
             ((_cardTypes & CardType.Discover)!=0) )
        return (16==cardNumber.Length);

    // JCB -- 3 -- 16 length
    else if ( (Regex.IsMatch(cardNumber,"^(3)")) && 
             ((_cardTypes & CardType.JCB)!=0) )
        return (16==cardNumber.Length);

    // JCB -- 2131, 1800 -- 15 length
    else if ( (Regex.IsMatch(cardNumber,"^(2131|1800)")) && 
               ((_cardTypes & CardType.JCB)!=0) )
        return (15==cardNumber.Length);
    else
    {
        // Card type wasn't recognised, provided Unknown is in the 
        // CardTypes property, then return true, otherwise return false.
        if ( (_cardTypes & CardType.Unknown)!=0 )
            return true;
        else
            return false;
    }
}
It's not the prettiest of code, but it effectively performs a RegEx comparison on the cardNumber for each possible card type. The regular expression is very simple, and searches for any of the numbers separated by the pipe character. So, for the AMEX type it searches for either 34 or 37. Because the string is prefixed by the epsilon (^) character, the search is performed at the start of the cardNumber. This search is performed through the IsMatch static method of the Regex class.
A further test is also done at the same time to determine whether or not the card's type exists in the AcceptedCardTypes property:
Collapse
&& ((_cardTypes & CardType.Amex)!=0)
Provided both the tests return true (i.e. the prefix is matched, and the card type exists in the _cardTypes member variable) a length test is performed and provided the card number is a valid length then the IsValidCardType method will return true.
If the card type is not recognised then as long as Unknown has been set in _cardTypes then IsValidCardType will return true - since the user will have specified that an Unknown card type is an accepted card type. Otherwise, it will return false and will fail.
The _cardTypes variable is set using a property accessor which is implemented as follows:
Collapse
public string AcceptedCardTypes
{
    get
    {
        return _cardTypes.ToString();
    }
    set
    {
        _cardTypes = (Etier.CardType)
                  Enum.Parse(typeof(Etier.CardType), value, false );
    }
}
This enables users to specify the card types using a string (e.g. "Amex, VISA, Unknown"), as opposed to having to programmatically set it via. the OnLoad event (e.g. AcceptedCardTypes = CardType.VISA | CardType.Amex etc.)
That's the second validation method implemented, all that's left to do is to produce the BaseValidator derived class that uses the above methods to validate the text in the associated control.

The Validation Control's implementation

That is the majority of the code written, all that's left to do is produce a class dervied from System.Web.UI.WebControls.BaseValidator and override the necessary functions. Incidentally, I used Cenk Civici's article for a "ListControl SelectedItem Validator" as the main source for this.
The BaseValidator requires that we override the EvaluateIsValid method, which suprisingly enough, is the function that determines whether or not the associated control has valid content -- in our case whether or not the associated text box has a valid credit card number entered. Following Cenk Cevi's article I also included an implementation of the ControlPropertiesValid helper function that determines whether the control specified by the ControlToValidate property is a valid control - thus ensuring that it is a textbox we're checking. Since most controls have a text property it's probably not a major issue but it would be strange to be validating the text property of a button etc., so as an extra precaution I included it.

Firstly, ControlPropertiesValid

Collapse
protected override bool ControlPropertiesValid()
    {
        // Should have a text box control to check
        Control ctrl = FindControl(ControlToValidate);
        
        if ( null != ctrl )
        {
            if (ctrl is System.Web.UI.WebControls.TextBox)
            {
                _creditCardTextBox = (System.Web.UI.WebControls.TextBox) ctrl;
                return ( null != _creditCardTextBox );
            } else
                return false;
        }
        else
            return false;
    }
The code first finds the ControlToValidate and checks that it does indeed point to something, and then checks whether it is a TextBox. If so, it sets the member variable _creditCardTextBox to the TextBox on the web form. If anything bad happens it returns false.

Finally, EvaluateIsValid

This method is declared as abstract in the BaseValidator class, and so has to be implemented by our derived class. It is also the method that is called to check that the contents of the associated control is valid.
The CreditCardValidator control includes two additional properties, one of which is ValidateCardType which can be used to set whether or not the card type should also be checked. If it is to be checked, then the length is checked before the number is evaluated against Luhn's formula. However, if the ValidateCardType property is set to false then the card number is validated against Luhn's formula directly.
Collapse
protected override bool EvaluateIsValid()
{
    if (_validateCardType)    // should the length be validated also?
    {
        // Check the length, if the length is fine then validate the
        // card number
        if (IsValidCardType(_creditCardTextBox.Text))
            return ValidateCardNumber( _creditCardTextBox.Text );
        else
            return false;        // Invalid length
    }
    else
        // Check that the text box contains a valid number using 
        // the ValidateCardNumber method
        return ValidateCardNumber( _creditCardTextBox.Text );
}
Provided the ValidateCardNumber method succeeds, the validation is considered a success.

Using the Credit Card Validator Control

That's all that's necessary to have the CreditCardValidator control finished. Now for a quick example of how it can be used in a real web form (the full code for this is included as a download at the top of the page).
The first that needs to be done is to include the declaration at the top of the aspx page that imports the assembly and maps the namespace to a prefix. This also requires that the assembly's DLL file is copied to the bin directory for the application. The location of this directory depends upon the setup of your application, but assuming there's a virtual directory off the root called /CreditCard/ then your bin directory would be /CreditCard/bin/.
Collapse
<%@ Register TagPrefix="etier" Namespace="Etier" Assembly="CreditCardValidator" %>
This allows you to then add the control to the page in the form of <etier:CreditCardValidator... For example, the following code would bind the validator control to a TextBox called CardNumber:
Collapse
<etier:CreditCardValidator
  Id="MyValidator"
  ControlToValidate="CardNumber"
  ErrorMessage="Please enter a valid credit card number"
  Display="none"
  RunAt="server"
  EnableClientScript="False"
  ValidateCardType="True"
  AcceptedCardTypes="Amex, VISA, MasterCard"
/>
The CreditCardValidator offers a few properties that are not taken from the BaseValidator base type, these are ValidateCardType and AcceptedCardTypes. The ValidateCardType property sets whether or not the card type should be checked. This will mean that a length test is performed, as well as specifying which card types should be accepted. The AcceptedCardTypes property can be set using the CardType enumeration. In the above code the accepted types are Amex, VISA, and MasterCard. If unrecognised card types are also to be accepted then you can include "Unknown" in the list. If you want all the recognised types accepted then you can use "All". Thus, to accept anything you should use "Unknown, All".
Since I chose to have the error message displayed in a ValidationSummary control, I set the Display property to none to ensure that it wasn't displayed in-line. Otherwise, the error message would be put at the location of this control.

Conclusion

ASP.NET provides a huge amount of functionality built in, and I can't remember how many times I've produced custom code to do the very thing that Validation controls provide. This, coupled with the Object-Oriented nature of .NET, means that you can extend the existing .NET offerings into something you can use time and again. Now that xcopy deployment is a real option with web applications, dropping in support for your own custom validation controls can be done in a fraction of the time than with traditional ASP development.
This concludes my second article, and I hope its of use to people. It doesn't really show anything new, but does roll up some important functionality into a re-usable control that can be deployed and used extremely easily.

Things that are missing/Ideas for improvement

At the moment, all validation is taken care of on the server-side. This is fine since the aim was to prevent requests being issued to the payment gateway with incorrect details - especially since there's likely to be a delay of a few seconds. However, since the validation algorithm is not a secret and is relatively simple to implement, a client side version could be implemented to check the value before the data is posted back to the form and thus saving round-trips.
If anyone decides to extend this to include client side scripting, or has any comments or questions then it'd be great to hear from you.

Slide-Show User Control in Asp.net

Preview.gif

Introduction

In this article, I’m going to present an implementation of an ASP.NET slide show user control. It is an HTML table based slide show. It is a very simple user control and can easily be modified depending upon your needs.

Background

A few months ago, I was searching the Internet for a JavaScript slide show that could easily be applied to a DataList control. I found some good JavaScript slide show samples, but all of them were div based and not HTML table based. Also, the JavaScript was very complicated and very difficult to grasp. So, I decided to develop my own HTML table based JavaScript slide show. Later, I converted it to a User Control so that it could easily be modified and reused depending upon future needs.

About the User Control

At runtime, the slide show user control has the following appearance:
Slide_Show.png
The upper part contains the title of the slide show while the lower part contains the left and right arrows (hyperlinks). The middle part consists of the necessary images for the slide show. When we put the mouse pointer over the left arrow, the images start to move from right to left, and when we put the mouse pointer over the right arrow, the images start to move from left to right. When we remove the mouse pointer from the left or right arrow, the slide show doesn’t stop immediately. The movement of the current image first completes, and then the slide show stops. The speed of the slide show differs from browser to browser in this user control.

Properties of the User Control

The slide show user control has the following public properties:
  • NoOfVisibleImages: The number of visible images.
  • ImageHeight: The height of the image.
  • ImageWidth: The width of the image.
  • ImageDataSource: The image item collection.
  • RightImage: Path of the right arrow image.
  • LeftArrowToolTip: Tool tip for the left arrow image.
  • RightArrowToolTip: Tool tip for the right arrow image.
  • Title: Title for the user control.
  • ArrowAlign: Horizontal alignment for the navigation arrows.
  • TitleAlign: Horizontal alignment for the user control title.
  • SlideShowSpeed: Speed of the slide show in milliseconds. The default value for it is 10 milliseconds.
  • EnableImageClick: Specifies whether to enable the Click event or not. The default value for it is false.

Events of the User Control

The slide show user control has only one server-side event:
  • Click: Fires when an image of the SlideShow is clicked.

Using the User Control

Drag and drop the user control into an ASPX page and set the values for its properties. The HTML code for this user control looks like:
Collapse
<uc1:SlideShow ID="SlideShow1" runat="server" NoOfVisibleImages="4" 
     ImageHeight="71" ImageWidth="95" LeftImage="~/ArrowImages/back[1].gif" 
     RightImage="~/ArrowImages/next[1].gif" Title="Image Slide Show"
     ArrowAlign="Left" TitleAlign="Left" />
Now, bind the slide show user control to an image data source of type ImageItems in the Page_Load event:
Collapse
SlideShow1.ImageDataSource = GetImageItems();
where the method GetImageItems returns an ImageItems collection. The code for the method GetImageItems is given below:
Collapse
private ImageItems GetImageItems()
{
   ImageItems oImageItems = new ImageItems();
   ImageItem oImageItem = null;

   FileInfo[] Images = new DirectoryInfo(Server.MapPath("Images")).GetFiles("*.*");

   foreach (FileInfo Image in Images)
   {
      oImageItem = new ImageItem();
      oImageItem.URL = string.Format("~/Images/{0}", Image.Name);
      oImageItem.ToolTip = Image.Name;
      oImageItems.Add(oImageItem);
   }

   return oImageItems;
}
You can also handle the Click event of the slide show user control. For this, first you have to enable the Click event as:
Collapse
< ... EnableImageClick="true" ... >
Then, create an event handler for the Click event:
Collapse
protected void SlideShow1_Click(object sender, SlideShowImageEventArgs e)
{
   ...
}
Next, wire up the event handler in the control tag by adding the prefix On in front of the event name:
Collapse
< ... OnClick="SlideShow1_Click" ... > 
I’ll discuss about the SlideShowImageEventArgs class later.

User Control HTML

The HTML of this UserControl is very simple.
Collapse
<asp:Panel ID="pnlParent" runat="server">
   <asp:Panel ID="pnlTitle" runat="server" HorizontalAlign="Left">      
      <asp:Label ID="lblTitle" runat="server" Text="Slide Show" 
           Font-Bold="True" Font-Names="Garamond" ForeColor="#404040">
      </asp:Label>
   </asp:Panel>
   <asp:Panel ID="pnlBase" runat="server">
      <table id="tblBase" border="0" cellpadding="0" cellspacing="0" 
             runat="server" style="position:absolute; left: 0px;">
         <tr>
            <td>
               <asp:DataList ID="dataList" runat="server" GridLines="Vertical" 
                    RepeatDirection="Horizontal" ShowHeader="False" 
                    CellPadding="0" OnItemCreated="dataList_ItemCreated"
                    ShowFooter="False">
                  <ItemTemplate>
                     <asp:ImageButton ID="imgSlideShow" alt="" ImageUrl='<%# Eval("URL") %>' 
                                      ToolTip='<%# Eval("ToolTip") %>' runat="server" />
                  </ItemTemplate>
                  <ItemStyle HorizontalAlign="Center" VerticalAlign="Middle" />
               </asp:DataList>
            </td>
         </tr>
      </table>
    </asp:Panel>
    <asp:Panel ID="pnlNavigation" runat="server" BackColor="#E0E0E0">
       <asp:HyperLink ID="aLeft" runat="server"></asp:HyperLink>
       <asp:HyperLink ID="aRight" runat="server"></asp:HyperLink>
    </asp:Panel>
</asp:Panel> 
There are three Panels in this UserControl inside a parent Panel [ID="pnlParent"]. The first Panel [ID="pnlTitle"] has been used to display the title of the user control. It contains a Label control for this purpose.
The second Panel [ID="pnlBase"] has been used to display the images of the slide show. It contains an HTML table [id="tblBase"] with position equal to absolute [style="position: absolute;”]. Here, position equal to absolute has been used so that the position of the HTML table within the container Panel [ID="pnlBase"] can be changed at runtime using JavaScript to create a moving effect. This HTML table holds a DataList server control with RepeatDirection="Horizontal". Actually, this DataList [ID="dataList"] has been used here in order to bind images to the user control. An ImageButton server control [ID="imgSlideShow"] has been put inside the ItemTemplate for this purpose. Data-binding expressions have been used to set the ImageUrl and ToolTip properties of the Image server control.
The third Panel [ID="pnlNavigation"] has been taken to display the left and right arrows. These left and right arrows are responsible for the motion of the images from right to left or from left to right. Two HyperLink controls [ID="aLeft" and ID="aRight"] have been put inside this Panel for this purpose.

User Control Code

The SlideShow class is derived from the System.Web.UI.UserControl class.
Collapse
public partial class SlideShow : System.Web.UI.UserControl
{
   ...
}
The Height and Width of each image has been set in the ItemCreated event of the DataList:
Collapse
if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
{
   ImageButton imgSlideShow = (ImageButton)e.Item.FindControl("imgSlideShow");
   imgSlideShow.Height = Unit.Pixel(_ImageHeight);
   imgSlideShow.Width = Unit.Pixel(_ImageWidth);
}
There are two private methods: GetCSS and GetJS in the SlideShow class that emit the necessary JavaScript and CSS for this UserControl at client side.
Collapse
private string GetJS()
{
   StringBuilder JS = new StringBuilder();
   JS.Append("");
   return JS.ToString();
}

private string GetCSS ()
{
   StringBuilder CSS= new StringBuilder();
   CSS.Append("");
   return CSS.ToString();
}
The CSS has been attached to the page’s header section in the Page_Load event of the UserControl as:
Collapse
//Adding a stylesheet to the page header
((HtmlHead)this.Page.Header).Controls.Add(new LiteralControl(GetCSS()));
Note that in order to attach the necessary CSS through code, the page’s header section should be marked as runat=server as:
Collapse
<head runat="server">
The JavaScript has been registered to the page in the Page_Load event of the User Control as:
Collapse
//Register client side script.
this.Page.ClientScript.RegisterStartupScript(typeof(Page), "MyScript", GetJS());
Client side mouseover and mouseout events also have been attached to the left and right HyperLink controls in the Page_Load event of the User Control as:
Collapse
aLeft.Attributes["onmouseover"] = "javascript:Start();MoveLeft();";
aLeft.Attributes["onmouseout"] = "javascript:Stop();StopMoveLeft();";
aRight.Attributes["onmouseover"] = "javascript:Start();MoveRight();";
aRight.Attributes["onmouseout"] = "javascript:Stop();StopMoveRight();";
The slide show user control has following definition for its Click event:
Collapse
public event SlideShowClick Click;
The delegate that represents the Click event has the following signature:
Collapse
public delegate void SlideShowClick(object sender, SlideShowImageEventArgs e); 
The Click event for this user control has been fired in the click event of the ImageButton as:
Collapse
protected void imgSlideShow_Click(object sender, ImageClickEventArgs e)
{
   // Fire the event.
   if (Click != null)
      Click(this, new SlideShowImageEventArgs((ImageButton)sender, e.X, e.Y));
}
The Click event has been attached to each of the ImageButton control in the ItemCreated event of the DataList in the following manner:
Collapse
if (_EnableImageClick)
   imgSlideShow.Click += new ImageClickEventHandler(imgSlideShow_Click);
else
   imgSlideShow.Attributes["onclick"] = "javascript:return false;";
Note that if EnableImageClick is false, then post back is been prevented through JavaScript. One more important thing: put the following line of the code in the system.web section of the web.config file if you want to handle the Click event of the slide show user control, i.e., if EnableImageClick is true:
Collapse
<pages enableEventValidation="false"></pages>
If you don’t add this line of the code, then you will face the following error:
error.png
I’ve tried to handle it through code, but couldn’t. If any one has ideas about it, then please let me know.
SlideShowImageEventArgs is an event argument class for this user control and has some basic information about the Image to be clicked. The definition of this class is given below:
Collapse
public class SlideShowImageEventArgs : EventArgs
{
    private ImageButton _ImageButton = null;
    private int _X = 0;
    private int _Y = 0;

    public int X
    {
        get { return _X; }
    }

    public int Y
    {
        get { return _Y; }
    }

    public string URL
    {
        get { return _ImageButton.ImageUrl; }
    }

    public string ToolTip
    {
        get { return _ImageButton.ToolTip; }
    }

    public SlideShowImageEventArgs(ImageButton O, int X, int Y)
    {
        _ImageButton = O;
        _X = X;
        _Y = Y;
    }
}

Images Source

The slide show user control has a public property, ImageDataSource. This is used to bind image items to the user control for the slide show. The ImageDataSource property accepts a collection of image items of type ImageItems as the image data source. ImageItems is a generic collection class inherited from the List<ImageItem> class. The definitions for the ImageItems and ImageItem classes are given below:
Collapse
public class ImageItems : List
{
   public ImageItems() { }
}

public class ImageItem
{
   private string _ToolTip = string.Empty;
   public string ToolTip
   {
      get { return _ToolTip; }
      set { _ToolTip = value; }
   }

   private string _URL = string.Empty;
   public string URL
   {
      get { return _URL; }
      set { _URL = value; }
   }

   // Default constructor.
   public ImageItem() { }

   public ImageItem(string ToolTip, string URL)
   {
      this._ToolTip = ToolTip;
      this._URL = URL;
   }
}

JavaScript Methods

There are six JavaScript methods that are responsible for performing the slide show on the client side, namely: Start, Stop, MoveLeft, StopMoveLeft, MoveRight, and StopMoveRight. As soon as we put the mouse pointer over the left or right HyperLnk, the onmouseover event gets fired. The Start method is first invoked and it sets a global flag equal to false [IsPaused = false;] indicating that the slide show is about to start. After that, the MoveLeft or MoveRight method is invoked. These methods are responsible for moving the images from right to left or left to right for the slide show. When we remove the mouse pointer from the left or right HyperLnk, the onmouseout event is fired. The Stop method is first invoked and it sets a global flag equal to true [IsPaused = true;] indicating that the slide show is about to stop. After that, the StopMoveLeft or StopMoveRight method is invoked. These methods don’t immediately stop the slide show. Rather, these methods first complete the movements of the current image from right to left or left to right, then stops the slide show.

Conclusion

So, this is my approach to implement an ASP.NET slide show using the DataList. I have tried my best to keep it bug free. I will most welcome suggestions and criticism for further improvements of this user control. I have tested this user control on various browsers and it works fine. A list of supported browsers along with versions is given below:
Browsers.png

ASP.NET Slideshow Control with jQuery and XML

Introduction

For the past few years, image sliders, slide-shows, and ad rotator scripts have become increasingly popular for web pages. There are thousands of these scripts on the Web, but it was difficult to find one that fit my requirements. I wanted a script that is flexible enough for me to use as an ad rotator, slide-show, or image rotator with navigation controls and that extracts the slides information from an XML file. After spending some time researching for it, I found the jQuery based slideshow with navigation controls from Dynamic Drive and the article Reading XML with jQuery. I had put together these findings and a brief tutorial "jQuery slideshow with XML". Recently, I decided to encapsulate this into ASP.NET User Controls to increase the reusability of code. Now, I can have multiple instances of this control on my page, and I can customize the width and height, options to hide or show navigation controls, XML file source, and other settings for each instance. I have put together a step by step tutorial on how I have accomplished this.
Figure 1
Sample results

Check list

Getting started

Before we begin, make sure to download a copy of the Simple Controls Gallery v1.3 from Dynamic Drive, Reading XML with JQuery, and the latest version of the jQuery JavaScript library. Here is the structure of my project. You are welcome to download this demo.
Figure 2
Project Structure

Putting everything together

SlideShow.ascx.cs

First, add a Web User Control to the project and create public properties and methods to allow interaction with the control at design-time or programmatically.
Listing 1

//slide divpublic string WrapperID
{
    get { return this.ClientID + "Div"; }
}
WrapperID will return the unique ID of the User Control. Every time we drop an ASP.NET User Control on to a page, it will automatically include an ID property that uniquely identifies the control. The ID value is the combination of the tag name and a number. If the tag name of the User Control is "SlideShow", WrapperID will return SlideShow1Div, SlideShow2Div …SlideShowNDiv.
Listing 2
Collapse
private int _width =728;
[DefaultValue(728)]
public int Width
{
    set { this._width = value; } get { return this._width; }
}
//height of the slideprivate int _height = 95;
[DefaultValue(95)]
public int Height
{
    set { this._height = value; }  get { return this._height; }
}
Add the Width and Height properties to allow us to adjust the height and width of the div control.
Listing 3

// autoplay true|false private bool _autoPlay = true;
[DefaultValue(true)]
public bool AutoPlay
{
    get { return this._autoPlay; }  set { this._autoPlay = value; }
}

// Show Navigation Control true|falseprivate bool _showNavigation = true;
[DefaultValue(true)]
public bool ShowNavigation
{
    get { return this._showNavigation; } set{ this._showNavigation = value; }
}

private int _delay_btw_slide = 10000;
/// delay between slide in miliseconds [DefaultValue(10000)]
public int Delay_btw_slide
{
    set { this._delay_btw_slide = value; }   get { return this._delay_btw_slide; }
}

private int _fadeDuration = 2000;
/// transition duration (milliseconds) [DefaultValue(2000)]
public int FadeDuration
{
    set { this._fadeDuration = value; }  get { return this._fadeDuration; }
}
private int _cycles_before_stopping = 99;
/// cycles befote stopping[DefaultValue(99)]
public int Cycles_before_stopping
{
    set { this._cycles_before_stopping = value; } 
    get { return this._cycles_before_stopping; }
}

// previous buttonprivate string _btnPrevious = "~/images/previous.gif";
[Category("Appearance"), Localizable(true)]
[Description("Previous button"), 
 DefaultValue("~/images/previous.gif"), Bindable(true)]
[Editor("System.Web.UI.Design.ImageUrlEditor, 
         System.Design, Version=2.0.0.0, Culture=neutral, 
PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]    [UrlProperty]
public string BtnPrevious
{
    get { return this._btnPrevious; } set { this._btnPrevious = value;}
}

// Next button// Play button // Pause button
  • AutoPlay - true = start the slide show automatically without pressing the Play button and vice versa.
  • ShowNavigation - true = show the navigation control, or false = hide the navigation control.
  • Delay_btw_slide - get or set the delay between each image slide.
  • FadeDuration - get or set the fade duration between each slide.
  • Cycles_before_stopping - get or set the number of rotations before stopping the rotation. If we set 99, it will rotate 99 times and stop until we hit the Play button again.
  • BtnPrevious, BtnNext, BtnPlay, BtnPause - get or set the image for the Previous, Next, Play, and Pause buttons, respectively. These properties accept relative URLs or absolute URL paths to the button images. Make sure to place the images folder at the root directory of the application if you are using relative URLs.
Listing 4

//xml fileprivate string _xmlSource = "~/xml/sites.xml";
[UrlProperty]
[Bindable(true)]
[DefaultValue("~/xml/sites.xml")]
public string XMLSource
{
    get { return this._xmlSource; } set { this._xmlSource = value; }
}

//xPathprivate string _xPath = "site";
[DefaultValue("site")]
public string xPath
{
    get { return this._xPath; }  set { this._xPath = value; }
}
Add a property to get and set the XML source path. The default path is set to ~/xml/sites.xml. This property accepts a relative URL or absolute URL path to the XML file. Make sure to place the XML folder at the root directory of the application if you are using relative URL. Then, add another property, XPATH, to navigate through elements and attributes in the XML document.
Listing 5

void CreateScript()
{
    StringBuilder ssScript = new StringBuilder(string.Empty);
    string arrName = "myArray" + this.WrapperID;

    //read XML    ssScript.Append("var " + arrName+ "= [];");
    ssScript.Append("$(document).ready(function() {");
    ssScript.Append(" $.ajax({");
    ssScript.Append("type: \"GET\",");
    ssScript.Append("url: '" + ResolveUrl(XMLSource) + "',");
    ssScript.Append("cache: true,");
    ssScript.Append("dataType: \"xml\",");
    ssScript.Append("success: function(xml) {");
    ssScript.Append("var count = 0;");
    ssScript.Append("$(xml).find('" + xPath + "').each(function() {");

    ssScript.Append(" var url = $(this).find('url').text();");
    ssScript.Append("var target = $(this).find('target').text();");
    ssScript.Append("var imageURL = $(this).find('imageURL').text();");
    ssScript.Append("var alt = $(this).find('alt').text();");

    ssScript.Append(arrName + "[parseInt(count)] = 
             new Array(imageURL, url, target, alt); ");
    ssScript.Append("count++;");
    ssScript.Append("});");

    //slide-shows    ssScript.Append(" var mygallery"+this.WrapperID+" = new simpleGallery({");
    ssScript.Append(" wrapperid: '" + this.ClientID + 
                    "_" + this.WrapperID + "',");
    //width/height of gallery in pixels.    //Should reflect dimensions of the images exactly    ssScript.Append("dimensions: [" + Width.ToString() + 
                    ","+ Height.ToString()+"],"); 
    ssScript.Append("imagearray: "+arrName+","); //array of images    ssScript.Append("navimages: ['" + ResolveUrl(BtnPrevious) + "', '" + 
                    ResolveUrl(BtnPlay) + "', '" + ResolveUrl(BtnNext) + 
                    "', '" + ResolveUrl(BtnPause) + "'],");
    ssScript.Append("showpanel: '" + ShowNavigation.ToString().ToLower() + "',");
    ssScript.Append(" autoplay: [" + AutoPlay.ToString().ToLower() + "," + 
                    Delay_btw_slide.ToString() + "," + 
                    Cycles_before_stopping.ToString() + "],"); 
    //[auto_play_boolean, delay_btw_slide_millisec, cycles_before_stopping_int]    ssScript.Append(" persist: true,");
    //transition duration (milliseconds)    ssScript.Append(" fadeduration:" + FadeDuration.ToString() + ",");
    //event that fires when gallery has initialized/ ready to run    ssScript.Append(" oninit: function() {");
    ssScript.Append("  },");
    //event that fires after each slide is shown    ssScript.Append("  onslide: function(curslide, i) {");
    //curslide: returns DOM reference to current    // slide's DIV (ie: try alert(curslide.innerHTML)    //i: integer reflecting current image within collection    //   being shown (0=1st image, 1=2nd etc)    ssScript.Append("   }");
    ssScript.Append("  })");
    ssScript.Append("  }");
    ssScript.Append("   });");
    ssScript.Append(" });");

        ClientScriptManager jScript = Page.ClientScript;
        jScript.RegisterClientScriptBlock(this.GetType(), 
                Guid.NewGuid().ToString(), ssScript.ToString(), true);
    }
Then, add a method to render the JavaScript programmatically. The main purpose of this JavaScript is to read the slide information from the XML file into an array and initialize the slide-show properties. The key items of this JavaScript are the array variable, HTML div control, and the slide-shows variable. In order to have multiple instances of slide-shows on the page, we have to ensure that the unique IDs and name are being assigned to the mentioned key items. We can utilize the WrapperID property to create unique names and IDs for each instance. Note that the ResolveUrl method is being utilized so that the browser can resolve the URL of the images and the XML file. The rest of the code is very straightforward; you can download the original code from Dynamic Drive to compare the changes.
Listing 6

void CreateDiv()
{
    System.Web.UI.HtmlControls.HtmlGenericControl ssDivWrapper = 
       new System.Web.UI.HtmlControls.HtmlGenericControl("div");
    ssDivWrapper.ID = this.WrapperID;
    ssDivWrapper.Style.Add("background", "white none repeat scroll 0% 0%");
    ssDivWrapper.Style.Add(HtmlTextWriterStyle.Overflow, "hidden");
    ssDivWrapper.Style.Add(HtmlTextWriterStyle.Position, "relative");
    ssDivWrapper.Style.Add(HtmlTextWriterStyle.Visibility, "visible");
    ssDivWrapper.Style.Add(" -moz-background-clip", "border");
    ssDivWrapper.Style.Add("-moz-background-origin", "padding");
    ssDivWrapper.Style.Add("-moz-background-inline-policy", "continuous");

    this.Controls.Add(ssDivWrapper);
}
The CreateDiv method will generate the HTML div on the page dynamically.
Listing 7

//load the javascriptinternal void LoadJScript()
{
    ClientScriptManager script = Page.ClientScript;
    //prevent duplicate script    if (!script.IsStartupScriptRegistered(this.GetType(), "JQuerySlideShowJS"))
    {
        script.RegisterClientScriptBlock(this.GetType(), "JQuerySlideShowJS",
           "");
    }

    if (!script.IsStartupScriptRegistered(this.GetType(), "SimpleGalleryJS"))
    {
        script.RegisterClientScriptBlock(this.GetType(), "SimpleGalleryJS",
           "");
    }
}
The purpose of the LoadJScript method is to load jquery-1.3.2.min.js and simplegallery.js dynamically with no duplicates. With that being said, if we drag 10 slide-show controls on to a page, it will only add the JavaScript once on to the ASP.NET page. This will help avoid unnecessarily assembling the client-side script.

simplegallery.js

I'm not going to post the whole contents of this JavaScript here, but I'll list the changes that I have made.
  1. Removed the static image button properties from the simpleGallery_navpanel interface since the image buttons will be assigned dynamically through the CreateScript method.
  2. Modified the script to read the image button properties from different interfaces.
  3. Added logic to hide and display the navigation control through the ShowNavigation property.
  4. Added alternate text to the image buttons.

Using the Code

Listing 8

<uc1:SlideShow ID="SlideShow2" runat="server" />
Drag and drop the User Control on to the page. Here is the default setting of the User control:
Figure 3
Default properties
Listing 9

<uc1:SlideShow ID="SlideShow5" runat="server" 
    BtnNext="~/images/nav-arrow-right.gif" 
    BtnPrevious="~/images/nav-arrow-left.gif" 
    AutoPlay="false" />
Above is an example on how to set the image buttons and the AutoPlay property.
Listing 10

<uc1:SlideShow ID="SlideShow7" runat="server" XPath="site500x281"  
    Width="500" Height="281" ShowNavigation="true" 
    XMLSource="~/xml/500x281.xml"/>
The above example demonstrates how to set the width and height of the HTML div control. The control gets the images information from the 500x821.xml file and selects the site500x281 nodes. By default, the ShowNavigation attribute is set to true.
Listing 11 - More sample usage
 
<uc1:SlideShow ID="SlideShow6" runat="server" BtnNext="~/images/nav-arrow-right.gif" 
        BtnPrevious="~/images/nav-arrow-left.gif" 
        XMLSource="~/xml/120x600.xml" XPath="site120x600" 
         Width="120" Height="600" />

<uc1:SlideShow ID="SlideShow1" XPath="site120x60" 
    Width="120" Height="60"
    ShowNavigation="false" 
    runat="server" XMLSource="~/xml/sites2.xml"  />

Conclusion

If you are not sure of the relative path to the images or XML file, I would suggest using absolute links. Please keep in mind that the links to the XML files have to be in the same domain, no cross-domain calls. Also, try not to place all the images information in one XML file for multiple slide-show controls. Imagine, if we have a XML file of 50KB in size, 10 slide-show controls on the page, and each of them consuming the same XML source. It will cost us 500KB bandwidth and degrade the loading speed. I hope someone will find this tutorial useful and share some thoughts with me on how to make it better.
Tested on IE 6.0/7.0/8.0, Google Chrome, and Firefox.

References

Popular Posts