Jeff Garoutte

c# .net and anything else that happens across my desk

The DropDownList, the DataBind and the Missing Value

I sat there and blinked at the ArgumentOutOfRangeException.  It was just a DropDownList that had the selected value data bound inside a FormView.  How can we prevent this from happening?  I did a little digging on how to handle this odd "spot" and I found this article on a subclass of the DropDownList control.  It is an interesting article, and I almost went this route but it has a draw back I did not like.  The articles solution is to add the missing value to the DropDownList in an over-ridden OnDataBinding method. 

It is a clean solution and it works.  It could be easily compiled into an assembly and if you were feeling really ambitious it could modified to fire a "ValueNotFoundEvent" where you could change the value or bubble up an exception that includes the value that the drop down list does not have; with those features added and removing the reference to DataRowView so it could work with an ObjectDataSource I think it would be a useful control in everyone's toolbox.

Consider the following DropDownList and FormView

<asp:FormView ID="FormView1" runat="server" DataSourceID="ObjectDataSource1">
        <ItemTemplate>
            <asp:DropDownList ID="ddlSomething" runat="server" 
                SelectedValue='<%# Bind("Value") %>'>
                <asp:ListItem>-choose-</asp:ListItem>
                <asp:ListItem>one</asp:ListItem>
                <asp:ListItem>two</asp:ListItem>
                <asp:ListItem>three</asp:ListItem>
                <asp:ListItem>five</asp:ListItem>
            </asp:DropDownList>
        </ItemTemplate>
    </asp:FormView>
    <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" 
        OldValuesParameterFormatString="original_{0}" SelectMethod="GetTest" 
        TypeName="UserInterface.TestSource"></asp:ObjectDataSource>

For reference, test and TestSource are as follows

namespace UserInterface
{
    [DataObject(true)]
    public class test
    {
        public test(String value)
        {
            _value = value;
        }
        private string _value;
        
        [DataObjectField(false,false,false)]
        public string Value
        {
            get { return _value; }
            set { _value = value; }
        }
    }
    [DataObject]
    public class TestSource
    {
        [DataObjectMethod(DataObjectMethodType.Select)]
        public test GetTest()
        {
            test result = new test("four");

            return result;
        }

    }
}

Please make note, I'm not going into detail on DataSources, the DataObjectAttribute or how many of my own (or common best) practices I violated in the samples in this article, let's leave it at I feel like I need a shower having written it and move on.

If you plug that all in and run it you will see something like this...

image

If you're in a crunch and cant add a custom control to your solution there is a way to handle this with an event.

Alter the DropDownList to look this...

<asp:DropDownList ID="ddlSomething" runat="server" 
                SelectedValue='<%# Bind("Value") %>' OnDataBinding="DropDownList_DataBinding">

and add the following into the pages code behind...

   1:  protected void DropDownList_DataBinding(object sender, EventArgs e)
   2:          {
   3:              DropDownList theDropDownList = (DropDownList)sender;
   4:              theDropDownList.DataBinding -= new EventHandler(DropDownList_DataBinding);
   5:              try
   6:              {
   7:                  theDropDownList.DataBind();
   8:              }
   9:              catch(ArgumentOutOfRangeException)
  10:              {
  11:                  UserInterface.test item=
                          (UserInterface.test)((IDataItemContainer)theDropDownList.NamingContainer).DataItem;
  12:                  //do whatever here, in this case we will just add the item
  13:                  theDropDownList.Items.Add(item.Value);
  14:              }
  15:          }

Now you will get the list and "four" will be added and selected.  Replace line 13 with whatever you would like to do for checks and tests.

ArgumentOutOfRangeException does have a property called "ActualValue" but the .net framework does not fill this this property when throwing the exception. 

The ActualValue property is not used within the .NET Framework class library. It carries a null value in all the ArgumentOutOfRangeException objects thrown by the .NET Framework class library. The ActualValue property is provided so that applications can use the available argument value.

