Extended DropDownList Control

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

It’s a very common situation.  A dropdownlist has a parent whose selected value determines what options will be bound to it.  But what happens when there are none?  And what happens if there’s only one option?  And why do I have to enter code to insert “Select an Item” to the list?  And why does it continue to display even when no item has been selected on its parent and it doesn’t have any values at all?  And why does it continue to display immediately after the parent’s value has been changed and the page is reloading?

Well, what if it wasn’t that way?  What if it accounted for all those things?  That would be really nice, and, in fact, it actually is quite nice.

This extended dropdownlist control has the following additional parameters:

  • ParentDropDownListID – Specifies the id field of the parent dropdownlist.
  • ParentItemUnpopulatedText – Specifies the text to display when the parent dropdownlist has yet to be populated.  The dropdownlist itself will be hidden and this text appears in its place, giving the user appropriate feedback.  The default is “n/a” since this dropdownlist really isn’t applicable until its parent is populated.  (This comes up in a series of three or parent/child dropdownlists.)
  • ParentItemNotSelectedText -Specifies the text to display when the parent dropdownlist has been populated but no item has yet been selected (The selected item is “Select an item”.)  So, for a state/city relationship it could say “Please select a state” or “n/a”–whatever you want.  But the dropdownlist itself is hidden, so the user can’t select it and wonder why there’s nothing in it to select.
  • ControlsToHideOnChange – This actually specifies children dropdownlists to hide whenever the selected item changes.  In a state/city relationship, if you change the state, the city dropdownlist will need to be repopulated.  However, sometimes the page takes a little while to load and the user may try to select a city before that gets repopulated.  To avoid the confusion, the city dropdownlist, whose ID is specified in this parameter, will be immediately hidden via JavaScript and a message will appear in its place indicating that the user needs to wait until it is repopulated.  You can actually specify multiple children to hide, so if you had county and city children, both would be hidden while the page reloads.  The text is specified in the ChildrenRepopulatingText.
  • ChildrenRepopulatingText – The text that replaces the child controls specified in ControlsToHideOnChange whenever the selected item for this control (the parent) changes.
  • HasSelectedItemText – true/false – defines whether or not a “Select Item” option should be added to the dropdownlist.  I will probably eliminate this and just add one based on whether or not the SelectItemText parameter is specified.
  • SelectItemText – Specifies the text to add as the first item (with a value of -1) to the dropdownlist.
  • NoItemsText – Specifies the message to display if there are no items in the dropdownlist, since an empty dropdownlist is annoying.  The actual dropdownlist will be hidden and this text will display.  So, this could be “There were no cities defined for the state you selected.”

Additionally, if there is only one item bound to the dropdownlist, the dropdownlist goes ahead and selects that item.  The dropdownlist itself is hidden, and the item text is displayed as a message.  It is just annoying when you have a dropdownlist with a single item, but you still have to select it because you have a “Select Item” option that is the initially selected item.

So, that’s the extended control as it is now.  Hopefully, it will save coding time and provide a better overall user experience.
NOTE: The control as it is written does use Jquery commands as well, so if you’re not referencing that, you’ll need to rewrite the JavaScript.


public class ExtendedDropDownList : DropDownList
{
    public bool HasSelectItemText
    {
        get
        {
            object o = ViewState["HasSelectItemText"];
            if (o == null)
                return false;
            else
                return (bool)o;
        }
        set
        {
            ViewState["HasSelectItemText"] = value;
        }
    }

    public string SelectItemText
    {
        get
        {
            object o = ViewState["SelectItemText"];
            if ((o == null) || ((string)o == ""))
                return "Select an Item";
            else
                return (string)o;
        }
        set
        {
            ViewState["SelectItemText"] = value;
        }
    }

    public string NoItemsText
    {
        get
        {
            object o = ViewState["NoItemsText"];
            if ((o == null) || ((string)o == ""))
                return "No items available to select";
            else
                return (string)o;
        }
        set
        {
             ViewState["NoItemsText"] = value;
        }
    }

    public string ParentDropDownListID
    {
        get
        {
            if (ViewState["ParentDropDownListID"] == null)
                ViewState["ParentDropDownListID"] = "";
            return (string)ViewState["ParentDropDownListID"];
        }
        set
        {
            ViewState["ParentDropDownListID"] = value;
        }
    }

    public string ParentItemNotSelectedText
    {
        get
        {
            if (ViewState["ParentItemNotSelectedText"] == null)
                ViewState["ParentItemNotSelectedText"] = "";
            return (string)ViewState["ParentItemNotSelectedText"];
        }
        set
        {
            ViewState["ParentItemNotSelectedText"] = value;
        }
    }

