Applies To:Input for WinForms
Author:John Juback
Published On:2/13/2006

ComponentOne Input for WinForms(a.k.a. C1Input) includes a variety of data-aware user interface controls for displaying and editing date/time, numeric, textual, and picture data. Developers can use these components directly on Windows Forms or as in-cell editors for the C1FlexGrid and C1TrueDBGrid controls. In addition to the built-in controls, C1Input provides a generic dropdown control that allows arbitrary Windows Forms to serve as the dropdown portion of a combo box.


This article demonstrates how to program the C1DropDownControl to display custom dropdowns based on two standard .NET controls. A ListView example demonstrates single selection from an iconic list; a CheckedListBox example demonstrates multiple selection from a textual list.


To get started, first add the C1DropDownControl component to the Visual Studio toolbox. For the .NET Framework 1.x, the assembly name is C1.Win.C1Input.dll. For the .NET Framework 2.0, the assembly name is C1.Win.C1Input.2.dll. If desired, you can also add the following C1Input controls to the toolbox:



  • C1DateEdit

  • C1DbNavigator

  • C1Label

  • C1NumericEdit

  • C1PictureBox

  • C1TextBox


The C1DropDownControl class is the base class of the C1DateEdit and C1NumericEdit controls. It derives from the C1TextBox control, which handles formatting for all data types, including special features for date/time formats. C1TextBox also supports edit masking, data validation, and total control over the conversion of user-specified strings into the appropriate typed data values.


The appearance of the C1DropDownControl is identical to that of a standard TextBox control with one exception. The VisibleButtons property lets you specify which built-in dropdown buttons are displayed, as shown in the following figure.


C1DropDownControl Button States

By default, both up/down (spin) and dropdown buttons are shown, but you can choose any combination of the available button states. Note that you can only customize the button images themselves if you are using the .NET 2.0 version of the control.


The following example uses the C1DropDownControl in conjunction with a standard ListView control to implement a single-valued row filter for a DataGrid. Although you can achieve similar functionality with a standard ComboBox control, using the C1DropDownControl lets you present the available choices in a more visually appealing way.


C1DropDownControl ListView Example

To implement a dropdown form for a C1DropDownControl, you derive a class from C1.Win.C1Input.DropDownForm. First, add a new Windows Form to the project, then switch to code view and change the base class so that it no longer inherits from System.Windows.Forms.Form. For example, in C#, this becomes:


public class CategoryDropDown : C1.Win.C1Input.DropDownForm

Or, if you are using Visual Basic:


Public Class CategoryDropDown
Inherits C1.Win.C1Input.DropDownForm

Next, place the desired controls on the surface of the dropdown form. In this example, the CategoryDropDown form contains a single ListView control (listView1) with the following property settings:


ActivationOneClick
DockFill
HideSelectionFalse

The dropdown form also contains an ImageList component that is attached to the ListView control by means of the LargeImageList property.


To ensure that the ListView control receives focus when the user clicks the dropdown button, select the DropDownForm subclass (CategoryDropDown) in the Properties window and set the following property:


FocusControllistView1

If you want the user to be able to resize the dropdown form, expand the Options node in the Properties window and set the following property:


FixedSizeFalse

Typically, you will use the dropdown form's Load event to initialize its constituent controls, as the following code illustrates:


// Load categories into the ListView control
private void CategoryDropDown_Load(object sender, System.EventArgs e)
{
Form1 f1 = this.Owner as Form1;
CategoriesDataSet ds = f1.categoriesDataSet;
 
for (int i = 0; i < ds.Categories.Count; i )
{
// The ImageList order matches the dataset order
listView1.Items.Add(ds.Categories[i].CategoryName, i);
}
}

In this example, the parent form (Form1) exposes a property that returns a strongly typed DataSet (categoriesDataSet) from which the ListView control is populated. For the sake of simplicity, this code also assumes that the sort order of the Categories table matches that of the images stored in the associated ImageList component.


To convey values from the control(s) on the dropdown form to the form's owner (a specific instance of a C1DropDownControl), handle the PostChanges event:


private void CategoryDropDown_PostChanges(object sender, System.EventArgs e)
{
// Set the text of the parent control according to the ListView selection
this.OwnerControl.Text = listView1.SelectedItems[0].Text;
}

In this example, there is only one control (listView1), so the Text property of the first (and only) selected item is assigned to the Text property of the OwnerControl that activated the dropdown form. The PostChanges event fires whenever one of the following occurs:



  1. The Value property of the OwnerControl is updated.

  2. The user closes the dropdown form by clicking the dropdown button of the OwnerControl, provided that DropDownForm.Options has the AlwaysPostChanges flag set.

  3. The user interacts with the controls on the dropdown form in such a way that the CloseDropDown method is called on the OwnerControl with an argument of true.


