Inherited Controls
Inherited controls are an ideal way to take functionality from the .NET base classes, and extend it. An inherited control can be dramatically different than its predecessor, or it may just add a few refinements. The .NET class library is filled with examples of inherited controls. For example, LinkLabel derives from Label and CheckedListBox derives from ListBox.
Unlike user controls, there is no design-time support for creating an inherited control. You simply create a class that derives from your selected control type and add the features you need. You'll also find that inherited controls are awkward to use in Visual Studio .NET. For example, it's difficult to add inherited controls to a form except through code. You overcome these difficulties in the next chapter by creating custom designers.
Inherited controls are generally more powerful than user controls, and more likely to be used across applications (and even organizations, if you are a tool vendor), not just between different windows in the same program. Some of the reasons that programmers develop inherited controls are to set defaults (for example, a control that automatically configures its appearance in its constructor) or to add features.
So far in this book, you've seen the following examples of inherited controls:
In
In Chapter 4, you saw an inherited menu control that handles its own drawing to allow custom fonts and embedded thumbnail images.
In Chapter 5, you saw inherited Form controls with visual inheritance.
In Chapter 6, you saw custom ListView and TreeView examples that support specific types of data.
In this chapter, I'll present two more advanced inherited control examples.
Inherited Controls or User Controls?
So, how do you know when to create a user control, and when you need a fullfledged inherited control? It's not always an easy question to answer, because most problems can be solved with either approach. However, here are a few pointers that you should consider before embarking on a custom control project:
User controls are easier and faster to program. If you don't anticipate reusing the control frequently in different scenarios and different programs, a user control may suffice.
If your control closely resembles an existing .NET control, it's probably best to create an inherited control. With a user control, you may need to spend a fair amount of effort creating new properties and methods to allow access to the members of the original control.
Inherited controls provide a fine-grained level of reuse. User controls typically provide only a few members, and thus are not as configurable. Tool vendors who wish to sell their controls will always use inherited controls.
User controls are well suited if you want to ensure that a block of interface is recreated exactly in more than one situation. Because a user control usually provides less flexible configuration, it guarantees a more standardized appearance.
If you want to integrate more than one control, you have two choices: you can use composition with a user control, or you can develop two separate inherited controls. The latter approach gives you the freedom to link controls (like a TreeView and ListView), but make the links optional. The application programmer can then use them separately or together, and has complete freedom about how to integrate them into a user interface. With user controls, however, the application programmer can only control the size taken by the full user control.
The DirectoryTree Control
The DirectoryTree control inherits from the standard TreeView and adds the features needed to display a hierarchical view of directories. .NET does not include any type of native directory control, so this TreeView is genuinely useful.
Perhaps most important, it fills itself by reading subdirectories "just in time." This means that the control operates very quickly, even if the drive has tens of thousands of subdirectories. Only the expanded directory levels are actually shown. The collapsed branches all have a dummy node inserted. Every time a directory branch is expanded, the inherited control checks if a dummy node is present, and, if it is, the dummy node is removed and the directories are read from the disk. (You see a variation of this technique to allow efficient data access in Chapter 9).
The full code listing follows. Notice that the currently selected drive is stored as a single character string (technically, a Char). Another approach would be to use an instance of the System.IO.DirectoryInfo class to track or set the currently highlighted directory. That approach would provide better control for the application programmer, but it would complicate design-time support.
using System;
using System.IO;
using System.Windows.Forms;
public class DirectoryTree : TreeView
{
public delegate void DirectorySelectedDelegate(object sender,
DirectorySelectedEventArgs e);
public event DirectorySelectedDelegate DirectorySelected;
private Char drive;
public Char Drive
{
get
{
return drive;
}
set
{
drive = value;
RefreshDisplay();
}
}
// This is public so a Refresh can be triggered manually.
public void RefreshDisplay()
{
// Erase the existing tree.
this.Nodes.Clear();
// Set the first node.
TreeNode rootNode = new TreeNode(drive + ":\\");
this.Nodes.Add(rootNode);
// Fill the first level and expand it.
Fill(rootNode);
this.Nodes[0].Expand();
}
private void Fill(TreeNode dirNode)
{
DirectoryInfo dir = new DirectoryInfo(dirNode.FullPath);
// An exception could be thrown in this code if you don't
// have sufficient security permissions for a file or directory.
// You can catch and then ignore this exception.
foreach (DirectoryInfo dirItem in dir.GetDirectories())
{
// Add node for the directory.
TreeNode newNode = new TreeNode(dirItem.Name);
dirNode.Nodes.Add(newNode);
newNode.Nodes.Add("*");
}
}
protected override void OnBeforeExpand(TreeViewCancelEventArgs e)
{
base.OnBeforeExpand(e);
// If a dummy node is found, remove it and read the real directory list.
if (e.Node.Nodes[0].Text == "*")
{
e.Node.Nodes.Clear();
Fill(e.Node);
}
}
protected override void OnAfterSelect(TreeViewEventArgs e)
{
base.OnAfterSelect(e);
// Raise the DirectorySelected event.
if (DirectorySelected != null)
{
DirectorySelected(this,
new DirectorySelectedEventArgs(e.Node.FullPath));
}
}
}
The base class events are handled by overriding the corresponding method (the recommended approach). The OnAfterSelect event is turned into a more useful DirectorySelected event, which provides a custom DirectorySelectedEventArgs class.
public class DirectorySelectedEventArgs : EventArgs
{
public string DirectoryName;
public DirectorySelectedEventArgs(string directoryName)
{
this.DirectoryName = directoryName;
}
}
Testing the DirectoryTree
To test the DirectoryTree, you can add it to the Toolbox, or you can add a project reference and programmatically add it to a form, which is the approach our simple test form will take. Make sure that you set the initial drive when using the control, or the display will be blank.
The following code snippet creates, configures, and displays the DirectoryTree control on a form. Figure 7-15 shows the results.