    public string ParentItemUnpopulatedText
    {
        get
        {
            if (ViewState["ParentItemUnpopulatedText"] == null)
                ViewState["ParentItemUnpopulatedText"] = "n/a";
            return (string)ViewState["ParentItemUnpopulatedText"];
        }
        set
        {
            ViewState["ParentItemUnpopulatedText"] = value;
        }
    }

    public string ControlsToHideOnChange
    {
        get
        {
            if (ViewState["ControlsToHideOnChange"] == null)
                ViewState["ControlsToHideOnChange"] = "";
            return (string)ViewState["ControlsToHideOnChange"];
        }
        set
        {
            ViewState["ControlsToHideOnChange"] = value;
        }
    }

    public string ChildrenRepopulatingText
    {
        get
        {
            if (ViewState["ChildrenRepopulatingText"] == null)
                ViewState["ChildrenRepopulatingText"] = "Repopulating Options...";
            return (string)ViewState["ChildrenRepopulatingText"];
        }
        set
        {
            ViewState["ChildrenRepopulatingText"] = value;
        }
    }

    public string[] ControlsToHide
    {
        get
        {
            return ControlsToHideOnChange.Split(',');
        }
    }

    public override void DataBind()
    {
        base.DataBind();

        int itemCount = this.Items.Count;
        if (HasSelectItemText && itemCount > 1)
            this.Items.Insert(0, new ListItem(SelectItemText, "-1"));
        if (itemCount == 0)
            this.SelectedIndex = -1;
        else
            this.SelectedIndex = 0;
    }

    protected override void CreateChildControls()
    {
        base.CreateChildControls();
        if (ControlsToHideOnChange != "")
        {
            ServerPage page = GetPage();
            foreach (string s in ControlsToHide)
            {
                this.Attributes["onchange"] = "$('#" + page.FindControl(s).ClientID + "').hide();" + "$('#" + page.FindControl(s).ClientID + "').parent().text('" + ChildrenRepopulatingText + "');" + this.Attributes["onchange"];
            }
        }
    }

    protected override void Render(System.Web.UI.HtmlTextWriter writer)
    {
        EnsureChildControls();

        DropDownList parent = (DropDownList)GetPage().FindControl(ParentDropDownListID);
        bool hasUnselectedParent = (ParentDropDownListID != "") && (parent != null) && (parent.Items.Count > 0) && (parent.SelectedValue == "-1");
        bool hasUnpopulatedParent = (ParentDropDownListID != "") && (parent != null) && (parent.Items.Count == 0);

        writer.WriteLine("<div>");

        if (hasUnselectedParent)
        {
            this.Items.Clear();
            writer.WriteLine("<span>" + ParentItemNotSelectedText + "</span>");
            this.Attributes.CssStyle["display"] = "none";
        }
        else if (hasUnpopulatedParent)
        {
            this.Items.Clear();
            writer.WriteLine("<span>" + ParentItemUnpopulatedText + "</span>");
            this.Attributes.CssStyle["display"] = "none";
        }
        else
        {
            if (this.Items.Count == 0)
            {
                writer.WriteLine("<span>" + NoItemsText + "</span>");
                this.CssClass = "hide";
            }
            else if (this.Items.Count == 1)
            {
                writer.WriteLine("<span>" + this.Items[0].Text + "</span>");
                this.CssClass = "hide";
            }
        }

        base.Render(writer);

        writer.WriteLine("</div>");
    }

    private Foliotek.Components.ServerPage GetPage()
    {
        System.Web.UI.Control control = this;
        while (control.GetType() != Type.GetType("Foliotek.Components.ServerPage"))
        {
            control = control.Parent;
        }
        return (Foliotek.Components.ServerPage)control;
    }
}
Advertisements

Getting the width of a hidden element with jQuery using width()

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

UPDATE #3: It needs to be noted that the fix introduced in jQuery 1.4.4 is only for the width() and height() methods.  If you need inner/outer dimensions the method below still needs to be used.  I have updated the method to return height/width, outer height/width, and inner height/width.  There is an optional parameter to include margins in the outer dimension calculations.  Thanks Ryan and Fred for the heads up.

UPDATE #2: jQuery 1.4.4 was released today (11/11/2010) and included in the release was an update to the width() and height() methods.z  Each method will now return the correct dimension of the element if it is within a hidden element.  For further information, you can view the bug report.