Since the ListView control was configured for single-click activation, we handle its Click event by calling a method on the OwnerControl that will force the PostChanges event to fire:


private void listView1_Click(object sender, System.EventArgs e)
{
// Close the dropdown when the user selects an item by clicking it
this.OwnerControl.CloseDropDown(true);
}

The final task for the CategoryDropDown class is to ensure that the appropriate list item is selected when the dropdown form opens. For this, we handle the VisibleChanged event as follows:


private void CategoryDropDown_VisibleChanged(object sender, System.EventArgs e)
{
// Do nothing if the dropdown is being closed
if (!this.Visible)
return;
 
listView1.SelectedItems.Clear();
 
for (int i = 0; i < listView1.Items.Count; i )
{
ListViewItem lvi = listView1.Items[i];
 
// Select the item that matches the text of the parent control
if (lvi.Text == this.OwnerControl.Text)
{
lvi.Selected = true;
listView1.EnsureVisible(i);
return;
}
}
}

Note that if the dropdown form is closing, no action is taken. Otherwise, the selected items are cleared, then scanned for a match with the Text property of the OwnerControl. If a match is found, then that item is selected and scrolled into view.


Now that the CategoryDropDown class is complete, all that remains is to place a C1DropDownControl on the main application form and associate it with that class by setting the following property:


DropDownFormClassNameC1CustomDropDown.CategoryDropDown

The value of this property denotes the fully-qualified class name of the dropdown form. Within the Properties window, you can select the available DropDownForm subclasses from a list. Note that each subclass can service multiple C1DropDownControl instances.


To remove the default up/down buttons from the control, set the following property:


VisibleButtonsDropDown

Finally, to apply dropdown control changes to the grid, handle the ValueChanged event as follows:


// Filter the data view when a new category is selected from the dropdown
private void c1DropDownControl1_ValueChanged(object sender, System.EventArgs e)
{
string filter = "";
 
if (c1DropDownControl1.Text.Length > 0)
filter = string.Format("Category = '{0}'", c1DropDownControl1.Text);
 
dataView1.RowFilter = filter;
}

The following example uses the C1DropDownControl in conjunction with a standard CheckedListBox control to implement a multi-valued row filter for a DataGrid. Although you can achieve similar functionality by placing the CheckedListBox control directly on the form, using the C1DropDownControl in this manner lets you conserve screen real estate while providing a concise representation of the selected values.


C1DropDownControl CheckedListBox Example

As in the previous example, we begin by deriving a class from C1.Win.C1Input.DropDownForm (CountryDropDown). Next, add a CheckedListBox control (checkedListBox1) to the CountryDropDown form and set the following properties:


CheckOnClickTrue
ColumnWidth90
DockFill
IntegralHeightFalse
MultiColumnTrue
ThreeDCheckBoxesTrue

The ColumnWidth and MultiColumn values are optional and were chosen to produce a two-column list.


To ensure that the CheckedListBox control receives focus when the user clicks the dropdown button, select the DropDownForm subclass (CountryDropDown) in the Properties window and set the following property:


FocusControlcheckedListBox1

Next, expand the Options node in the Properties window and set the following properties:


AlwaysPostChangesTrue
AutoResizeTrue
FixedSizeFalse

As in the ListView example, we set FixedSize to False to enable the user to resize the dropdown form. We also set AutoResize to True to force the initial width of the dropdown form to be the same as the width of the owner C1DropDownControl.


Unlike the ListView example, this dropdown form is used for multiple selection, so clicking a single item will not dismiss the form. Therefore, AlwaysPostChanges needs to be true so that the PostChanges event fires whenever the user closes the form with the dropdown button.


To populate the CheckedListBox control, we use a strongly typed DataSet (countriesDataSet) exposed by the parent form (Form1). We also add an extra check box for clearing or selecting all items at once.


// Load country names into the CheckedListBox control
private void CountryDropDown_Load(object sender, System.EventArgs e)
{
Form1 f1 = this.Owner as Form1;
CountriesDataSet ds = f1.countriesDataSet;
 
// Add an item for checking/unchecking the entire list
checkedListBox1.Items.Add("(All)");
 
for (int i = 0; i < ds.Countries.Count; i )
{
checkedListBox1.Items.Add(ds.Countries[i].Country);
}
}

To deal with the extra check box, we handle the ItemCheck event. If the extra item is clicked, we apply the new value to all items in the list using a private method (CheckAllItems). Otherwise, we scan the list for an unchecked item and clear the extra item if one is found:


