Thursday, January 15, 2009

How to Handle the AjaxControlToolkit's Client Event or Rewrite the Delegate of the Client Event

My last post has mentioned the AjaxControlToolkit Extender client classes provide many useful APIs that enable us to bind to events and to provide handlers for those events. Now, let's focus on how to achieve this goal.(How to Find the Client Behavior of the Extender which is placed inside the DataBind Control)

Every extender provides some established functions to handle its client events. These functions is defined as this "add_Event Name".

For example, the AutoCompleteExtender has several client events, such as "GotFocus", "LostFocus", "KeyDown", "CompletionListBlur", "ListMouseOver". We can know their function by the name, the "GotFocus" event means the cursor focus on the AutoComplete's target -- the input TextBox; the "ListMouseOver" event is the mouseover event on the autocomplete flyout(we can say it SuggestionList) . To understand the exact description of these events, please download the AjaxControlToolkit-Framework3.5SP1.zip.(with complete source), and refer to this file:\AjaxControlToolkit-Framework3.5SP1\AjaxControlToolkit\AutoComplete\AutoComplete.aspx

Let's take the "ListMouseOver" event as the sample. The AutoComplete creates a delegate and a handler to the event.

this._mouseOverHandler = Function.createDelegate(this, this._onListMouseOver);


_onListMouseOver: function(ev) {
/// <summary>
/// Handler for the mouseover event on the autocomplete flyout.
/// </summary>
/// <param name="ev" type="Sys.UI.DomEvent" DomElement="false" mayBeNull="false" />
/// <returns />
var item = ev.target;
if(item !== this._completionListElement) {
var children = this._completionListElement.childNodes;
// make sure the selected index is updated
for (var i = 0; i < children.length; ++i) {
if (item === children[i]) {
this._highlightItem(item);
this._selectIndex = i;
break;
}
}
}
}


Besides, there are two APIs named add_itemOver and remove_itemOver which are used to handle the same event.

add_itemOver : function(handler) {
/// <summary>
/// Add an event handler for the itemOver event
/// </summary>
/// <param name="handler" type="Function" mayBeNull="false">
/// Event handler
/// </param>
/// <returns />
this.get_events().addHandler('itemOver', handler);
},
remove_itemOver : function(handler) {
/// <summary>
/// Remove an event handler from the itemOver event
/// </summary>
/// <param name="handler" type="Function" mayBeNull="false">
/// Event handler
/// </param>
/// <returns />
this.get_events().removeHandler('itemOver', handler);
},


Ok, let's use these functions.

Here is a scenario. An AutoComplete should be added into one DIV, another DropDownList is under this AutoComplete's input TextBox. Normally, the generated CompletionList can display upon the DropDownList. like this:


But if we place them inside a relative postion DIV, the behavior is changed. The DropDownList would stay on the top of any controls!(This is the established design of IE6.)

To display the CompletionList on the top, we need add a shared absolute position IFRAME behind the CompletionList.

At the first, we need to handle the CompletionList's ClientShowing event.
<ajaxToolkit:AutoCompleteExtender ID="AutoCompleteExtender1" runat="server" TargetControlID="txtName"
    ServicePath="AutoComplete1.asmx" ServiceMethod="GetCompletionListByRandom" MinimumPrefixLength="2"
    CompletionInterval="10" EnableCaching="true" OnClientShowing="clientShowing"
    CompletionListCssClass="autocomplete_completionListElement" CompletionListHighlightedItemCssClass="autocomplete_highlightedListItem"
    CompletionListItemCssClass="autocomplete_listItem">
</ajaxToolkit:AutoCompleteExtender>


Second, we need find the Client Behavior and the CompletionList of the AutoComplete in the script.

Third, add the shared absolute position IFRAME.
<script type="text/javascript">
    function clientShowing(source, args) {
        var popup = source._completionListElement;
        var height = popup.scrollHeight;
        var width = popup.scrollWidth;
        //This iframe's height and width should be smaller than the CompletionList but bigger than the DropDownList
        var iframe1 = "<IFRAME id='iframe1' style=' height:" + height + "; width:" + width + "; position:absolute; z-index:-1;border:0px; border-style:none; border-width:0px; ' ></IFRAME>";
        popup.innerHTML = iframe1 + popup.innerHTML;
    }
