Augment Default Controls With Inheritance

Visual Studio .NET ships with a nice set of controls and classes to build Windows applications, but with only a little effort, you can augment these controls so they serve you better.

Technology Toolbox: C#

Inheritance lets you define objects for easy reuse. The advantage of this is that you can alter controls—both your own and those created by others—so they behave exactly as you need them to. This is especially useful when you have code that is almost, but not quite, what is required. Inheritance lets you reuse this code, adding modifications "by exception."

For example, assume you have a business object class. This class might contain fundamental functionality to load, modify, and verify data. You can break, or subclass, this class into several different classes, such as a name business object, an invoice business object, and so forth. These subclassed objects are only concerned with functionality specific to their problem domains, and they don't need to re-create functionality that can access databases, and so on. This means you can keep the code to perform common tasks such as data access in a single location. Once tested, you can use this code across all the classes that inherit from the base business object. Similarly, you could fix any bug in this code in one place, which increases maintainability of the code base greatly.

An additional benefit: You can subclass the "child classes"—the individual business object classes that inherit from the base business object—to provide more specific functionality. A customer business object might be an object derived from the name business object and require only a few minor details be changed.

The business object scenario is a relatively common one, but you can use this same technique for visual control classes as well. In fact, Microsoft makes heavy use of this technique in the Visual Studio .NET controls, which themselves derive from Windows controls. A simple button, for instance, is subclassed from a class called ButtonBase. Several other classes, such as checkboxes, also inherit from this base class. ButtonBase in turn inherits from a class called Control, which inherits from another class called Component, which inherits from a class called MarshalByRefObject, which inherits from the mother of all classes: Object. The same is true for other WinForms controls as well.

It isn't difficult to create these inheritance chains, but you do have to put some thought into creating a flexible and well-designed inheritance structure. People don't fear the ability to fix things in a global manner, but they do fear breaking them globally. What if Microsoft puts a bug into the Control class in a future version of the .NET Framework? The answer is simple: It breaks all WinForms controls. Fortunately, these types of bugs are generally so obvious that they're often easier to detect and fix than other bugs.

Create an Inherited Control
Let's begin by inheriting from the Button class. Inheriting from a sophisticated control like this one lets you make your WinForms controls look and behave exactly the way you want with only a minimum of effort. Perhaps you want all your buttons to appear in a flat style with a "Tahoma" font and a height of 21 pixels. You could implement such a control by changing all these properties every time you drop a button on a form, but that would be a waste of time. Instead, subclass the button and use your subclass instead:

public class MyButton : 
	System.Windows.Forms.Button 
{
	public MyButton()
	{
		this.Font = new 
		System.Drawing.Font("Tahoma", 8);
		this.Height = 21;
		this.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
	}
}

This is nothing more than a standard button control with a slightly customized appearance. This control looks like any other WinForms button, exposes the same events and properties, and provides the same developer experience as a regular button. The only difference is that you don't need to set these properties manually when you drop this control on a form.

The nice thing about this construct is that it provides a place where you can make additional changes later in the process. Assume you have a customer who really likes an application you wrote, but now wants all the buttons to have a blue background, similar to what you see in Microsoft Office 2003. The customer also wants you to change the font so it's just a tad larger. You can now take this same button class and make the desired changes quickly and easily:

public class MyButton : 
	System.Windows.Forms.Button 
{
	public MyButton()
{
		this.Font = new System.Drawing.Font("Tahoma", 10);
		this.Height = 21;
		this.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
		this.BackColor = System.Drawing.Color.LightBlue; 
	}
}

All your buttons are now blue and have a 10-point font size.

This scenario raises an interesting point: You might be happy with a button's default appearance now, but you might want to change the look and feel later. Subclassing the control now gives you a single entry point to make changes later.

Similarly, you might want to take advantage of new features in Windows. For example, Tablet PCs are becoming more popular, but few developers write applications for them specifically. Creating an entry point into your control could enable you to take advantage of inking capabilities without a major rewrite.

You now have a new, customized Button class. Let's add it to the toolbar. You need to compile it into a .NET assembly before you can add it to the toolbox, but it doesn't matter whether you compile it into an EXE or DLL. However, you must include a reference to the WinForms namespace, or it won't compile.

Right-click on your toolbox, and select Add Tab to create a new category of controls once you compile the assembly. Note that you could add your control to an existing default category, but that might make things confusing down the road. Next, right-click in the new tab and choose Customize Toolbox. This brings up a dialog that lets you pick controls installed in the Global Assembly Cache (GAC) or as COM components. The new assembly is neither, so click on the Browse button to navigate to the newly compiled file. Choosing a file makes all the classes in it appear in the toolbox automatically. Click on OK to complete the process. You can now see your button control in the toolbox (see Figure 1). You can drag and drop it onto forms like any other control.

Choose a Toolbox Icon
Note that the button shows up with a default icon that is not particularly useful. However, you can specify a ToolboxBitmap attribute easily. You can use either an existing, similar bitmap, or a new icon that you create specifically for this control. For the sake of simplicity, let's use the same bitmap as other Button controls:

[System.Drawing.ToolboxBitmap(
	typeof(System.Windows.Forms.Button))]
public class MyButton : 
	System.Windows.Forms.Button 
{?}

You could easily create your own bitmap and specify its name in the attribute. You could also use a naming convention that points the IDE to a bitmap file, but I recommend using the explicit definition through the ToolboxBitmap attribute because it's much less fragile.

I encourage you to experiment a bit with this new button class. Drop instances of the button on a form, and change things around. You can change the button's caption or attach event handler code. The button really isn't much different from any other button, and you can change the properties you set in it initially. You can adjust the default settings as your needs require.

Try creating a few forms, drop some buttons onto them, and run the sample. Then go back to your Button class, change a few properties, and re-run the sample app without changing anything else. All the buttons on all the forms will change to the new defaults specified by the class. The only exceptions to this are properties you set specifically on some form.

The button control example is useful, but also relatively simple and unsophisticated. You might want to do more than change a control's visual appearance. For example, I hate the fact that the DataGrid doesn't feature a real double-click event that fires whenever the user double-clicks a row in the grid, so I wrote some code that does this (see Listing 1). This class fires a new event called GridDoubleClick every time the user double-clicks a row in the grid. The problem with the native double-click is that the first click is handled by the grid itself, but the second grid is trapped by the control used for each column (such as a textbox), resulting in two single clicks on different objects. GridDoubleClick gets around this problem by memorizing the time of the first click in the grid.

The new class hooks events fired by the column controls automatically by binding to them whenever they get added. This happens in the OnControlAdded() method. Whenever someone clicks on the control, the event handler traps the click and compares the current time with the last time the grid was clicked on. The class fires the GridDoubleClick event if the interval is no longer than the system-specified double-click interval.

No solution fits everyone's needs out of the box, but you can use inheritance and subclassing to augment functionality in the ways your applications require. I recommend that you ignore the standard tab full of default controls in WinForms projects, and instead create your own subclasses and use them exclusively. Prepare one set of subclasses that you use as the starting point for every project, then another for each project.

Featured

comments powered by Disqus

Subscribe on YouTube