Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples

Building a Complex Custom Control: Rolodex

by Jesse Liberty, author of Programming Visual Basic 2005

This article marks the end of "Liberty on Whidbey" and the beginning of a new series: "Liberty On Beta 2." Each article will demonstrate a real-world problem I've had to solve for a client, and will leave you with a complete design and working code. The goal of the column is to put Beta 2 to work and to explore applying the constructs and concepts of C#, ASP.NET, and Windows programming to solving real-world problems.

Analysis and Requirements

My customer asked for a complex Windows application that included, as one of its features, the ability to scroll through a list of his customers, suppliers, and employees using the visual metaphor of a Rolodex, much as he might look at contacts in Outlook. The Rolodex is shown in Figure 1.

Double-clicking on an entry in the program will bring you to a detail page for that entry (not implemented in this article, and left as an exercise for the reader). Note that for this article, I'll be using the NorthWindDatabase, available with SQL Server and Access, and soon with SQLServerExpress.


There is no Rolodex control in .NET, and so we'll build our own composite control, consisting of custom panels, custom RolodexEntries, and various standard controls such as panels, buttons, and scroll bars. We want to build the custom controls to provide maximum flexibility, so that we can reuse a well-factored design to display not only customers, but also suppliers, employees, and data from other tables as the requirements change.

figure 1
Figure 1. Rolodex

There are two ways to approach a custom control of the complexity of the Rolodex. One is to build it incrementally, and the other is to design it up front. To be honest, I would normally build this project incrementally ("get it working and keep it working"), factoring out common code as I go. To make this article clear, however, I've designed and built the entire project (so that you don't have to suffer the dead ends and frustrations along the way), and I'll describe the architecture as if I were able to design it on paper without having to revise it once I started writing code (which is great in theory, but not so hot in practice).

We will create three custom controls, and a design pattern to create more as we need them. The controls are RolodexPanel, RolodexEntry, and RolodexCustomerEntry. In addition, we will create a form to hold a RolodexPanel, frmRolodex, and we will create a derived form: frmCustomerRolodex. We will factor as much common code into the base classes as possible so that we can later add new RolodexEntry types (e.g., RolodexSupplierEntry) and new forms (e.g., frmSupplierRolodex).

Areas of Responsibility

Following the principle of encapsulation, each class will have one job to do, one area of responsibility. The job of the Rolodex Panel is to lay out RolodexEntry objects (without regard to whether they are Customer or Supplier or other RolodexEntry subclasses). The job of the derived Rolodex entries is to know what information they display and to notify the form if they are clicked.

Notice that there are derived forms and derived entry objects, but only one form for all. (One form to rule them all, one form to find them, one form to bring them all and in the darkness bind them.)

The job of a frmRolodex is to hold a RolodexPanel, and the job of the derived forms is to know how to retrieve the data to create the Rolodex Entries that are held in the Panel.

Thus, the architecture looks like this:

figure 2
Figure 2. Rolodex architecture

The Rolodex Form sub-type (e.g., FormCustomerRolodex) has exactly one RolodexPanel, which in turn has one or more (usually 12) RolodexEntry objects. The particular RolodexEntry object used will determine which information is displayed. At the moment, nothing enforces that a frmCustomerRolodex will display RolodexCustomerEntry objects, though that is the intention.


The complete code for this entire project is available for download here. To save space, this article will not reproduce the full source code, but will show the important bits as we go.

