AjaxControlToolkit (ASP.NET/C#) : CascadingDropDown Extender - Part 2



Source Code

In the previous part of this post we had a quick look at how to use the CascadingDropDown extender, in this part we're going to look at other perhaps more interesting properties but more in particular databinding (something which seems to be a common question about this extender control).

If you ever attempted to use traditional databinding (e.g. binding your DropDownList to a datasource) in conjunction with this extender, you might have noticed your items being overridden, the reason being that the extender takes control of the DropdownList being extended and performs its own databinding.

Note that it performs binding client side (even on initial binding), like seen in the following firebug screenshot:

Cascade Firebug trace
(would have been nice if initial binding didn't perform client side calls and rather retrieved the state of the DropDownLists as part of the main page request)

So basically we're going to have to leave it up to the extender to perform databinding.

Furthermore if we need to provide a default value (e.g. previously selected values), we can do that by setting the SelectedValue property on the extender, like seen in the following snippet.

 
<asp:CascadingDropDown ID="cddlCountries" runat="server" TargetControlID="ddlCountry"
    Category="Country" PromptText="- Please Select -" ServicePath="~/Services/Service.asmx"
    ServiceMethod="GetCountries" SelectedValue='<%# Eval("CountryId") %>' LoadingText="Please wait">
</asp:CascadingDropDown>
 
<asp:CascadingDropDown ID="cddlProvinces" runat="server" TargetControlID="ddlProvince"
    ParentControlID="ddlCountry" Category="Province" PromptText="- Please Select -"
    ServicePath="~/Services/Service.asmx" ServiceMethod="GetProvinces" SelectedValue='<%# Eval("ProvinceId") %>' LoadingText="Please wait">
</asp:CascadingDropDown>
 
<asp:CascadingDropDown ID="cddlCities" runat="server" TargetControlID="ddlCity" ParentControlID="ddlProvince"
    Category="City" PromptText="- Please Select -" ServicePath="~/Services/Service.asmx"
    ServiceMethod="GetCities" SelectedValue='<%# Eval("CityId") %>' LoadingText="Please wait">
</asp:CascadingDropDown>
 


One important thing to note is that the physical placement of the CascadingDropDown extenders in your markup is important, if you swapped your CascadingDropDown nodes around (cddlCountries with cddlCities) your SelectedValue won't be set, this is thanks to the way the JavaScript gets generated for this extension.

So I would suggest that you place your nodes in order of the cascade relation - starting with the top most parent.

Before I realised this small little detail, I made use of contextkeys (enables us to send through additional values) like seen in the following markup:

 
<asp:CascadingDropDown ID="cddlCities" runat="server" TargetControlID="ddlCity" ParentControlID="ddlProvince"
    Category="City" PromptText="- Please Select -" ServicePath="~/Services/Service2.asmx"
    ServiceMethod="GetCities" UseContextKey="true" ContextKey='<%# Eval("CityId") %>'">
 


Which calls a webmethod that looks something like this.

 
[WebMethod]
public CascadingDropDownNameValue[] GetCities(
  string knownCategoryValues,
  string category, string contextKey)
{
    StringDictionary values = CascadingDropDown.ParseKnownCategoryValuesString(knownCategoryValues);
    Int32 ProvinceId = Convert.ToInt32(values["Province"]);
    return City.Get(ProvinceId).Select(p =>
        new CascadingDropDownNameValue(p.Title, p.CityId.ToString(), 
            p.CityId == Convert.ToInt32(contextKey))
    ).ToArray();
}
 


Which brings me to the small concern I mentioned in the previous part of this post, well... its actually more of a general concern when working with Ajax, observe the following image.

Cascade Error

There are a number of things that could have gone wrong in the preceding image causing the error 500, some we can/must catch & handle server side before they even reach the browser, but what about those we can't, or deliberate (for some dark reason) ones?

Unfortunately the creator of this extender didn't provide default functionality to handle these errors, would have been nice if they followed the whole success/failed callback "methodology" that we see when using PageMethods, instead of simply populating the DropDownList with an error 500 message.

I did however manage to create a small little workaround by means of the WebRequestManager, which I hope someone that wants more control over their requests might find useful, observe the following snippet:

 
function pageLoad() {
    Sys.Net.WebRequestManager.add_completedRequest(On_completedRequest);
}
 
function On_completedRequest(sender, eventArgs) {
    var url = sender.get_webRequest().get_url();
    if ((sender.get_statusCode() == 500) && (url == 'Services/Service.asmx/GetCities')) {
        alert(sender.get_statusText());
    }
}
 


Basically I provided a callback (attach event) which will be called when our ajax requests completes and within our handler/callback function we can determine if a request failed (error 500 in this case) and which url caused the failure (url gives us a clue of where we are in the process) then handle it appropriately from there.

Now there is another "non-error" scenario that we can also manage like this, imagine the service used for populating the cascade gets delayed for some reason (e.g. network traffic, database performance), setting the LoadingText for the cascade won't be enough to prevent the user from posting the form.

In order to prevent the user from submitting dodgy data, the basic idea is to disable the submit button (by default), with "please wait" text as button text and as soon as the appropriate request completes we enable the button again (inside our On_completedRequest callback).

All in all I don't think this is a bad extender, we've successfully used it in a few of our projects despite a few small little glitches, but nothing that can't be sorted out.




Post/View comments
 

AjaxControlToolkit (ASP.NET/C#) : CascadingDropDown Extender - Part 1



When working with relational data on a frontend one of the common practices (among TreeViews) are to make use of DropDowns that update in relation to one another - cascading.

In the following image we've got three DropDownLists that demonstrates this cascading effect.

Cascading DropDowns

When an user selects a country, it updates the list of provinces (or states) available for that country, which in turn updates the list of available cities for the selected province.

One way to achieve this (many ways to skin a cat) is to attach handlers to the SelectedIndexChanged events of our DropDownLists and manually handling the cascade relation like seen in the following snippet.

 
protected void ddlCountry_SelectedIndexChanged(object sender, EventArgs e)
{
    ddlCountry.Cascade(ddlProvince, (CountryId) => Province.Get(CountryId));
    ddlProvince.Cascade(ddlCity, (ProvinceId) => City.Get(ProvinceId));
}
 
protected void ddlProvince_SelectedIndexChanged(object sender, EventArgs e)
{
    ddlProvince.Cascade(ddlCity, (ProvinceId) => City.Get(ProvinceId));
}
 


Basically when an user selects a country, we clear all provinces and cities and rebind the province DropDownList with provinces relating to the selected country. If the user selects a province we clear the cities DropDownList and rebinds it with cities relating to the selected province.

Now the Cascade method seen in the snippet above is an extension method to which we pass our DataSource via a lambda expression, the method can be seen in the snippet below. (also note that all source code for this post can be downloaded from here)

 
public static class ExtensionMethods
{
    public static void Cascade<T>(this DropDownList parentDropDownList, DropDownList childDropDownList, Func<Int32, T> dataSource)
    {
        childDropDownList.Items.Clear();
        childDropDownList.AppendDataBoundItems = true;
        childDropDownList.Items.Add(new ListItem("- Please Select -", ""));
        if (!String.IsNullOrEmpty(parentDropDownList.SelectedValue))
        {
            childDropDownList.DataSource = dataSource(Convert.ToInt32(parentDropDownList.SelectedValue));
            childDropDownList.DataBind();
        }
    }
}
 


If we need to get rid of the postbacks caused by the cascade we can put our DropDownLists in an UpdatePanel like seen in the download accompanying this post.

Another option (which brings is to the title of this post) is to make use of the CascadingDropDown extender which is part of the AjaxControlToolkit

Observe the following markup:

 
<asp:DropDownList runat="server" ID="ddlCountry">
</asp:DropDownList>
<asp:DropDownList runat="server" ID="ddlProvince">
</asp:DropDownList>
<asp:DropDownList runat="server" ID="ddlCity">
</asp:DropDownList>
 
<asp:CascadingDropDown ID="cddlCountries" runat="server" TargetControlID="ddlCountry"
    Category="Country" PromptText="- Please Select -" ServicePath="~/Services/Service.asmx"
    ServiceMethod="GetCountries">
</asp:CascadingDropDown>
 
<asp:CascadingDropDown ID="cddlProvinces" runat="server" TargetControlID="ddlProvince"
    ParentControlID="ddlCountry" Category="Province" PromptText="- Please Select -"
    ServicePath="~/Services/Service.asmx" ServiceMethod="GetProvinces">
</asp:CascadingDropDown>
 
<asp:CascadingDropDown ID="cddlCities" runat="server" TargetControlID="ddlCity" ParentControlID="ddlProvince"
    Category="City" PromptText="- Please Select -" ServicePath="~/Services/Service.asmx"
    ServiceMethod="GetCities">
</asp:CascadingDropDown>
 


Okay, so what we've got here is our three DropDownLists (country, province, city) and three CascadingDropDown extenders, here is quick list of the attributes you're seeing (there are others) on the extenders.

TargetControlID ID of the DropDownList being extended / populated.
ParentControlID ID of the DropDownList thats above this control in the hierarchy, the extender needs the SelectedValue of the parent DropDownList in order to be able to databind the target control.
Category Category represented by the control, this property is used as key(s) to distinguish the values of each DropDownList SelectedValue in the knownCategoryValues parameter passed to the webmethod - parseable by the CascadingDropDown.ParseKnownCategoryValuesString method.
PromptText The text of the default item in the DropDownList (almost feels like something that should have been part of the DropDownlist WebControl, hint hint). PromptValue sets the value of the default item.
ServicePath Path to the webservice, uhm... yes you're going to need to write a webservice which will be used by the extender to databind DropDownLists client side.
ServiceMethod Method in the webservice thats going to be called in order to populate the DropDownList.


In the following code example you can get an idea of what the required web service will look like.

 
[WebService(Namespace = "http://cstruter.com/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
[System.Web.Script.Services.ScriptService]
public class Service : System.Web.Services.WebService
{
    [WebMethod]
    public CascadingDropDownNameValue[] GetCountries(
      string knownCategoryValues,
      string category)
    {
        return Country.Get().Select(p => 
            new CascadingDropDownNameValue(p.Title, p.CountryId.ToString())
        ).ToArray();
    }
 
    [WebMethod]
    public CascadingDropDownNameValue[] GetProvinces(
      string knownCategoryValues,
      string category)
    {
        StringDictionary values = CascadingDropDown.ParseKnownCategoryValuesString(knownCategoryValues);            
        Int32 CountryId = Convert.ToInt32(values["Country"]);
        return Province.Get(CountryId).Select(p =>
            new CascadingDropDownNameValue(p.Title, p.ProvinceId.ToString())
        ).ToArray();
    }
 
    [WebMethod]
    public CascadingDropDownNameValue[] GetCities(
      string knownCategoryValues,
      string category)
    {
        StringDictionary values = CascadingDropDown.ParseKnownCategoryValuesString(knownCategoryValues);
        Int32 ProvinceId = Convert.ToInt32(values["Province"]);
 
        return City.Get(ProvinceId).Select(p =>
            new CascadingDropDownNameValue(p.Title, p.CityId.ToString())
        ).ToArray();
    }
}
 


Like seen in the example above, each method needs to return an array of CascadingDropDownNameValue - which is essentially our ListItems.

You might have noticed the methods Country.Get/Province.Get and City.Get, these are all part of the dummy data class provided within the accompanying download to this post.

In the next part of the post we're going to have a look at how to databind the CascadingDropDown extender (I've already included the source code for the databinding in the download) and I am going to share a small concern I've got regarding this extender and some possible solutions.




Post/View comments
 
First 1 2 3 4 5 6 7 8 9 10 Last / 65 Pages (130 Entries)

Latest Posts

MS SQL: Parameter Sniffing


2012-05-21 22:38:48

Be the best stalker you can be


2011-12-13 22:33:54

Top 5 posts

Moving items between listboxes in ASP.net/PHP example


Move items between two listboxes in ASP.net(C#, VB.NET) and PHP
2008-06-12 17:07:43

Simple WYSIWYG Editor


Creating a WYSIWYG textbox for your website is actually quite simple.
2007-02-01 12:00:00

C# YouTube : Google API


Post on how to integrate with YouTube using the Google Data API
2011-03-12 08:37:51

Populate a TreeView Control C#


Populate a TreeView control in a windows application.
2009-08-27 16:01:03

Cross Browser Issues: Firefox Word Wrapping


Firefox word wrapping issues
2008-06-09 09:51:21