Figure 7-15: The DirectoryTree in action
private void Form1_Load(object sender, System.EventArgs e)
{
DirectoryTreeControl.DirectoryTree dirTree = new
DirectoryTreeControl.DirectoryTree();
dirTree.Size = new Size(this.Width - 30, this.Height - 60);
dirTree.Location = new Point(5, 5);
dirTree.Drive = Char.Parse("C");
this.Controls.Add(dirTree);
}
Another option is to follow the steps outlined at the beginning of this chapter, which allow you to add the DirectoryTree to the Toolbox and configure it at design-time.
The DirectoryTree could have been created as a user control, but the inheritance approach provides far more flexibility. For example, all the original TreeView events, properties, and methods are still available to the client code. Images can be assigned, the Nodes collection can be traversed, and restricted directories could have their nodes removed. Best of all, you don't need to write any code to delegate the properties of your custom control class to an underlying control. Clearly, inherited controls provide a far greater level of flexibility.
A Masked TextBox Control
The final inherited control example is one for a custom masked text box. A masked text box is one that automatically formats the user's input into the correct format. For example, it may add dashes or brackets to make sure it looks like a phone number. This task is notoriously difficult. One useful tool is Microsoft's masked edit text box, which is provided as an ActiveX control with previous versions of Visual Studio.
The example of a masked text box is important because it demonstrates how features (rather than data) might be added to an existing control by subclassing. The example I provide is still quite limited-notably, it restricts deletions and the use of the arrow keys. Tracking the cursor position, which is required to allow inline masked edits, results in a good deal of tedious code that only obscures the point.
Here's the full class code for the masked text box:
using System;
using System.Windows.Forms;
public class MaskedTextBox : TextBox
{
private string mask;
public string Mask
{
get
{
return mask;
}
set
{
mask = value;
this.Text = ";
}
}
protected override void OnKeyPress(KeyPressEventArgs e)
{
if (Mask != ")
{
// Suppress the typed character.
e.Handled = true;
string newText = this.Text;
// Loop through the mask, adding fixed characters as needed.
// If the next allowed character matches what the user has
// typed in (a number or letter), that is added to the end.
bool finished = false;
for (int i = this.SelectionStart; i < mask.Length; i++)
{
switch (mask[i].ToString())
{
case "#" :
// Allow the keypress as long as it is a number.
if (Char.IsDigit(e.KeyChar))
{
newText += e.KeyChar.ToString();
finished = true;
break;
}
else
{
// Invalid entry; exit and don't change the text.
return;
}
case "." :
// Allow the keypress as long as it is a letter.
if (Char.IsLetter(e.KeyChar))
{
newText += e.KeyChar.ToString();
finished = true;
break;
}
else
{
// Invalid entry; exit and don't change the text.
return;
}
default :
// Insert the mask character.
newText += mask[i];
break;
}
if (finished)
{ break; }
}
// Update the text.
this.Text = newText;
this.SelectionStart = this.Text.Length;
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
// Stop special characters.
e.Handled = true;
}
}
To use the masked control, the application programmer chooses a mask and applies it to the Mask property of the control. The number sign (#) represents any number, and the period (.) represents any letter. All other characters in the mask are treated as fixed characters, and are inserted automatically when needed. For example, in the phone number mask (###) ###-#### the first bracket is inserted automatically when the user types the first number. Figure 7-16 shows this mask in action.

Figure 7-16: The MaskedTextBox in action
private void Form1_Load(object sender, System.EventArgs e)
{
MaskedTextBoxControl.MaskedTextBox txtMask = new
MaskedTextBoxControl.MaskedTextBox();
txtMask.Location = new Point(10, 10);
txtMask.Mask = "(###) ###-####";
this.Controls.Add(txtMask);
}