To get started, create a new Windows project named "Rolodex." Immediately add a new project of the type Windows Control Library, and call the new project "RolodexControls." Make sure both projects are in the same solution. Rename the UserControl1 that Visual Studio creates to RolodexPanel.cs. (Unfortunately, we can't do much with this custom control until we have Rolodex entries, so bear with me while we create some more custom controls.)

Note: The design and code for the Rolodex shown in this article are based on work done by Liberty Associates, Inc., on behalf of and owned by Catalyst, Inc., and are used with their generous permission.

Creating the RolodexEntry Base Class

Right-click on the RolodexControls project and choose Add -> User Control. Name the new control "RolodexEntry.cs;" the base class for RolodexEntries. Set its size to 225,75. The goal now is to factor out those features that are common to all Rolodex entries. Enter the following code:

protected const int MaxStringLength = 24;
protected const int TruncatedStringLength = 22;
protected bool selected;
public delegate void EntrySelected(object sender, EventArgs e);
public event EntrySelected OnEntrySelected;

public RolodexEntry()

protected virtual void SetSelectedProperties() { }

public bool Selected
  get { return selected; } 
  set { selected = value; } 

// will be called by the rolodex entries
protected virtual void Clicked( object sender, EventArgs e )
   if ( OnEntrySelected != null )
    OnEntrySelected( sender, e );

The base class has two constants and a Boolean (selected) for which there is a public accessor. Other than the constructor, the only methods are SetSelectedProperties (which does nothing but which will be overridden in the derived classes) and Clicked, which informs any interested observer (e.g., the form) when an entry has been selected (e.g., to show details about that entry).

Creating the Customer Rolodex Entry

Create a new user control, RolodexCustomerEntry. Set its size to 225,75. Add seven labels; the top label (lblCompanyName), stretches across the entire width of the control, and has its background color set to silver and its font set to size 12. The remaining six labels have their background color set to Control, and their font size set to 8.25. They are: lblContactPrompt, lblContactName, lblPhonePrompt, lblPhone, lblFaxPrompt, and lblFax . The CustomerRolodexEntry control is shown in Figure 3:

figure 3
Figure 3. CustomerRolodexControl

This virtual method sets the company name's BackColor to red when the entry is selected. Next, click on the form and each label in turn and set all of the Click Events to ClickHandler. Implement ClickHandler as follows:

private void ClickHandler( object sender, EventArgs e )
  Clicked( this, e );

ClickHandler invokes the base class' Clicked method, passing the current CustomerRolodexControl as the source. At this point change the derivation of CustomerRolodexControl from UserControl to Roldoex Entry (you couldn't do so before because Visual Studio would not let you look at the designer until your derived class was completed):

public partial class RolodexCustomerEntry : RolodexEntry
Override SetSelectedProperties to set the CompanyName label's background to red if the entry is selected, silver otherwise:
lblCompanyName.BackColor = Selected ? Color.Red : Color.Silver;

Finally, add a public method to load the values passed in by the CustomerRolodexPanel:

public void LoadValues(
  string companyName,
  string contactName,
  string phone,
  string fax )
  lblCompanyName.Text = companyName;
  lblContactName.Text = contactName;
  lblPhone.Text = phone;
  lblFax.Text = fax;

Creating the Rolodex Panel

Return to your RolodexPanel control and set its size to 875,510. Add an ASP:Panel control named "pnlEntries" with a size of 872,440, and a border of Fixed3D, to the upper left-hand corner of the RolodexPanel. Add another ASP:Panel, pnlNavigation," at location 14,451 and size it to 848,40. Add 26 buttons, each sized 32,23 and each with a white background and a capital letter (A-Z), as shown in Figure 4.

This panel will be responsible for adding the (specific) Rolodex entries, raising an event when a letter button is clicked or the scroll bar is moved, and raising an event when an entry is double-clicked. There are two slightly tricky methods in this class. The first is LetterButton_Click, which is the event handler for all of the buttons on the navigation panel. You convert the sender to type button and extract the letter of the button from its Text property. You then set the Panel's chosenLtr variable to that letter and you raise the ButtonSelectedEvent. We'll see what that event does in the section on the form, below.

figure 4
Figure 4. Rolodex panel

The second interesting method is RolodexPanel_Load, in which you add the vertical scroll bar (and set its event handler) and resize pnlNavigate to accommodate the scroll bar. There is also some fine tuning of the size and positioning of the panels based on the size of the Rolodex entries. To get this to fire, click on the RolodexPanel and set its Load event handler to this method. Also be sure to set the event handlers for all 26 buttons to LetterButton_Click.

Implementing the Forms

The custom controls are now ready; you only need to create the base form and the two derived forms. The derived forms are responsible only for knowing how to access the data; all of the display work (and responding to the two events of the panel) is handled by the base form. Create a form named FrmRolodex and set its size to 887, 533 (these numbers are somewhat arbitrary; they represent the sizes I happened to use when I stretched my form and panels to an agreeable size).