private static bool recursion = false;
 
// Fired when the user checks/unchecks an item, but before the value itself changes
private void checkedListBox1_ItemCheck(object sender, System.Windows.Forms.ItemCheckEventArgs e)
{
// Recursion breaker (calling SetItemCheckState will re-fire this event)
if (recursion)
return;
 
recursion = true;
 
// If the "(All)" item is clicked, apply the new value to all items
if (e.Index == 0)
CheckAllItems(e.NewValue);
 
// If any item is unchecked, then uncheck the "all" item
else if (e.NewValue == CheckState.Unchecked)
checkedListBox1.SetItemCheckState(0, e.NewValue);
 
recursion = false;
}
 
// Apply a check box state to all items
private void CheckAllItems(CheckState value)
{
for (int i = 0; i < checkedListBox1.Items.Count; i )
{
checkedListBox1.SetItemCheckState(i, value);
}
}

Note the use of a static boolean field to prevent infinite recursion. This is necessary because calling the SetItemCheckState method within the body of the ItemCheck event handler will cause the event to be fired again.


To convey values from the control(s) on the dropdown form to the form's owner (a specific instance of a C1DropDownControl), we handle the PostChanges event:


// Set the text of the parent control when the user closes the dropdown
private void CountryDropDown_PostChanges(object sender, System.EventArgs e)
{
// If "(All)" is checked, clear the text in the parent
if (checkedListBox1.GetItemChecked(0))
{
this.OwnerControl.Text = "";
return;
}
 
StringBuilder sb = new StringBuilder();
 
// Build a semicolon delimited string of the selected items
for (int i = 0; i < checkedListBox1.CheckedItems.Count; i )
{
if (sb.Length > 0)
sb.Append(";");
 
sb.Append(checkedListBox1.CheckedItems[i].ToString());
}
 
this.OwnerControl.Text = sb.ToString();
}

Since this dropdown form is used for multiple selection, we build a semicolon delimited string of values from the CheckedItems collection.


The final task for the CountryDropDown class is to ensure that the appropriate check boxes are selected when the dropdown form opens. For this, we handle the VisibleChanged event as follows:


private void CountryDropDown_VisibleChanged(object sender, System.EventArgs e)
{
// Do nothing if the dropdown is being closed
if (!this.Visible)
return;
 
string value = this.OwnerControl.Text;
 
// If the parent control's text is "(All)", treat it as an empty string
if (value == checkedListBox1.Items[0].ToString())
value = "";
 
// Prevent the ItemCheck event from firing now
recursion = true;
 
// Clear all items before checking individual countries, otherwise check them all
CheckAllItems(value.Length > 0 ? CheckState.Unchecked : CheckState.Checked);
 
if (value.Length > 0)
{
// The parent control's text is a semicolon delimited string of country names
string[] countries = value.Split(";".ToCharArray());
 
for (int i = 0; i < countries.Length; i )
{
int n = checkedListBox1.FindStringExact(countries[i]);
 
// Check the item, if found
if (n != -1)
checkedListBox1.SetItemCheckState(n, CheckState.Checked);
}
}
 
recursion = false;
}

Note that if the dropdown form is closing, no action is taken. Otherwise, individual boxes are checked based on the Text property of the parent control (a semicolon delimited list of country names).


As in the previous example, we place a C1DropDownControl on the form and set its DropDownFormClassName property to the fully-qualified class name of the dropdown form (C1CustomDropDown.CountryDropDown, in this example). The following properties are also set:


InitialSelectionCaretAtStart
VisibleButtonsDropDown

The InitialSelection setting CaretAtStart forces long strings to be displayed starting with the first character. By default, the entire string is selected, which may cause unwanted horizontal scrolling.


Finally, to apply dropdown control changes to the grid, handle the ValueChanged event as follows:


// Filter the data view when one or more countries are selected from the dropdown
private void c1DropDownControl2_ValueChanged(object sender, System.EventArgs e)
{
StringBuilder sb = new StringBuilder();
 
if (c1DropDownControl2.Text.Length > 0)
{
// Country names are separated by semicolons
string[] countries = c1DropDownControl2.Text.Split(";".ToCharArray());
 
for (int i = 0; i < countries.Length; i )
{
if (sb.Length > 0)
sb.Append(" OR ");
 
sb.AppendFormat("Country = '{0}'", countries[i].ToString());
}
}
 
dataView2.RowFilter = sb.ToString();
}

Click the following link to download the Visual Studio 2003 project (in C#) that was used to implement the custom dropdown controls described in this article.


    C1CustomDropDown.zip