Why does this work? The DataBinding event fires before the DataBind happens. 

  • On line 3 we type cast the sender as a DropDownList, because the event is wired into the DropDownList on the aspx page we know this the case (you could add a type check if you wanted). 
  • On line 4 we remove the event handler to avoid an infinite loop because calling DataBind on the DropDownList (line 7) will fire the DataBinding event again. 
  • With line 5 to 8 we try to DataBind the DropDownList.
  • Line 9 catches the only exception we are interested in, the ArgumentOutOfRangeException.  Any other exception should bubble up.  you could add a 2nd catch block under the ArgumentOutOfRangeException block like catch(Exception) and handle it there if you did not want it to bubble up.  Notice I do not use the ArgumentOutOfRangeException that was thrown anywhere and that I did not make a variable for it.  This avoids the warning "variable err is declared but never used" that you get when you use "catch(ArgumentOutOfRangeException err)".
  • Line 11 get the current UserInterface.test item that the FormView is attempting to bind.  By replacing UserInterface.test with the correct object you can reuse this snip of code. In theory one should be able to use generics with this but I have not tested or tried that.  If I was going to reuse this enough to try generics; I would just subclass the DropDownList, override the DataBinding method and fire a custom event when the value was not in the items collection instead of tinkering with Generics.  If ActualValue actually had the value in it, this this would be unneeded, declaring the variable on line 9 would allow us to use err.ActualValue.
  • Line 13 adds the value to the DropDownList.

This hack is quick and clean.  It leaves a very small footprint in the code; but you can not change the value, it does not work.  Really, a sub classed DropDownList that fires off an event that is able to return back the new value to use is a better solution to the problem.

How does that look? I declared the class within the UserInterface namespace

    public class DataBindDropDownList : System.Web.UI.WebControls.DropDownList
    {
        public class ValueNotFoundArgs : EventArgs
        {
            public ValueNotFoundArgs(string value)
                : this(value, false)
            {
            }

            public ValueNotFoundArgs(string value, Boolean addNewValue)
                : base()
            {
                _addValue = addNewValue;
                _value = value;
                _displayName = value;

            }
            private string _value;

            public string Value
            {
                get { return _value; }
                set { _value = value; }
            }

            private Boolean _addValue;

            public Boolean AddValueIfItDoesNotExist
            {
                get { return _addValue; }
                set { _addValue = value; }
            }

            private string _displayName;

            public string DisplayName
            {
                get { return _displayName; }
                set { _displayName = value; }
            }
        }

        public delegate void ValueNotFoundEventHandler(object sender, ValueNotFoundArgs e);

        public event ValueNotFoundEventHandler ValueNotFound;
        private string _cachedValue = "";
        public override string SelectedValue
        {
            get
            {
                return base.SelectedValue;
            }
            set
            {
                base.SelectedValue = value;
                _cachedValue = value;
            }
        }
        private void ThrowArgumentOutOfRangeException(string paramName, string value)
        {
            throw new ArgumentOutOfRangeException(paramName, value, null);
        }

        protected override void OnDataBinding(EventArgs e)
        {

            try
            {
                base.OnDataBinding(e);
            }
            catch (ArgumentOutOfRangeException err)
            {
                if (ValueNotFound != null)
                {
                    ValueNotFoundArgs notFoundArgs = new ValueNotFoundArgs(_cachedValue, false);
                    ValueNotFound(this, notFoundArgs);

                    this.ClearSelection();

                    System.Web.UI.WebControls.ListItem item = this.Items.FindByValue(notFoundArgs.Value);
                    if (item != null)
                        item.Selected = true;
                    else if (notFoundArgs.AddValueIfItDoesNotExist)
                    {
                        item =
                           new System.Web.UI.WebControls.ListItem(notFoundArgs.DisplayName, notFoundArgs.Value);
                        item.Selected = true;
                        this.Items.Add(item);
                    }
                    else
                    {
                        ThrowArgumentOutOfRangeException(err.ParamName, notFoundArgs.Value);
                    }
                }
                else
                {
                    ThrowArgumentOutOfRangeException(err.ParamName, _cachedValue);
                }
            }
        }
    }