The constructor registers the panel's RowFillEventEventHandler and ButtonSelectedEventEventHandler. Intellisense will help you with this. Also create four protected methods, the first three of which are virtual and will be overriden in the derived class. The last is not virtual but will be called from the derived class:

protected virtual void AddEntry( DataRow dataRow, int column, int row ) { }
protected virtual void LoadRolodex() { }
protected virtual void LoadRolodex(char letter) { }
protected void DoLoad( int count, char letter )
  this.rolodexPanel.Vbar.Maximum = count;
  this.rolodexPanel.Vbar.Value = 0;

You'll overload the FillRows method. One version will take a letter (called by the event handler for a letter-button click), and the other will take no parameters. The one that takes a letter will set the offset for the vertical scroll bar to the position it should be in for the chosen letter. This will fire the ValueChanged event on the vertical scroll bar, which is handled in the panel class which in turn fires its own RowFillEvent, which is handled back in the form by calling FillRows with no parameter.

The job of the overloaded FillRows method that takes no parameters is to fill 12 Rolodex entries into the panel. This is done by the form because it is the form that knows how to get the data from the database. Of course, that information is specialized in the derived from. Create a second form called frmCustomerRolodex. Create a data source for Northwind adding the customers and suppliers tables (at least) and drag the customers table onto your new form (you may have to expand the form temporarily to make room below the navigation panel). Four objects are created. Delete the BindingNavigator and the BindingSource--you won't need them--and readjust the height of the form.

Nasty Tricks Department

Open Program.cs and change the run statement to create a new instance of frmCustomerRolodex rather than of frm1. Also, and this is particularly sneaky, open the file frmRolodex.Designer.cs and change the access level of rolodexPanel from private to protected, so that the derived forms can access it directly.

The frmCustomer.cs code file overrides LoadRolodex to get the appropriate data from customersTableAdapter:

  (NorthwindDataSet.CustomersDataTable ) 
  northwindDataSet.Tables["Customers"] );
NorthwindDataSet.CustomersDataTable dataTable =
frmCustomer.cs also overrides AddEntry to make instances of the RolodexCustomerEntry with the data retrieved.
protected override void AddEntry( DataRow dataRow, int column, int row )
  RolodexControls.RolodexCustomerEntry entry =
    new RolodexControls.RolodexCustomerEntry();
  string companyName = string.Empty;
  string contactName = string.Empty;
  string phone = string.Empty;
  string fax = string.Empty;
  if ( dataRow["CompanyName"] != null ) 
    companyName = dataRow["CompanyName"].ToString();
  if ( dataRow["contactName"] != null ) 
     contactName = dataRow["contactName"].ToString();
  if ( dataRow["phone"] != null ) 
     phone = dataRow["phone"].ToString();
  if ( dataRow["fax"] != null ) 
     fax = dataRow["fax"].ToString();
  entry.LoadValues( companyName, contactName, phone, fax );
  entry.Left = this.rolodexPanel.StartX + column * rolodexPanel.XIncrement;
  entry.Top = this.rolodexPanel.StartY + row * rolodexPanel.YIncrement;
  entry.OnEntrySelected += 
     new RolodexControls.RolodexEntry.EntrySelected(rolodexPanel.entryClick);
  rolodexPanel.Add( entry );

Be sure to add a reference to the RolodexControls in your Rolodex application.

You'll want to build (or download) the entire source and step through it in your debugger to see precisely how the architecture shown above is implemented.

Implementing the supplier RoldoexEntry and form are left as exercises for the reader.

Jesse Liberty is a senior program manager for Microsoft Silverlight where he is responsible for the creation of tutorials, videos and other content to facilitate the learning and use of Silverlight. Jesse is well known in the industry in part because of his many bestselling books, including O'Reilly Media's Programming .NET 3.5, Programming C# 3.0, Learning ASP.NET with AJAX and the soon to be published Programming Silverlight.

Return to OnDotNet.com

Copyright © 2009 O'Reilly Media, Inc.