12,065,888 members (29,492 online)
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:
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!
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.
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.
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).
An enumeration is an ordered set of values, one for each category in the category set.
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).
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.
For any given category, those preceding it in category order are its leading categories.
For any given category, those succeeding it in category order are its trailing categories.
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.
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:
/* connect to a data store */ System.Data.OleDb.OleDbConnection con = new System.Data.OleDb.OleDbConnection(connectionString); con.Open(); /* set up a DrillDownDataSet */ DrillDownDataSet d = new DrillDownDataSet(); System.Data.OleDb.OleDbDataAdapter adap; /* fill the DrillDownDataSet */ 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:
|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:
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:
Based on the data in the examples above, the control might look as follows:
Behind the scenes, the control makes use of the
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.
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
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.
Discovered the strength of the
DataRelation object in typed
DataSets. This object makes it very easy to work with relational data in memory.
IncludeBlankEnumerationproperty to the control to force default blank combos. Also modified the code to allow spaces in the category names.