and the aspx becomes...

<cc1:DataBindDropDownList ID="ddlSomething" runat="server" OnValueNotFound="DataBindDropDownList_ValueNotFound" 
      SelectedValue='<%# Bind("Value") %>' >
      <asp:ListItem>-choose-</asp:ListItem>
      <asp:ListItem>one</asp:ListItem>
      <asp:ListItem>two</asp:ListItem>
      <asp:ListItem>three</asp:ListItem>
      <asp:ListItem>five</asp:ListItem>
</cc1:DataBindDropDownList>

and in the pages code behind...

protected void DataBindDropDownList_ValueNotFound(object sender, 
        UserInterface.DataBindDropDownList.ValueNotFoundArgs e)
        {
            e.AddValueIfItDoesNotExist = false;
            e.Value = "6";
        }

The aspx and page code behind do not look a whole lot different.  Depending on your assemblies/app_code directory the tags in your aspx may look different.   I can still get the same ArgumentOutOfRangeException as before; expect now I get the ActualValue property populated when the exception is thrown. 

image

Change the page code behind slightly...

protected void DataBindDropDownList_ValueNotFound(object sender, 
        UserInterface.DataBindDropDownList.ValueNotFoundArgs e)
        {
            e.AddValueIfItDoesNotExist = true;
            e.Value = "6";
        }

and the missing value is added to the DropDownList.

More importantly the ValueNotFound event allows a logic layer to be hooked in to change the value or add the value to the list.  There is no need to worry about DataSets, ObjectDataSources, DataRows, DataRowViews or type casting. 

How does it work?

To understand what is going on taking a look at the ListControl with Reflector helps a great deal.  Basically, ListControl's set accessor for SelectedValue checks to see if the items collection has been populated yet and puts the new selected value into a cache until the controls DataBind method is invoked.  Once the items list is built the cached value is pulled out and made active or throws an exception if it is not in the items collection.  The DataBindDropDownList  does the same thing, it caches the selected value and if the base class (DropDownList) raises the ArgumentOutOfRangeException it fires the ValueNotFoundEvent.  If the event is unused a ArgumentOutOfRangeException with the ActualValue populated is thrown.  If the event is used the value in the events ValueNotFoundArgs is evaluated.  If it finds the value, which the event may have changed, in the DataBindDropDownList items it is set to be the selected item.  If the value is not found it checks to see if AddValueIfItDoesNotExist is true.  If it is we create the ListItem with the value and the specified DisplayName.  If AddValueIfItDoesNotExist is false a ArgumentOutOfRangeException is thrown with the value from the ValueNotFoundArgs as the ActualValue because that was the last value we tried to set.

Between the 3 different ways outlined here, the original method from attractor's article, the quick event hack and the event based sub-class of the DropDownList control you should be able to address the problem of the missing value in a data bound DropDownList in a way that fits your code/environment.

Back when I used Visual Studio 2005 I would use the quick event hack because of the "issues" getting VS2005 to see controls in an assembly and display them in the toolbox.  Because 2008 has addressed the "common issues" with the toolbox and assemblies I would rather build the DataBindDropDownList in an assembly, add the reference to the project and use it.

Happy coding.

kick it on DotNetKicks.com

Comments

Ira United States said:

Ira

Great article Jeff. It is a great qwerk to keep in mind when using the DropDownList.

# July 10 2008, 22:12

David Australia said:

David

Thanks so much for this. Ive been looking for a good solution like this for ages.

# September 10 2008, 14:23

Fritz Praus United States said:

Fritz Praus

