Introduction
CascadingDropDownExtender
is one of the most popular AjaxControlToolkit extenders. But, because there is a help article about the server-side part of the control and almost nothing about the client-side (that really makes it work), developers became baffled when they needed to add some advanced event handling or customization.
I think that if you understand how it works, it is much easier to implement your own functionality. So, in this article, I'll describe how the CascadingDropDown
works, and then I'll try to answer some common questions.
Little server
To render it shortly for those who do not know - CascadingDropDown
– is an extender control used to fill the data in one DropDownList
depending on the value chosen in another DropDownList
. It gets all the necessary data through AJAX using a web method. Now, let’s consider its organization in more details.
So, the CascadingDropDown
is an extender for DropDownList
. As the target control, we set the "dependent" (or child) DropDownList
(i.e., the one that is going to be filled). To set the ID of the parent control, we use the ParentControlID
property, which, by the way, is not required (i.e., there can be no parent DropDownList
). Other required properties (in addition to TargetControlID
) are Category
and ServiceMethod
. The ServiceMethod
contains the name of the web method used by CascadingDropDown
, and Category
fulfills the role of filter name and will be passed to the web method as part of one of the parameters. The optional properties are:
PromptText
(text to display before the user has selected a value from the DropDownList
),PromptValue
(value set when PromptText
is displayed),EmptyText
(text to display when the DropDownList
has no data to display),EmptyValue
(value set when EmptyText
is displayed),LoadingText
(text which is displayed during the loading),ServicePath
(the path to web-service; if you do not declare it, the PageMethod
will be used instead),ContextKey
(place for storing any additional information),UseContextKey
(tells you to pass or not to pass ContextKey
as one of the properties to the web-method),SelectedValue
(default value for the child DropDownList
).
Apart from the above described properties, the CascadingDropDown
has a couple of helper static methods which can be used in web methods, but they will be discussed later. All that the CascadingDropDown
does on the server-side is collect the values of the above-described properties into the script descriptor and «sends» it to the related client class that is derived from Sys.UI.Behavior
(exactly – from AjaxControlToolkit.BehaviorBase
). Then, all the main process runs on the client-side:
Big client
In the initialize
method, override the handlers subscribed on the change
event of the child and parent (if defined) select
DOM elements. Also, if the parent select
is specified, there appears a kind of bond between the parent and child select
s: the child select
receives the CascadingDropDownParentControlID
field that refers to the parent select
, and the parent select
gets (if it already does not have it) an array field called childDropDown
, to which a reference to the child select
is added. Besides all these, during the initialization stage, all options are removed from the child select
DOM element, the CascadingDropDownCategory
field (which will contain the value of the Category
property) is added to it, the text from PromptText
or LoadingText
properties is put in the parent select
, and some other minor actions are performed. Also, if the child select
has no parent, or the parent is specified and in the moment initialize is called, its selected value isn’t equal to PromptValue
or EmptyValue
, the child select
will be filled by data (i.e., there will be the same action as when the user changes the value of the parent select
). So, it’s high time to look how the data filling occurs.
Everything begins in the _onParentChange
method (notice the underline prefix - this method isn't for public usage). As it was already said, the web method passes 2 or 3 parameters depending on the UseContextKey
value. These arguments are knownCategoryValues
, category
, and contextKey
. All these arguments are strings. It is logical that _category
and _contextKey
field values are passed as second and third parameters. The value for the first one is more interesting. It is formatted as follows: if the child select
has no parent, the value is null
. If the parent select
is specified, then its CascadingDropDownCategory
field value and selected value are added to knownCategoryValues
. If the parent select
has the CascadingDropDownParentControlID
field specified, then the parent of the parent select
is taken and the above described actions are repeated, and this continues to the end of the hierarchy. New pairs are added to the beginning of knownCategoryValues
, and, as a result, knownCategoryValues
will look like this:
'category1:value1;category2:value2;...'
Here, I would like to take your attention to some unobvious fact. To make this all work, the parent select
must have the CascadingDropDownCategory
field specified. If there is a CascadingDropDownBehavior
client control that has the parent select
as the target control, this field will be specified automatically. Otherwise, you will have to define the field yourself.
But let’s return to the web method. It is called not through the generated proxy, but directly – through the Sys.Net.WebServiceProxy.invoke
method. After collecting all the parameters for the web method, the populating
(Object
, Sys.CancelEventArgs
) client-side event is raised, and if the Sys.CancelEventArgs.cancel
property wasn’t set to false
in the handlers, the request is sent to the server. The web method should return the array of CascadingDropDownNameValue
objects.
If the web method call fails, then the information about the error (for some unknown for me reason) will be put in the child select
as options. If the web method call returns successfully, then the child select
options collection will be filled with the received values, and if among one of the received objects, the CascadingDropDownNameValue.value
field equals to SelectedValue
, or CascadingDropDownNameValue.isDefaultValue == true
, its value will be selected. Regardless whether there were or were no mistakes, in the end, the populated
(Object
, Sys.EventArgs
) client-side event is raised. That’s all, the update is finished.
It’s worth mentioning, that at any change of the selected value in the child select
, the SelectionChanged(Object, AjaxControltoolkit.CascadingDropDownSelectionChangedEventArgs)
client-side event is raised:
At the moment of writing the article, there was a bug. The LoadingText
is set before the populating
event is raised, and if the request was cancelled, LoadingText
remains in the child select
instead of promptText
or old options, and this can make the user puzzled.
And when we know more...
Now, when we have more or less got into the principle of work, we can make some conclusions:
- As
CascadingDropDown
works via a web method (web service method or PageMethod
), it has nothing to do with the server-side or client-side page life-cycles. And, consequently, when the select
DOM element is updated with CascadingDropDown
, there will be no postbacks, no ASP.NET validators will act, no events of life-cycle will be raised, no SelectedIndexChanged
will work etc. Besides, for the work of CascadingDropDown
, you do not need to put it in UpdatePanel
– it won’t use it all the same. - As
CascadingDropDown
works via a web method, the order of parameters is unimportant. What is important is that the signature of the web method matches the types and the names of the parameters. I.e., the names of the arguments must be the same as given in the documentation. And again, it is indifferent what you use – web service method or PageMethod
. CascadingDropDown
has no server logic (web method is not counted because it is not part of a control). All that the server-side part of the control performs is help us to simplify the creation of the client-side part. That is why CascadingDropDownBehavior
can be created and used separately from server-side and, more than that, without ASP.NET 2.0 at all. Just add the required scripts to the page and use the $create method:
Sys.Application.add_init(function() {
$create(
AjaxControlToolkit.CascadingDropDownBehavior,
{
"Category":"Model",
"ClientStateFieldID":"hiddenFieldClientStateId",
"ParentControlID":"parentDropDownList",
"PromptText":"Please select a model",
"ServiceMethod":"GetDropDownContents",
"ServicePath":"MyService.asmx",
"id":"exCascadingDropDown"
},
null,
null,
$get("childDropDownList")
);
});
The only thing that we haven’t considered here is the ClientStateFieldID
property. We need to pass the ID of some hidden-column to this property, so CascadingDropDownBehavior
can use this hidden field to store the selected value during postbacks (it is called “ClientState”).
- Unlike
UpdatePanel
, requests sent through Sys.Net.WebServiceProxy.invoke
are performed independently from each other. That is why we can have concurrency problems on the client if request executions can take a long time. For example: the user has selected some value in the parent select
. This action will send the first request. Without waiting for the answer, the user chooses some other value in the same parent select
. And here we get the second request. Let’s suppose that to perform the second request, significantly less time is needed than for performing the first one. And as a result, the answer to the second request is given first. The child select
has been filled, the user has made up his mind to choose something in it, and at this moment, the answer to the first request is given, and the select
is filled with non-valid-at-that-moment values. As a result, the values in the child select
do not correspond to the choice in the parent select
. I do not know whether or not we can consider this as a bug. However, we will have to resolve this situation on our own. - In order to perform client-side validation, control the priority of requests, and to show and hide an animated GIF instead of a “Loading” message, we need to use the
populating
and populated
client-side events. Again, to make it clear: we must subscribe to the events of the CascadingDropDownBehavior
that has the child select
as its target element, but these events will be raised when the user changes selection in the parent select
. - Because it is required to have the
CascadingDropDownCategory
property specified, it is more preferable (though not required) to fill the top DropDownList
with CascadingDropDown
instead of classic databinding - The location of
CascadingDropDown
in the web form (and, consequently, the order of their initialization) plays an important role. CascadingDropDown
, filling the parent select
, should be initialized (and placed) first.
And in the end, about those two helper methods I’ve mentioned in the beginning:
- The
ParseKnownCategoryValuesString
method parses the knownCategoryValues
web method argument from String
to StringDictionary
. That is rather useful. - The
QuerySimpleCascadingDropDownDocument
method can help you to retrieve a collection of CascadingDropDownNameValue
objects from a dataset, based on an existing category/value StringDictionary
. I really don’t like datasets, so I don’t use this method. But if you like them, you can see this tutorial.