</script>


Now we achieve the goal about stay the CompletionList on the top of the DropDownList:

Take a break here.

Beautiful thing is difficult. If you think that's all but that's wrong. I have metioned there is a "MouseOver" event of the CompletionList. If we add the IFRAME, would any chaos be added in? YEAH! That's what I focused.
If the input text is long enough, we can find the options value of the CompletionList are no in same length. What would be raised if hovering over the end of the shorter option? You may think of the result--an error!

The cause is the added IFRAME does not have a valid value to choose. Let's fix it by handling the "MouseOver" event.

Firstly, we need to remove the original evevt delegate in the pageLoad function. Have you remembered how to find the Client Behavior of the Extenders? We need use it now!

Actually, we can use this method $find(the AutoComplete's BehaviorId) instead of the currentBehavior if there is only one extender.

Second, in the customMouseOverHandler function, rewrite the original method by adding the judge of the Hovered Item type:
function customMouseOverHandler(ev) {
    /// <summary>
    /// Handler for the mouseover event on the autocomplete flyout.
    /// </summary>
    /// <param name="ev" type="Sys.UI.DomEvent" DomElement="false" mayBeNull="false" />
    /// <returns />
    var item = ev.target;
   
    // The judge of the MouseOverred Item Type
    if (item.tagName == "IFRAME") {
        return;
    }
    if (item !== _currentAutoComplete._completionListElement) {
        var children = _currentAutoComplete._completionListElement.childNodes;
        // make sure the selected index is updated
        for (var i = 0; i < children.length; ++i) {
            if (item === children[i]) {
                _currentAutoComplete._highlightItem(item);
                _currentAutoComplete._selectIndex = i;
                break;
            }
        }
    }
}


OK! This is the really end of this huge work! I assume you have a basic understanding of how to handle the AjaxControlToolkit extender's client event or rewrite the delegate of the client event. Any questions, feel free to tell me here or post your request to the AJAX forum.

The complete code:
<%@ Page Title="" Language="C#" MasterPageFile="~/Master.Master" AutoEventWireup="true"
    CodeBehind="Content.aspx.cs" Inherits="SolluTest_AutoComplete.Content" %>

<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="ajaxToolkit" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">

    <script type="text/javascript">
        function clientShowing(source, args) {
            var popup = source._completionListElement;
            var height = popup.scrollHeight;
            var width = popup.scrollWidth;
            //This iframe's height and width should be smaller than the CompletionList but bigger than the DropDownList
            var iframe1 = "<IFRAME id='iframe1' style=' height:" + height + "; width:" + width + "; position:absolute; z-index:-1;border:0px; border-style:none; border-width:0px; ' ></IFRAME>";
            popup.innerHTML = iframe1 + popup.innerHTML;
        }

        function pageLoad() {
            //If any Extender is placed in the DataBind Control, it's hard to defind the BehaviorId and get the Client behavior.
            //We can use the method to find all the correct type behaviors.
            var currentBehavior = null;
            var allBehaviors = Sys.Application.getComponents();
            for (var loopIndex = 0; loopIndex < allBehaviors.length; loopIndex++) {
                currentBehavior = allBehaviors[loopIndex];
                if (currentBehavior.get_name() == "AutoCompleteBehavior") {
                    _currentAutoComplete = currentBehavior;
                    var completionListElement = currentBehavior._completionListElement;
                    $removeHandler(completionListElement, 'mouseover', currentBehavior._mouseOverHandler);
                    //rewrite the MouseOver event's delegate
                    currentBehavior._mouseOverHandler = Function.createDelegate(currentBehavior, customMouseOverHandler);
                    $addHandler(completionListElement, "mouseover", currentBehavior._mouseOverHandler);
                }
            }
        }

        function customMouseOverHandler(ev) {
            /// <summary>
            /// Handler for the mouseover event on the autocomplete flyout.
            /// </summary>
            /// <param name="ev" type="Sys.UI.DomEvent" DomElement="false" mayBeNull="false" />
            /// <returns />
            var item = ev.target;

            // The judge of the MouseOverred Item Type
            if (item.tagName == "IFRAME") {
                return;
            }
            if (item !== _currentAutoComplete._completionListElement) {
                var children = _currentAutoComplete._completionListElement.childNodes;
                // make sure the selected index is updated
                for (var i = 0; i < children.length; ++i) {
                    if (item === children[i]) {
                        _currentAutoComplete._highlightItem(item);
                        _currentAutoComplete._selectIndex = i;
                        break;
                    }
                }
            }
        }
    </script>

    <style type="text/css">
        /*AutoComplete flyout */.autocomplete_completionListElement
        {
            visibility: hidden;
            margin: 0px !important;
            background-color: inherit;
            color: windowtext;
            border: buttonshadow;
            border-width: 1px;
            border-style: solid;
            cursor: 'default';
            overflow: auto;
            height: 200px;
            text-align: left;
            list-style-type: none;
        }
        /* AutoComplete highlighted item */.autocomplete_highlightedListItem
        {
            background-color: #ffff99;
            color: black;
            padding: 1px;
        }
        /* AutoComplete item */.autocomplete_listItem
        {
            background-color: window;
            color: windowtext;
            padding: 1px;
        }
    </style>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="contentBody" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server" />
    <asp:TextBox ID="txtName" runat="server" Width="200px" MaxLength="60" AutoPostBack="false"></asp:TextBox><br />
    <ajaxToolkit:AutoCompleteExtender ID="AutoCompleteExtender1" runat="server" TargetControlID="txtName"
        ServicePath="AutoComplete1.asmx" ServiceMethod="GetCompletionListByRandom" MinimumPrefixLength="2"
        CompletionInterval="10" EnableCaching="true" OnClientShowing="clientShowing"
        CompletionListCssClass="autocomplete_completionListElement" CompletionListHighlightedItemCssClass="autocomplete_highlightedListItem"
        CompletionListItemCssClass="autocomplete_listItem">
    </ajaxToolkit:AutoCompleteExtender>
    <asp:DropDownList ID="DropDownList1" runat="server" Height="16px" Width="84px">
        <asp:ListItem>123</asp:ListItem>
        <asp:ListItem>234</asp:ListItem>
        <asp:ListItem>qwe</asp:ListItem>
    </asp:DropDownList>
</asp:Content>

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Master.master.cs" Inherits="SolluTest_AutoComplete.Master" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
    <asp:ContentPlaceHolder ID="head" runat="server">
    </asp:ContentPlaceHolder>
</head>
<body style="width:100%;text-align:center;background-color:#666699">
    <form id="frmMain" runat="server">
        <div id="mainDiv" style="position:relative;top:10px;width:803px;height:auto; background-color:white;border:solid 1px #666666">       
            <div id="contentDiv" > 
                <div >
                <asp:ContentPlaceHolder ID="contentBody" runat="server"></asp:ContentPlaceHolder>
                </div>
            </div>   
        </div>
    </form>
</body>

</html>


The related thread url: http://forums.asp.net/t/1362145.aspx

How to Find the Client Behavior of the Extender which is placed inside the DataBind Control

We all know the Extenders of the AjaxControlToolkit implement their Client Partial-Refresh action by using their Client behaviors which are generated after they are rendered. In addition, the client classes provide many useful APIs that enable you to bind to events and to provide handlers for those events.

For example, the AutoComplete Extender's client behavior document would like this:

The AutoCompleteExtender
<AjaxControlToolkit:AutoCompleteExtender ID="AutoCompleteExtender1" runat="server"
TargetControlID="TextBox2" ServicePath="AutoComplete1.asmx" ServiceMethod="GetCompletionListByRandom"
MinimumPrefixLength="2" CompletionInterval="10" EnableCaching="true" OnClientPopulated="acePopulated"
OnClientItemSelected="IAmSelected" >
</AjaxControlToolkit:AutoCompleteExtender>

Its Client Behaviior Document



You may notice that we simply used this method "$find(the Extender's BehaviorID)" to get the Client Behavior Document. If the BehaviorId is not defined, we can use this "$find()".

But sometimes things are not so simple. If we place the Extender inside a DataBind Control's ItemTemplete, such as GridView. The same method would raise error which sames like this:

Sys.InvalidOperationException:
Two components with the same id 'XXXXXBehavior1' can't be added to the application.


That's because the BehaviorId shoule be unique.

Then, how can we get the Behaviors in this situation?

Actually, every behavior has been cached into the Sys.Application's Components object after the page client initialize phase in the Life-Cycle. When script in a load event handler runs, all scripts and components have been loaded and are available. That's the key to achieve our goal.

function pageLoad() {
//If any Extender is placed in the DataBind Control, it's hard to define the BehaviorId and get the Client behavior.
//We can use the method to find all the correct type behaviors.
    var currentBehavior = null;
    var allBehaviors = Sys.Application.getComponents();
    for (var loopIndex = 0; loopIndex < allBehaviors.length; loopIndex++) {
        currentBehavior = allBehaviors[loopIndex];
        if (currentBehavior.get_name() == "AutoCompleteBehavior") {
            // Now we get the ClientBehavior here: currentBehavior!
        }
    }
}

Monday, January 12, 2009

Customize the Tooltip of AjaxControlToolkit Rating Extender

When we hover over the stars of the Rating control, the hovered star's value would be displayed as the Tooltip. Customer may wish to customize the Tooltip of the Rating's star. Now, let's do it.

After the Rating control is rendered, three sections will be generated. An INPUT document, whose id is "the Rating's ID"_"RatingExtender"_"ClientState", which is used to save the Rating behavior's value.
<INPUT id=ThaiRating_RatingExtender_ClientState type=hidden value=2 name=ThaiRating_RatingExtender_ClientState>
An A document, whose id is "the Rating's ID"_"A", which is used to disply the Tooltip.
<A id=ThaiRating_A title=4 style="TEXT-DECORATION: none" href="#">
A few SPAN documents, whose id are "the Rating's ID"_"Star"_"the Index", which are used to display the stars with the corresponding format CssClass.
<SPAN class="ratingStar filledRatingStar" id=ThaiRating_Star_1 style="FLOAT: left" value="1">&nbsp;</SPAN>


Then, to customize the Tooltip, we need find the current A document. The RatingBehavior provides a function add_MouseOver(handler) to help us handle the mouse over event. In the handler function, we can find the Rating's client behavior by the parameter-sender.
function ratingOnMouseOver(sender, eventArgs) {
    var elt = sender.get_element();
}
Notice that we can also simply use $find(the Rating's Id) to achieve this in any client function.

Then, the A document is this $get(elt.id + "_A"). Modifying the title property is the final step.

Here is the complete code:
<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="Default.aspx.vb" Inherits="SoluTest_RatingControl._Default" %>

<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="cc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
    <style type="text/css">
        /* Rating */
        .ratingStar
        {
            font-size: 0pt;
            width: 13px;
            height: 12px;
            margin: 0px;
            padding: 0px;
            cursor: pointer;
            display: block;
            background-repeat: no-repeat;
        }
        .filledRatingStar
        {
            background-image: url(Image/FilledStar.png);
        }
        .emptyRatingStar
        {
            background-image: url(Image/EmptyStar.png);
        }
        .savedRatingStar
        {
            background-image: url(Image/SavedStar.png);
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    </asp:ScriptManager>
    <div>
        <cc1:Rating ID="ThaiRating" runat="server" BehaviorID="RatingBehavior1" CurrentRating="2"
            MaxRating="5" StarCssClass="ratingStar" WaitingStarCssClass="savedRatingStar"
            FilledStarCssClass="filledRatingStar" EmptyStarCssClass="emptyRatingStar" Style="float: left;" />
    </div>
    </form>

    <script language="javascript" type="text/javascript">

    function pageLoad()
    {
        $find("RatingBehavior1").add_MouseOver(ratingOnMouseOver);
    }
    function ratingOnMouseOver(sender, eventArgs) {
        var elt = sender.get_element();
        //please modify this code as your wish, for example sender._currentRating*10
        $get(elt.id + "_A").title = sender._currentRating + " Stars";
    }
    </script>

</body>
</html>
Thread url:http://forums.asp.net/t/1369024.aspx

Sunday, January 11, 2009

AJAX can not be supported in VS2008 Express Edition

A customer asked me why the AjaxControlToolkit can't be installed in the VS2008 Express Edition. Firstly, I thought there are some misunderstanding in his installation steps. I suggested him follow these information to install.

Maybe your explorer disabled some functions of the JavaScript or some third party components conflicted with the AjaxControlToolkit.

To test it, I recommend you have a look at the samples of the AjaxControlToolkit: http://www.asp.net/ajax/ajaxcontroltoolkit/samples/

Get the tutorial video and code from this link:http://www.asp.net/learn/ajax-videos/

But the issue did still exist. After searching in the VisualStudio2008 ProductComparison, I found that the Express Edition does not support some functions of the Ajax.


Here is detail: http://www.microsoft.com/downloads/details.aspx?FamilyID=727BCFB0-B575-47AB-9FD8-4EE067BB3A37&displaylang=en

So, we may install the AjaxControlToolkit until using the correct edition.

Thread url:http://forums.asp.net/t/1368594.aspx

Thursday, January 8, 2009

Stay Animation of Accordion When Header Controls Cause PostBack

Every time we use the AccordionExtender of AjaxControlToolkit, we may would like to enable the Header's control to cause post back. To achieve this, simply set the property SuppressHeaderPostbacks with false. (Notice that we need use the latest version.)

But if the service event of the control is fired, the whole page would be refreshed with all the controls be rendered again. Thus the client animations of the Accordion would not be produced, the behavior is indeed an update of the page with the new style of the AccordionPane.

That's not what we want, we need to stay the animation. Now, let's do it.
First, we need to disable the ServiceEvent by adding 'return false' inside the OnClientClick function.

<asp:LinkButton ID="LinkButton1" OnClientClick="linkClientClick(0,this);return false;" runat="server">LinkButton1</asp:LinkButton>


Then, in the ClientClick function, we add an event handler to the Fade Animation's ended event. The AjaxControlToolkit animation provides an function named 'add_ended(handler)' to help us do it.

function linkClientClick(id, link) {
    //To handle the animation ended event
    accordion._getAnimation(accordion._panes[id])._animations[0].add_ended(animationEndedHandler);
    //get the Link's uniqueID from the href
    a = link.href.replace("javascript:__doPostBack('", "");
    currentLink = a.substring(0, a.indexOf("'", 0));
}


In the handler, call the _doPostBack function whose eventTarget is set as the clicked LinkButton.

function animationEndedHandler() {
    if (currentLink) {
        //do post back
        __doPostBack(currentLink, '');
    }
}



Here is the whole code:
.aspx file

<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="Test1.aspx.vb" Inherits="SoluTest_Accordion.Test1"
    EnableEventValidation="false" %>

<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="cc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <style type="text/css">
        .accordionHeader
        {
            border: 1px solid #2F4F4F;
            color: white;
            background-color: #2E4d7B;
            font-family: Arial, Sans-Serif;
            font-size: 12px;
            font-weight: bold;
            padding: 5px;
            margin-top: 5px;
            cursor: pointer;
            height: 10px;
        }
        .accordionHeaderSelect
        {
            border: 1px solid #2F4F4F;
            color: red;
            background-color: #2E4d7B;
            font-family: Arial, Sans-Serif;
            font-size: 12px;
            font-weight: bold;
            padding: 5px;
            margin-top: 5px;
            cursor: pointer;
            height: 10px;
        }
        .accordionContent
        {
            background-color: #D3DEEF;
            border: 1px dashed #2F4F4F;
            border-top: none;
            padding: 5px;
            padding-top: 10px;
        }
    </style>

    <script type="text/javascript">
        var accordion;
        var currentLink;
        function linkClientClick(id, link) {
            //To handle the animation ended event
            accordion._getAnimation(accordion._panes[id])._animations[0].add_ended(animationEndedHandler);
            //get the Link's uniqueID from the href
            a = link.href.replace("javascript:__doPostBack('", "");
            currentLink = a.substring(0, a.indexOf("'", 0));
        }

        function pageLoad() {
            accordion = $find("Accordion2_AccordionExtender");
            currentLink = null;
        }

        function animationEndedHandler() {
            if (currentLink) {
                //do post back
                __doPostBack(currentLink, '');
            }
        }
    </script>

</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:ScriptManager ID="ScriptManager1" runat="server" />
        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>
                <cc1:Accordion ID="Accordion2" runat="server" SelectedIndex="0" HeaderCssClass="accordionHeader"
                    HeaderSelectedCssClass="accordionHeaderSelect" ContentCssClass="accordionContent"
                    FadeTransitions="True" FramesPerSecond="40" TransitionDuration="1250" AutoSize="None"
                    RequireOpenedPane="false" SuppressHeaderPostbacks="false">
                    <Panes>
                        <cc1:AccordionPane ID="AccordionPane1" runat="server">
                            <Header>
                                <p>
                                    Run fade animation before Link's service event is fired.</p>
                                <asp:LinkButton ID="LinkButton1" OnClientClick="linkClientClick(0,this);return false;"
                                    runat="server">LinkButton1</asp:LinkButton><%-- return false to disable the server event--%>
                            </Header>
                            <Content>
                                <p>
                                    This is the content area!</p>
                            </Content>
                        </cc1:AccordionPane>
                        <cc1:AccordionPane ID="AccordionPane2" runat="server">
                            <Header>
                                <p>
                                    Click the LinkButton with no animation</p>
                                <asp:LinkButton ID="LinkButton2" runat="server">LinkButton2</asp:LinkButton>
                            </Header>
                            <Content>
                                <p>
                                    This is the content area!</p>
                            </Content>
                        </cc1:AccordionPane>
                    </Panes>
                </cc1:Accordion>
            </ContentTemplate>
        </asp:UpdatePanel>
    </div>
    </form>
</body>
</html>


.aspx.vb file

Public Partial Class Test1
    Inherits System.Web.UI.Page

    Protected Sub LinkButton1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles LinkButton1.Click
        LinkButton1.Text = "Link1 is clicked!"
    End Sub

    Protected Sub LinkButton2_Click(ByVal sender As Object, ByVal e As EventArgs) Handles LinkButton2.Click
        LinkButton2.Text = "Link2 is clicked!"
    End Sub

End Class


Thread url:http://forums.asp.net/p/1367664/2855920.aspx#2855920

CustomControl needs to implement INamingContainer interface to handle its child control's events

Customer's question:
One CustomControl with UdpatePanel and HoverMenuExtender inside cannot fire the dopostback.
His code is below:
[DefaultProperty("Text")]
[ToolboxData("&lt;{0}:MagicText runat=server></{0}:MagicText>")]
public class MagicText : WebControl
{
protected UpdatePanel panel;
protected HoverMenuExtender menu;
private HttpSessionState Session;
protected TextBox textbox;
protected Panel currentMenu;


[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public string Text
{
get
{
String s = (String)ViewState["Text"];
return ((s == null) ? "[" + this.ID + "]" : s);
}

set
{
ViewState["Text"] = value;
}
}

protected override void CreateChildControls()
{
panel = new UpdatePanel();
this.Controls.Add(panel);
panel.ID = this.ID + "ajaxPanel";

panel.UpdateMode = UpdatePanelUpdateMode.Conditional;
panel.ChildrenAsTriggers = true;
panel.Load += new EventHandler(UpdatePanel_OnLoad);
textbox = new TextBox();
textbox.ReadOnly = true;
textbox.ID = this.ID + "textbox";
textbox.Text = Text;
Button b = new Button();
b.CommandName = "hi";
b.Text = "hi";
b.Click += new EventHandler(ButtonClick);
b.ID = "button1";
panel.ContentTemplateContainer.Controls.Add(b);
menu = new HoverMenuExtender();
menu.ID = this.ID + "AjaxHoverMenu";
menu.BehaviorID = this.ID + "AjaxHoverMenuBehavior";
menu.PopupControlID = GetPanelForMenu();
menu.TargetControlID = textbox.ID;
menu.HoverCssClass = "popupHover";
menu.HoverDelay = 25;
menu.PopupPosition = HoverMenuPopupPosition.Right;
panel.ContentTemplateContainer.Controls.Add(textbox);
panel.ContentTemplateContainer.Controls.Add(menu);
panel.ContentTemplateContainer.Controls.Add(currentMenu);

PostBackTrigger trigger = new PostBackTrigger();
trigger.ControlID = b.ClientID.ToString();
panel.Triggers.Add(trigger);

base.CreateChildControls();
//panel.Controls.Add(currentMenu);

}

void ButtonClick(object sender, EventArgs e)
{
}

protected string GetPanelForMenu()
{
currentMenu = new Panel();
currentMenu.ID = this.ID + "menuForPopup";
currentMenu.CssClass = "popupMenu";
LinkButton lb;

lb = new LinkButton();
lb.ID = "magicMenuItem1";
lb.CssClass = "popupItem";
lb.Click += new EventHandler(ModifyText);
lb.Text = "Edit";
currentMenu.Controls.Add(lb);


currentMenu.Controls.Add(new LiteralControl("&lt;br/>"));

lb = new LinkButton();
lb.ID = "magicMenuItem2";
lb.CssClass = "popupItem";
lb.Text = "Add";
//lb.Click += new EventHandler(AddToText);
lb.CommandName = "Add";
currentMenu.Controls.Add(lb);

currentMenu.Controls.Add(new LiteralControl("&lt;br/>"));

lb = new LinkButton();
lb.ID = "magicMenuItem3";
lb.CssClass = "popupItem";
lb.Click += new EventHandler(TranslateText);
lb.Text = "Translate";
currentMenu.Controls.Add(lb);

currentMenu.Controls.Add(new LiteralControl("&lt;br/>"));

lb = new LinkButton();
lb.ID = "magicMenuItem4";
lb.CssClass = "popupItem";
lb.Click += new EventHandler(CommentText);
lb.Text = "Comment";
currentMenu.Controls.Add(lb);

currentMenu.Controls.Add(new LiteralControl("&lt;br/>"));

lb = new LinkButton();
lb.ID = "magicMenuItem5";
lb.CssClass = "popupItem";
lb.Click += new EventHandler(FlagText);
lb.Text = "Flag";
currentMenu.Controls.Add(lb);

currentMenu.Controls.Add(new LiteralControl("&lt;br/>"));

lb = new LinkButton();
lb.ID = "magicMenuItem6";
lb.CssClass = "popupItem";
lb.Click += new EventHandler(ViewRevisionHistory);
lb.Text = "View Revisions";
currentMenu.Controls.Add(lb);

currentMenu.Controls.Add(new LiteralControl("&lt;br/>"));

return currentMenu.ID;
}

void UpdatePanel_OnLoad(object sender, EventArgs e)
{
int x = 0;
x = x;
}

void ModifyText(object sender, EventArgs e)
{
textbox.ReadOnly = false;
textbox.BackColor = Color.Pink;
}

void AddToText(object sender, EventArgs e)
{
throw new NotImplementedException();
}

void TranslateText(object sender, EventArgs e)
{
throw new NotImplementedException();
}

void CommentText(object sender, EventArgs e)
{
throw new NotImplementedException();
}

void FlagText(object sender, EventArgs e)
{
throw new NotImplementedException();
}

void ViewRevisionHistory(object sender, EventArgs e)
{
throw new NotImplementedException();
}


protected override void RenderContents(HtmlTextWriter output)
{
panel.RenderControl(output);
currentMenu.RenderControl(output);
}

void RenderMenu(HtmlTextWriter writer)
{
base.Render(writer);
}
}

The cause is the CustomControl has not implemented the INamingContainer interface. Custom control MUST implement this Marker interface if it needs to handle any of its child control's events. There is nothing to do with the AjaxControlToolkit and the UpdatePanel.

Here are some useful links:
http://www.dotnetjunkies.com/WebLog/srivallichavalidotnet/archive/2005/11/14/133759.aspx
http://msdn.microsoft.com/en-us/library/system.web.ui.inamingcontainer.aspx

Thread url:http://forums.asp.net/t/1367187.aspx