Introduction
How many times in a UI is there a requirement for a control containing several combo boxes, where the list of potential selections in any given combo is dependent on previous selections in other combos? As an example, consider a website that deals in second-hand cars:
The user enters a Manufacturer, a Configuration (sedan, station wagon, hatch), followed perhaps by Gear-box type (manual, automatic) and Year of model. Finally, they get to the category in which they actually want to make a selection - the Model. This technique is so ubiquitous because it is so powerful - imagine a user having to select a model of car from all models ever made by any manufacturer!
The goal of this control is to not only provide a managed way of cascading user selections, but also to provide a schema for storing the configuration data behind the control. The control should be:
- fully configurable in terms of categories, values, and dependencies;
- easily configured at run-time by passing in the data from a store (i.e., the categories, values, and dependencies should live in an independent data store such as a database or an XML file, so that categories can be easily added or removed, or their order changed); and
- independent of the type and location of the data store (data abstraction).
Background
Before diving in, here's a bit of a glossary for the rest of the article. If you have any suggestions for changing these terms to make them clearer for future readers, I'm all ears!
Category
A category represents a scale for values. The scale has no direction (or order). On the control, each category is represented by a combo box.
Value
A value is one of a set for each category. A value has no intrinsic meaning apart from the fact that it is distinct from other values in the set - i.e., it has no type such as "real number" that implies things such as order and arithmetic operations. Any meaning attached to a value is dependent on its interpretation, which is aided by the description of the category to which it belongs (so values could be interpreted as numbers, if this is a property of the category to which they belong). Values are what the user selects in each combo box.
Set of categories
The categories themselves form a set - the set of categories! The programmer should be able to manage this set without coding, including addition and removal of members, as well as the order of members (the categories fall on an ordinal scale determining the order in which they are presented to the user).
Enumeration
An enumeration is an ordered set of values, one for each category in the category set.
Partial enumeration
A partial enumeration is an ordered set of values, one per category, from the first category to an arbitrary category that is not the last (e.g., values for Manufacturer, Configuration and Gear-box, but not for Year and Model).
Total enumeration
The total enumeration of a tree is a list of all possible paths from the root to the leaves. In our case, the total enumeration represents every valid ordered combination of values.
Leading category(ies)
For any given category, those preceding it in category order are its leading categories.
Trailing category(ies)
For any given category, those succeeding it in category order are its trailing categories.
Limitations
Before going on, I should mention one major limitation of this control. In some cases, there is a requirement that the next trailing category be determined by a user's selection. For instance, if the user selects "bird" from an "animals" category, then the next trailing category might be "beak type". However, the "beak type" category would be inappropriate if the user had selected, for example, "fish". This type of tree is not supported by the drill-down control. For one thing, it would mean that the control could not provide an overview of what input is required from the user, since combo boxes would have to appear one at a time based on user selections (this frustrates me on some websites, especially when the server is slow!). Perhaps a tree-like control (ala Explorer, Regedit) would be more appropriate where this is a requirement.
Using the code
To use the code, just drop a DrillDown
control on a form. Use the Initialise
method to set up the control. This method will label the combos and populate them with a default enumeration:
System.Data.OleDb.OleDbConnection con =
new System.Data.OleDb.OleDbConnection(connectionString);
con.Open();
DrillDownDataSet d = new DrillDownDataSet();
System.Data.OleDb.OleDbDataAdapter adap;
adap = new System.Data.OleDb.OleDbDataAdapter(
"SELECT * FROM Categories", con);
adap.Fill(d.DdCategoriesTable);
adap = new System.Data.OleDb.OleDbDataAdapter(
"SELECT * FROM Outputs", con);
adap.Fill(d.DdOutputsTable);
adap = new System.Data.OleDb.OleDbDataAdapter(
"SELECT * FROM Enumerations", con);
adap.Fill(d.DdEnumerationsTable);
MyDrillDown.Initialise(d);
Initialise
takes a DrillDownDataSet
. The schema of this strongly typed DataSet
is shown below:
The table DdCategories describes the set of categories, including their order. For example:
DdCategoryIdentifier | DdCategoryName | DdCategoryOrdinal |
1 | Colour of Item: | 0 |
2 | Type of Item: | 1 |
3 | Name of Item: | 2 |
The table DdEnumerations is a normalized table of enumerations constituting the total enumeration. Every enumeration is expected to be linked to the full complement of categories. An example is shown below:
DdEnumerationIdentifier | DdCategoryIdentifier | DdValue |
1 | 1 | Red |
1 | 2 | Vegetable |
1 | 3 | Rhubarb |
2 | 1 | Red |
2 | 2 | Vegetable |
2 | 3 | Tomato |
3 | 1 | Red |
... | ... | ... |
The table DdOutputs maps each enumeration to an output value, an integer. It is up to the programmer to decide whether the mapping is one-to-one or one-to-many. The output value is a representation of what the user has ultimately selected using the drill-down. An example is shown below:
DdEnumerationIdentifier | DdOutput |
1 | 0 |
2 | 1 |
3 | 2 |
4 | 3 |
5 | 4 |
6 | 5 |
7 | 6 |
8 | 7 |
... | ... |
Based on the data in the examples above, the control might look as follows:
Behind the scenes
Behind the scenes, the control makes use of the DataTable
and DataView
objects to manage cascading. When the Initialise
method is called, the control sets up a denormalised (or "flat") table, with one category per column and one enumeration per row. A user selection simply triggers a cascading filter on the trailing categories.
Features
One of the features of this control is that, in order to save the state of the drill down, all the programmer needs to do is store the enumeration identifier or the output value associated with the user's selection. This is done via the EnumerationId
and Output
properties. Setting either property restores the control to the appropriate state. In the case of a non-unique output, the control is set back to the first valid enumeration matching the said output. The enumeration identifier is always unique.
A second feature of the control is that it tries to remember user selections in the trailing categories. If a selection is still valid when a leading category changes values, then that selection will remain. That way, the user needs to re-enter as little data as possible in case of a correction up the hierarchy.
Points of interest
Discovered the strength of the DataRelation
object in typed DataSet
s. This object makes it very easy to work with relational data in memory.
History
- 21st December, 2005
- Added
IncludeBlankEnumeration
property to the control to force default blank combos. Also modified the code to allow spaces in the category names.
- 22nd August, 2004
- Added properties to control the appearance of run-time generated category combos.
- 10th August, 2004