UPDATE #1: Based on the feedback in the comments regarding the use of the swap method, I am updating this post with a solution from Ryan Wheale.  He created a function to return the dimensions of an element that is hidden or nested within 1 more hidden elements.  Here is the code that he posted below in his comment:

//Optional parameter includeMargin is used when calculating outer dimensions
(function($) {
$.fn.getHiddenDimensions = function(includeMargin) {
    var $item = this,
        props = { position: 'absolute', visibility: 'hidden', display: 'block' },
        dim = { width:0, height:0, innerWidth: 0, innerHeight: 0, outerWidth: 0, outerHeight: 0 },
        $hiddenParents = $item.parents().andSelf().not(':visible'),
        includeMargin = (includeMargin == null)? false : includeMargin;

    var oldProps = [];
    $hiddenParents.each(function() {
        var old = {};

        for ( var name in props ) {
            old[ name ] = this.style[ name ];
            this.style[ name ] = props[ name ];
        }

        oldProps.push(old);
    });

    dim.width = $item.width();
    dim.outerWidth = $item.outerWidth(includeMargin);
    dim.innerWidth = $item.innerWidth();
    dim.height = $item.height();
    dim.innerHeight = $item.innerHeight();
    dim.outerHeight = $item.outerHeight(includeMargin);

    $hiddenParents.each(function(i) {
        var old = oldProps[i];
        for ( var name in props ) {
            this.style[ name ] = old[ name ];
        }
    });

    return dim;
}
}(jQuery));

This basically performs the same operations as the swap method.  This is safer to use in case the swap method is removed from the jQuery core.

I have tested this in multiple cases and each time the correct results were returned.

Thanks Ryan.

————————————————————————————————————————————-

Original post

I recently ran into a problem with jQuery’s width().  The problem is with a visible element that is inside a hidden element will return a value of 0 instead of its’ actual calculated width.  After messing around with it for a little bit I finally came up with a solution.  The method I used involved adding some CSS properties to the hidden element.  The CSS properties involved are position, visibility, and display.

//$HiddenItem is the element that is wrapping the element that you want the width of
//$Item is the element you want the width of

$HiddenItem.css({
    position: "absolute",
    visibility: "hidden",
    display: "block"
})
$Item.width();

$HiddenItem.css({
    position: "",
    visibility: "",
    display: ""
})

After setting the above CSS properties on the element, you can then call width() and the correct value will be returned. After you call the width() method you should clear the properties in order to return the element to the way it was.

Setting the properties to an empty string is probably not the best way to do it though. What if there was a position value already set? Using this method would clear out that initial values of the CSS properties.

I found the swap() method to be handy in this situation. I found this method while looking through the jQuery source code.

 // A method for quickly swapping in/out CSS properties to get correct calculations swap: function( elem, options, callback ) {      var old = {};       // Remember the old values, and insert the new ones      for ( var name in options ) {           old[ name ] = elem.style[ name ];           elem.style[ name ] = options[ name ];      }       callback.call( elem );       // Revert the old values      for ( var name in options ){          elem.style[ name ] = old[ name ];      } } 

By using the swap method, the old CSS properties will be remembered and reapplied after finding the width of the element. The swap method takes in 3 parameters:

  1. The element that you would like to swap the CSS properties on
  2. CSS key/value pairs that you want to change
  3. A callback function to call after the properties are set

To rewrite the above to use the swap method I would do the following:

var props = { position: "absolute", visibility: "hidden", display: "block" };
var itemWidth = 0;

$.swap($HiddenItem[0], props, function(){
     itemWidth = $Item.width();
});

//Use itemWidth

I coded up a small example on jsbin. Here is the link http://jsbin.com/ofine3/2.

$HiddenItem.width();

The non-breaking space ” “

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

This post may sound elementary to those of you who went through college after the web development was in full swing.  Admittedly, that was just slightly before my time.  So, what was a small epiphany for me may be common knowledge to you, but please pardon my ignorance and delight at what I learned.

Sometimes you just need a little more space somewhere, and fortunately, sometime long ago when I was just starting to work on the web, someone introduced me to the beloved “&nbsp;”.  Since that time, I have used it occasionally whenever it was too tedious to add space another way.

However, I recently needed to have a cell in a header row define the space for the column.  I don’t recall the exact circumstance now, but for whatever reason, defining no wrap wasn’t available.

Enter the often-used but seldom understood &nbsp;.  I needed the text in this column to not break at the spaces.  I was working with another person, when it occurred to me that the non-breaking space is probably a non-breaking space.

I replaced the spaces with &nbsp; and the problem was solved!