Fixing combobox from ajax control toolkit

I was using the ComboBox control of Microsoft’s Ajax Control Toolkit and ran into two issues.

  • When its initially hidden (its inside something with a style of display:none) and you make it visible, the toggle button and actual dropdown list are incorrectly sized to be almost meaningless.
    how it should look:
    ComboBox_OnShow_Correct
    how it actually looks
    ComboBox_OnShow_Incorrect
    Notice the toggle button has virtually disappeared. That grey underline is the dropdown list.

  • When its in a ModalDialog created by the ModalPopupExtender, the dropdown lists appear far away from where they belong.
    Here the Category dropdown toggle was clicked. The list should be flush with the bottom of the Category’s textbox.
    ComboBox_InMDE

I created the following class to solve both of these problems. It was developed with v3.0.30512.1 of the AjaxControlToolkit.dll. (Get it as a zip file here)

   1: /// <summary>
   2: /// These static methods fix bugs related to the AjaxControlToolkit's ComboBox.
   3: /// </summary>
   4:    public class ComboBoxFixer
   5:    {
   6: /// <summary>
   7: /// Use this when adding ACT Comboboxes that may be initially hidden when loaded.
   8: /// It registers all comboboxes into a client-side array. If the user writes
   9: /// scripts that make the ACT Comboboxes visible, they should call
  10: /// ActComboBoxMadeVisible_All(), a client-side method.
  11: /// </summary>
  12:       public static void RegisterComboBox(AjaxControlToolkit.ComboBox pComboBox)
  13:       {
  14:          ClientScriptManager vClientScript = pComboBox.Page.ClientScript;
  15:          if (!vClientScript.IsClientScriptBlockRegistered("ActComboBoxFixer"))
  16:          {
  17:             string vScript =
  18:                "function ActComboBoxMadeVisible_All()\r\n" +
  19:                "{\r\n" +
  20:                 "   if (window.gActComboboxes)\r\n" +
  21:                 "      for (var vI = 0; vI < window.gActComboboxes.length; vI++)\r\n" +
  22:                 "      {\r\n" +
  23:                 "         var vCB = $find(window.gActComboboxes[vI]);\r\n" +
  24:                 "         if (vCB)\r\n" +
  25:                 "         {\r\n" +
  26:                 "            ActComboBoxMadeVisible(vCB);\r\n" +
  27:                 "         }\r\n" +
  28:                 "      }\r\n" +  
  29:                "}\r\n" +
  30:                "function ActComboBoxMadeVisible(pCB)\r\n" +
  31:                "{\r\n" +
  32:                 "  if (pCB && !pCB._optionListItemHeight)\r\n" +
  33:                 "  {\r\n" +
  34:                 "     var vBtn = pCB.get_buttonControl();\r\n" +
  35:                 "     vBtn.style.width = '';\r\n" +
  36:                 "     vBtn.style.height = '';\r\n" +
  37:                 "     pCB.initializeButton();\r\n" +
  38:                 "     pCB._optionListHeight = null;\r\n" +
  39:                 "     pCB._optionListWidth = null;\r\n" +
  40:                 "     pCB._optionListItemHeight = 21;\r\n" +
  41:                 "     pCB._getOptionListWidth();\r\n" +
  42:                 "     pCB._getOptionListHeight();\r\n" +
  43:                 "  }\r\n" +
  44:                "}\r\n";
  45:             vClientScript.RegisterClientScriptBlock(typeof(Page), "ActComboBoxFixer", vScript, true);
  46:          }
  47:          pComboBox.Page.ClientScript.RegisterArrayDeclaration("gActComboboxes", "'" + pComboBox.ClientID + "'");
  48:       }
  49:  
  50: /// <summary>
  51: /// Helps ModalDialogExtenders work with AjaxControlToolkit.ComboBox. Call for each ModalDialogExtender
  52: /// that may have ComboBoxes. If you have more than one and they may appear simulateously, add them
  53: /// in a specific order where the topmost one is added before those below it.
  54: /// </summary>
  55: /// <remarks>
  56: /// <para>Requires each ComboBox is passed to ComboBoxFixer.RegisterComboBox.</para>
  57: /// </remarks>
  58: /// <param name="pModalExtender"></param>
  59:       public static void RegisterModalPopupExtender(AjaxControlToolkit.ModalPopupExtender pModalExtender)
  60:       {
  61:          ClientScriptManager vClientScript = pModalExtender.Page.ClientScript;
  62:          vClientScript.RegisterArrayDeclaration("gACTModalDEIDs", "'" + pModalExtender.ClientID + "'");
  63:  
  64:          if (!vClientScript.IsClientScriptBlockRegistered("ActComboBoxInMDE"))
  65:          {
  66:    // The basic idea: Replace AjaxControlToolkit._popupShown with ActComboBoxInMDE_PopupShown.
  67:    // ActComboBoxInMDE_PopupShown is a clone of _popupShown, but inserts code to change x,y
  68:    // when a ModalDialogExtender is visible. 
  69:    // MDEs are registerd in the client-side array gACTModalDEIDs.
  70:    // This allows multiple MDEs and will only evaluate the first whose content element (called _foregroundElement)
  71:    // is visible. So if there are nested MDEs, register the topmost one first and bottommost last.
  72:             string vScript =
  73:             "function ActComboBoxInMDE_Init()\r\n" +
  74:             "{\r\n" +
  75:             "   if (window.gActComboboxes)\r\n" +
  76:             "      for (var vI = 0; vI < gActComboboxes.length; vI++)\r\n" +
  77:             "      {\r\n" +
  78:             "         var vCB = $find(gActComboboxes[vI]);\r\n" +
  79:             "         if (vCB.InitedMDE) continue;\r\n" +
  80:             "         vCB._popupShown = ActComboBoxInMDE_PopupShown;\r\n" +
  81:             "         vCB._popupShownHandlerFix = Function.createDelegate(vCB, ActComboBoxInMDE_PopupShown);\r\n" +
  82:             "         vCB._popupBehavior.add_shown(vCB._popupShownHandlerFix);\r\n" +
  83:             "         vCB.InitedMDE = 1;\r\n" +
  84:             "      }\r\n" +
  85:             "}\r\n" +
  86:             "function ActComboBoxInMDE_PopupShown() {\r\n" +
  87:             "\r\n" +
  88:             "   this.get_optionListControl().style.display = 'block';\r\n" +
  89:             "\r\n" +
  90:             "   // check and enforce correct positioning.\r\n" +
  91:             "   var tableBounds = Sys.UI.DomElement.getBounds(this.get_comboTableControl());\r\n" +
  92:             "   var listBounds = Sys.UI.DomElement.getBounds(this.get_optionListControl());\r\n" +
  93:             "   var textBoxBounds = Sys.UI.DomElement.getBounds(this.get_textBoxControl());\r\n" +
  94:             "   var y = listBounds.y;\r\n" +
  95:             "   var x;\r\n" +
  96:             "\r\n" +
  97:             "   if (this._popupBehavior.get_positioningMode() === AjaxControlToolkit.PositioningMode.BottomLeft\r\n" +
  98:             "      || this._popupBehavior.get_positioningMode() === AjaxControlToolkit.PositioningMode.TopLeft) {\r\n" +
  99:             "      x = textBoxBounds.x;\r\n" +
 100:             "   }\r\n" +
 101:             "   else if (this._popupBehavior.get_positioningMode() === AjaxControlToolkit.PositioningMode.BottomRight\r\n" +
 102:             "      || this._popupBehavior.get_positioningMode() === AjaxControlToolkit.PositioningMode.TopRight) {\r\n" +
 103:             "      x = textBoxBounds.x - (listBounds.width - textBoxBounds.width);\r\n" +
 104:             "   }\r\n" +
 105:             "\r\n" +
 106:             "   if (window.gACTModalDEIDs)\r\n" +
 107:             "      for (var vI = 0; vI < gACTModalDEIDs.length; vI++)\r\n" +
 108:             "      {\r\n" +
 109:             "         var vMDE = $find(gACTModalDEIDs[vI]);\r\n" +
 110:             "         if (vMDE._foregroundElement.style.display == '')\r\n" +
 111:             "         {\r\n" +
 112:             "            var vMDBounds = Sys.UI.DomElement.getBounds(vMDE._foregroundElement);\r\n" +
 113:             "            x = x - vMDBounds.x;\r\n" +
 114:             "            y = (textBoxBounds.y + textBoxBounds.height) - vMDBounds.y;\r\n" +
 115:             "            break;\r\n" +
 116:             "         }\r\n" +
 117:             "      }\r\n" +
 118:             "   Sys.UI.DomElement.setLocation(this.get_optionListControl(), x, y);\r\n" +
 119:             "\r\n" +
 120:             "   // enforce default scroll\r\n" +
 121:             "   this._ensureHighlightedIndex();\r\n" +
 122:             "   this._ensureScrollTop();\r\n" +
 123:             "\r\n" +
 124:             "   // show the option list\r\n" +
 125:             "   this.get_optionListControl().style.visibility = 'visible';\r\n" +
 126:             "\r\n" +
 127:             "}\r\n";
 128:             vClientScript.RegisterClientScriptBlock(typeof(Page), "ActComboBoxInMDE", vScript, true);
 129:  
 130:             vClientScript.RegisterStartupScript(typeof(Page), "ActComboBoxInMDEInit",
 131:                "Sys.Application.add_load(ActComboBoxInMDE_Init);\r\n", true);
 132:  
 133:             // on MDE popup, also fix comboboxes
 134:             vScript =
 135:             "function ActComboBoxInMDE_MDEPopupInit()\r\n" +
 136:             "{\r\n" +
 137:             "   if (window.gACTModalDEIDs)\r\n" +
 138:             "      for (var vI = 0; vI < gACTModalDEIDs.length; vI++)\r\n" +
 139:             "      {\r\n" +
 140:             "         var vMD = $find(gACTModalDEIDs[vI]);\r\n" +
 141:             "         vMD.add_shown(ActComboBoxInMDE_MDEPopupShown);\r\n" +
 142:             "      }\r\n" +
 143:             "}\r\n" +
 144:             "function ActComboBoxInMDE_MDEPopupShown(sender, args)\r\n" +
 145:             "{\r\n" +
 146:             "   if (window.ActComboBoxMadeVisible_All)\r\n" +
 147:             "      ActComboBoxMadeVisible_All();\r\n" +
 148:             "}\r\n";
 149:             vClientScript.RegisterClientScriptBlock(typeof(Page), "ActComboBoxInMDE_MDEPopupInitBlock", vScript, true);
 150:             vClientScript.RegisterStartupScript(typeof(Page), "ActComboBoxInMDE_MDEPopupInit",
 151:                "Sys.Application.add_load(ActComboBoxInMDE_MDEPopupInit);\r\n", true);
 152:          }
 153:       }
 154:    }

