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.
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.
Posted by - Christoff Truter
Date - 2011-10-07 19:48:59
Comments - 1
Date - 2011-10-07 19:48:59
Comments - 1
Syntactic sugar (C#): Enum
Last week I had the opportunity to work with a rather expensive OCR SDK, one that could have been a great deal more elegant if the creators of the SDK used this sexy piece of syntactic chocolate, syntactic steak, syntac... uh I think you get the picture, observe the following extract.
public class OCRContext : IDisposable { public OCRContext(int iLanguage) {
There are two things I dislike about the constructor in the preceding snippet:
-
Naming convention - iLanguage? Are you serious? This is what is called hungarian notation, the
i describes the type of the variable, in this case its trying to tell us that language is an integer.
Personally I feel that this convention doesn't have any place in a strongly typed language like C#.
-
Use of a magic number - we need to pass an integer to the constructor,
which tells the class which language to use, but what number represents English for example? Ideally we don't want someone to
dig through a thousand page manual for something so trivial.
This "magic number" convention can lead to all kinds of confusion, observe the if condition below:
if ((iLanguage == 17) || (iLanguage == 2)) { // Do some funky stuff }
Not all that clear from this snippet that 17 represents polish, while 2 is supposedly french...
But wait, it gets a little bit more interesting, the developer included the following crude class:
public class Languages { public const int Afrikaans = 33; public const int English = 0;
This is somewhat helpful, but it would have been nicer if we had a programmatic relation enforcing this class (telling us where we can find our languages), providing some kind of constraint at the same time.
Like you can see from preceding image, we still need to pass an int value, nothing is preventing or telling us to do something else.
This is where our syntactic sugar in question comes into play, we can group our constants into a neat strongly typed little box of values, which constrains us to what we can send to the parameter and leads the developer to the list of possible constants.
public class OCRContext : IDisposable { public OCRContext(Languages language) { // something } // etc.. } // Poorly designed enum don't do this, read on.. public enum Languages { English, Afrikaans }
Obviously (like with everything else), we can misuse this piece of sugar ending up with a system suffering from hyperglycemia.
Seeing as the underlying type of our enum is an int (can also use other integral types) it can lead to some interesting issues when using an enum as bit flags via bitwise operators.
Have a look at the following snippet (using the Languages enum in this post):
Languages languages = Languages.Afrikaans | Languages.English; if (languages == Languages.Afrikaans) Console.WriteLine("Afrikaans"); if (languages == Languages.English) Console.WriteLine("English"); if (languages == (Languages.Afrikaans | Languages.English)) Console.WriteLine("AfrikaansEnglish");
(Note: When an enumeration is to be used in bitwise operations, be sure to set the flags attribute)
Which produces the following:
What happend here exactly? Why did the first condition validate as true?
Well, its rather simple, if we don't provide enums with values, each member will simply be incremented starting by 0, so the value behind Languages.English is 0 - which technically means we didn't really add English to the equation.
To solve this issue you can add values to your members like seen below.
public enum Languages { English = 1, Afrikaans = 2 }
Or according to best practices rather provide a sensible default value.
public enum Languages { None, English, Afrikaans }
There are a few other valuable design guidelines provided by microsoft (not going to go into too much detail in this post) with regards to enumeration, which you can read more about over here.
There is one last tip I want to give you before I send you on your way.
Lets assume that you did infact design your class like the one seen at the beginning of this post and you want to use an enum instead, but unfortunately you've got a billion people using your class already, you can simply use the obsolete attribute like seen in the following snippet.
class OCRContext { [Obsolete("Rather use OCRContext(Languages language)")] public OCRContext(int iLanguage) { // some code } public OCRContext(Languages language) { // some code } }
If the developer tries to use the old constructor, he will get the following warning in his IDE (assuming he's using Visual Studio):
Additional reading
Enumeration Design
Enumeration Types (C# Programming Guide)
When to Use an Enumeration
Posted by - Christoff Truter
Date - 2011-08-04 16:50:18
Comments - 2
Date - 2011-08-04 16:50:18
Comments - 2
First 1 2 3 4 5 6 7 8 9 10 Last / 65 Pages (129 Entries)