Hey I followed your snip it of code(quick event hack) that adds the missing data value to the dropdownlist after I catch the exception. I set the selectedvalue, selectedindex, and text of the dropdownlist and I still get the data exception error/stack message coming up to my webpage. I thought once I handled the exception and added the data item to the dropdownlist values the exception would go away. Any suggestions would be greatly appreciated. I am using VB instead of C#, I am sure that doesn't really matter. Thanks

# April 21 2009, 09:03

Jeff United States said:

Jeff

Hey Fritz
Just set the SelectedValue.  Are you inside a templated control when you are making the call?

if the error is still bubbling up, odds are you have some other error happening besides a ArgumentOutOfRangeException.  Add a catch(excpetion) {} block, set a break point on it and look at Exception in $exception the locals or watch windows while debugging.

# May 10 2009, 02:26

tatuaggi United States said:

tatuaggi

Great post! I am just starting out in community management/marketing media and trying to learn how to do it well - resources like this article are incredibly helpful. As our company is based in the US, it?s all a bit new to us. The example above is something that I worry about as well, how to show your own genuine enthusiasm and share the fact that your product is useful in that case.

# January 07 2010, 10:11

Albert United States said:

Albert

Sorry for my english...
I catch the ArgumentOutOfRangeException in the DataBinding event ..so I don't have problems with the null values or missing values...so far so good.
I'm getting my ListItems from a SqlDataSource..If my SelectedValue match with one of the values in the ListItems, the DropDownList doesn't show the selected value...this is what I think is happening: The first call to Databind method (when the Databinding event rises the first time) bounds the ListItems and meets the SelectedValue with the actual Bind or Eval value..
The second time (when you call DataBind() explicitly in your try catch) the list bounds again but this time the Bind value is obviated..If you especify AppendDataBoundItems="True" you will be able to see the list two times but the SelectedValue will be the right one...
How do I avoid this?

# February 17 2010, 22:52

Albert United States said:

Albert

This one works for me...
<aspLaughingropDownList ID="MatsDrp" runat="server" DataSourceID="SqlMaterials" DataTextField="MatName" DataValueField="IdMat" SelectedValue='<%# Bind("IdMat") %>' AppendDataBoundItems="True"                            ondatabinding="MatsDrp_DataBinding" ondatabound="MatsDrp_DataBound"> </aspLaughingropDownList>

protected void MatsDrp_DataBinding(object sender, EventArgs e)
    {
        DropDownList drpNull = sender as DropDownList;
        drpNull.DataBinding -= new EventHandler(FIntDropDown_DataBinding);
        try
        {
            drpNull.DataBind();
        }
        catch (ArgumentOutOfRangeException)
        {
            drpNull.SelectedIndex = 0;
            drpNull.SelectedValue = drpNull.Items[0].Value;
            drpNull.AppendDataBoundItems = false;
        }        
    }
    private int drpCnt;
    protected void FIntDropDown_DataBound(object sender, EventArgs e)
    {
        DropDownList drpNull = sender as DropDownList;
        int actualCnt = drpNull.Items.Count;
        if (drpCnt == 0)
            drpCnt = actualCnt;
        else
        {
            for (int i = actualCnt - 1; i >= drpCnt; i--)
                drpNull.Items.RemoveAt(i);
            drpCnt = 0;
        }
    }

//I do not think this is the best or the most optimal solution but is one that works.... Could be improved? Thanks in advance.

# February 18 2010, 00:26

jeff United States said:

jeff

@Albert
Using ondatabinding="MatsDrp_DataBinding" will work.  Is it the most "optimal" solution to the problem?  It depends on what you want it to do.  Really, both solutions I showed deal with catching and handeling an exception; they just go about it differently.

Ideally, because we know there could be an exception during the databind, looking to see if the selected value exists in the data before binding might be the best solution.  But I was not able to find a way to do that with a lower overhead than just catching the exception.



# February 18 2010, 12:27

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
Loading