You can retrieve a zip file with C# and VB versions of this code here.

Here is how to use this class:

  • Call ComboBoxFixer.RegisterComboBox(combobox) for each ComboBox that is initially hidden. Generally this is done in Page_Load or if inside of a Databound control like ListView or FormView, from their ItemCreated event.
  • Call ComboBoxFixer.RegisterModalPopupExtender(modalpopupextender) for each ModalPopupExtender that contains ComboBoxes. If you have nested ModalDialogs, register the topmost first.
  • If you have a button that makes the comboboxes visible, add this javascript to its client-side onclick event. It must be run after the comboboxes are visible.
    ActComboBoxMadeVisible_All();

A little background on what these scripts do:

  1. The ComboBox initializes the sizes of its toggle button and list when the page is first loaded. If this happens when the control is hidden, its calculations do not get the correct sizing information as the clientWidth and clientHeight properties of DOM elements are usually 0 in this case. The ActComboBoxMadeVisible(cb) function resets properties that determine width. It explicitly calls a function to recalculate the button size and lets the ComboBox’s _popupShowing() method use the rest to know to recalculate.
  2. The ComboBox’s _popupShown() method calculates the x and y coordinates for the dropdownlist using the offset of its textbox from the upper left of the browser window. It uses style=”position:absolute;top:x;left:y;” to position it. This works well until the ModalPopupExtender gets involved. The ModalPopupExtender uses style=”position:fixed” and this appears to impact positioning. _popupShown() needs to position from the upper left of the <div> using that style=”position:fixed”.
    To fix this, _popupShown() is replaced by a clone generated by ComboBoxFixer.RegisterModalPopupExtender(). This new function inserts code that detects if a ModalPopupExtender is visible and adjusts the x and y coordinates based on its location.

