Ajax.NET 1.0 came out of beta and luckily a project came up, so I had a good opportunity to try it out in real-world. This was my first experiment with Ajax.NET (formerly Atlas). Specifically, my point of interest was to Toolkit's CascadingDropDown to implement the required functionality. Samples looked very attractive and it seemed that it ideally fits my requirements until I actually started implementing the project...
To make things clearer for those reading this post, here is a brief description of what I was trying to achieve:
- Create a web-form with a bunch of dropdowns allowing a visitor to specify computer (PC) hardware configuration (i.e. chassis, motherboard, processor, cooler, RAM, monitor, etc.), get the price, and place an order for it.
- Configuration's price should only be displayed when all components required for a minimal configuration are selected (this was the client's requirement to prevent users from looking up individual prices for video-adapters, coolers and 'other things hidden in the box' easily. Naturally, they can calculate it anyways by picking different configurations, but this is neglectable).
- UI should indicate by dropdown color which components are required for a minimal configuration.
- Large components selected by the user should additionally be visualized, i.e. UI should display images for selected chassis, monitor, printer, scanner, speakers, keyboard, mouse.
Once again, the AJAX.NET Toolkit's CascadingDropDown extender seemed to be a perfect fit for the main part of the functionality. Adding an UpdatePanel with image placeholders (for component's image display) and a couple of labels to indicate which required components are not selected yet or to show the price of selected configuration, a ModalPopup for fancy order-placement (specifying client's name, email, and address of delivery) seemed to be sufficient to implement the web-form.
Implementation
The CascadingDropDown sample shows how to implement a simple hierarchy dependency. However, with computer hardware configuration it is a bit different (and a bit more complex):
- motherboards have some dependency on chassis
- processors depend on selected motherboard
- coolers depend on processors (cooler is required only for tray processors and cannot be installed on box processors)
- RAM depends on motherboard
- HDD depends on motherboard
- other components have no dependencies (e.g. DVD/CD, monitor, printer, scanner, keyboard, mouse, modem, speakers, UPS, software and the like)
Considering that the data for this project has to be updated frequently, and moreover, client's requirement was to feed the data from MS Excel, I decided to use a database for storage (MS Access mdb-file in particular to reduce hosting fees). Unfortunately, ASP.NET hosting is quite expensive when compared to *NIX hosting, and even more expensive when there is a need for MS SQL. Certainly, I could have used XML as well, but I thought it would make things a bit slow in terms of performance.
Passing additional information with Cascading drop down
I will not dwell upon the object model and class implementation, but will briefly describe what I came up with: each component with complex dependencies has additional compatibility keys (e.g. motherboard instance has the following fields: Name, Price, ChassisCompatibilityKey, ProcessorCompabilityKey, RAMCompatibilityKey, VideoCompatibilityKey, HDDCompatibilityKey).
In order to reduce the number of database requests I thought it would be a good idea to pass some additional information, besides the currently selected dropdown value, to the webserivce handling Ajax calls (as the original sample and probably the extender's implementation itself suggests). One ASP.NET forums member also asked for a solution on how to pass additional info in CascadingDropDowns, and basically, this is the only reason why I wrote this section. My solution is pretty straight-forward and very simple: why not concatenate the dropdown option's value? For instance, motherboard option's value would be: 99.99;~;2;~;1;~;2;~;3;~;4
(price and compatibility keys to indicate chassis/processor/ram/etc. type). I chose ;~;
as a delimeter to ensure that I don't touch any data, and it is very unlikely that data would contain something weird like ;~;
:-)
Thus, a dropdown's OnChange client-side event fires Ajax call and the web-service parses its value string to get required compatibility keys needed to populate other dropdowns that depend on this one. When the minimal configuration is selected, its price should be displayed, which is also very simple in this scenario: get selected value of each dropdown, split it by delimiter and add up the first item to configuration's price.
Adding own events to CascadingDropDown extender
One of the additional requirements was to have some UI indication of components which are required for minimal PC-configuration, e.g. different color of dropdown's PromptText. Unfortunately, the extender does not have a property like PromptTextCssClass or similar.
The first idea that came to my mind was to 'recolor' (change CSS class) of the prompt text client-side: loop through all dropdowns and change options[0].className
to some defined CSS class. However, this approach yields no result for one simple reason: when the web-form is loaded, dropdowns do not have any options until all webservice calls are completed. Additional challenge would be to handle OnChange events for further 'recoloring'/re-styling of dependent dropdowns.
Thus the only solution is to re-style dropdown's options only AFTER the webservice returns results and the dropdown is populated. But how do we know when this happens? Regrettably, the CascadingDropDown extender does not have a OnClientPopulated event to inform subscribers that a particular dropdown finished populating its options.
Here's how I implemented the OnClientPopulated event in the CascadingDropDown extender:
1. Open the AjaxControlToolkit VS solution.
2. Modify the CascadingDropDownBehavior.js (add the following functions: add_populated, remove_populated, raisePopulated):
add_populated : function(handler) {
/// <summary>
/// Add a handler on the populated event
/// </summary>
/// <param name="handler" type="Function">
/// Handler
/// </param>
this.get_events().addHandler("populated", handler);
},
remove_populated : function(handler) {
/// <summary>
/// Remove a handler from the populated event
/// </summary>
/// <param name="handler" type="Function">
/// Handler
/// </param>
this.get_events().removeHandler("populated", handler);
},
raisePopulated : function(arg) {
/// <summary>
/// Raise the populated event
/// </summary>
/// <param name="arg" type="Sys.EventArgs">
/// Event arguments
/// </param>
var handler = this.get_events().getHandler("populated");
if (handler) handler(this.get_element(), arg Sys.EventArgs.Empty);
}
3. In CascadingDropDownBehavior.js locate _onMethodComplete
function and modify it to look as follows (add line #4):
1: _onMethodComplete : function(result, userContext, methodName) {
2: // Success, update the DropDownList
3: this._setOptions(result);
4: this.raisePopulated(null);
5: }
4. Open the CascadingDropDownExtender.cs and add the OnClientPopulatedBehavior:
[DefaultValue("")]
[Category("Behavior")]
[ExtenderControlEvent]
[ClientPropertyName("populated")]
public string OnClientPopulated
{
get
{
return (string)(ViewState["OnClientPopulated"] ?? string.Empty);
}
set { ViewState["OnClientPopulated"] = value; }
}
5. Build the solution.
6. Ensure that the project/solution you work on references this new version of the Toolkit!!!
7. Now in your aspx file you can add and set the OnClientPopulated
property in your CascadingDropDown extender (intellisense should also work for this property now).
That's it :-) Hope this blog entry helps someone.