7 Comments

  • I have added link to a zip file with C# and VB versions of this code into the blog. Also use this link:

    http://www.peterblum.com/Blog%20Files/ACTComboBoxFixer.zip

  • Nice job! I noticed this problem a couple days ago, was dreading having to go through and fix it. You should send this to Stephen Walther and his Ajax team to use this code... although, they should have already caught this.

  • Link to zip file is not working !!!

  • Thanks for the bug fix. It worked with AjaxToolkit Version 4.1.51116.0 just fine. My scenario was comboboxes inside of a CollapsiblePanelExtender that was initially hidden. One thing I did have to do to make your fix work was to set the CollapsedSize="1". It would not work when set to "0". Again Thanks, I have spent the good part of a day researching this problem.

  • Once again, you are the man! Thanks for posting this! I had the same problem, but I'm putting my combobox inside a div that uses jQuery to make it popup and visible. I ran your RegisterCombobox and ActComboBoxMadeVisible_All, but I didn't need to run the RegisterModalPopupExtender and it still worked great.

  • Code is not working, most likely I am not using it correctly. On my page I have combobox (CSS uses absolute), and it renders with displacement.

    I have used this:
    protected void Page_Load(object sender, EventArgs e)
    {
    jericho.classes.ComboBoxFixer.RegisterComboBox( cbmBride);
    }

    But it does not fix it still. Any help is appreciated. I am using 3.5 version of the Ajax tool kit.

  • If I understand "renders with displacement" correctly, you are describing the positioning problem shown in my second case, where a ModalPopupExtender is involved. My solution is only built for the ModalPopupExtender case. If you have that, you must also call Call ComboBoxFixer.RegisterModalPopupExtender().
    If you don't have it, my code is probably not relevant.
    Since Microsoft has reported a bug fix related to this, and it came out sometime in June 2012 (very recently!), make sure you are on that build to see if it resolves issues. If it does not, please create a bug report here: http://ajaxcontroltoolkit.codeplex.com/workitem/list/basic.

Comments have been disabled for this content.