Cutting Edge

Custom Provider Controls

Dino Esposito

Code download available at:CuttingEdge0311.exe(139 KB)

Contents

Derive a New Class
Providers
The ToolTip Provider Control
HelpProvider and ErrorProvider
The IExtenderProvider Interface
Writing a Custom Extender Control
Extending the ToolTip Extender
Summing It Up

Sometimes I find the Windows® Forms controls in Visual Studio® .NET to be lacking a particular feature that's indispensable to the application I'm building. While I'm not saying that the set of Windows Forms controls is inadequate, a general-purpose toolbox can't possibly meet all user requirements. Let's consider the TextBox control, for instance. It's relatively easy to agree on a minimum set of properties and methods that this control has to support. However, there is a large set of extended properties that would be very useful. For example, you might want the ability to change the background color when the control gets the focus, an input mask, or perhaps an associated autocomplete list. Of course, I'd rather have a lightweight control that can be extended than a bloated component that has all the features I'd ever need.

In Windows Forms, inheritance is just one option for building extended versions of existing controls. Another option involves provider controls, which is the subject of this month's column. Provider controls add new properties to existing controls without the need for inheritance. The idea is that you use an external extender control to provide new properties and behaviors to existing controls. The implementation of such extensions doesn't add weight to the base control; it's a lightweight alternative to inheritance. Provider controls don't replace inheritance; rather, they extend and complement it in many ways.

Derive a New Class

Suppose you want a textbox that changes its background color when it has focus. One option you have is to derive a new class. Figure 1 shows a simple class that inherits from TextBox and wires up the GotFocus and LostFocus events. The AutoColorTextBox class changes the background color when the control gets the focus and restores the original color when the LostFocus event is fired. You can easily assign different colors to each textbox. All you need to do is place instances of the AutoColorTextBox class on the form and set the background color when the control gets focus:

private void Form1_Load(object sender, EventArgs e) { autoColorTextBox1.SelectedBackColor = Color.Cyan; autoColorTextBox2.SelectedBackColor = Color.Yellow; autoColorTextBox3.SelectedBackColor = Color.LightGreen; }

Figure 1 The Original Control

using System; using System.Drawing; using System.Windows.Forms; namespace MsdnMag.ExtenderLib { public class AutoColorTextBox : TextBox { public AutoColorTextBox() { SelectedBackColor = this.BackColor; m_oldBackColor = this.BackColor; this.GotFocus += new EventHandler(AutoColorTextBox_GotFocus); this.LostFocus += new EventHandler(AutoColorTextBox_LostFocus); } public Color SelectedBackColor; private Color m_oldBackColor; private void AutoColorTextBox_GotFocus(object sender, EventArgs e) { if (this.BackColor != SelectedBackColor) { m_oldBackColor = this.BackColor; this.BackColor = SelectedBackColor; } } private void AutoColorTextBox_LostFocus(object sender, EventArgs e) { if (m_oldBackColor != this.BackColor) { this.BackColor = m_oldBackColor; m_oldBackColor = this.BackColor; } } } }

Inheritance is an excellent choice if you need to customize the behavior and the appearance of controls. But what if you want to add another feature to your TextBox class? Suppose you want the textbox to accept only numbers and display all negative numbers in red. Figure 2 shows this new version of the control. The class AutoNumericTextBox inherits from the previously defined AutoColorTextBox. As long as you need both features in all textboxes, inheritance is fine.

Figure 2 The Modified Control

Figure 2** The Modified Control **

However, what if you need to use extensions as individual attributes? As these controls are currently designed, all the features are loaded even if you need only the numeric mode (see Figure 3). An alternative scheme, in which extensions are managed individually, is at odds with the core idea of inheritance. With inheritance an object has all the features of the class. So if you want to be able to combine the basic features of the TextBox class with any possible extensions like auto-color and numeric mode, you need to build one of the following classes: TextBox+AutoColor, TextBox+Numeric, or TextBox+AutoColor+Numeric. Scale this pattern to meet the complexity (and the quantity) of real-world classes and you will understand why inheritance is not always the best option for extending controls .

Figure 3 Loading All Features

using System; using System.Drawing; using System.Windows.Forms; namespace MsdnMag.ExtenderLib { public class AutoNumericTextBox : AutoColorTextBox { public AutoNumericTextBox() { Value = 0.0f; NumericMode = false; m_oldForeColor = this.ForeColor; this.TextAlign = HorizontalAlignment.Right; this.LostFocus += new EventHandler(AutoNumericTextBox_LostFocus); } public double Value; private Color m_oldForeColor; public bool NumericMode; private void AutoNumericTextBox_LostFocus(object sender, EventArgs e) { if (NumericMode) { Value = Convert.ToDouble(this.Text); if (Value <0) { this.ForeColor = Color.Red; this.Font = new Font(Font.Name, Font.Size, FontStyle.Bold); } else { this.ForeColor = m_oldForeColor; this.Font = new Font(Font.Name, Font.Size); } } } } }

Providers

A provider control lets you extend existing controls without creating new specific classes. The code that performs the extension (the GotFocus and LostFocus handlers) is loaded into an external class that plugs into the base class at run time. The excellent design-time support in Visual Studio .NET makes the presence of provider controls go almost unnoticed.

To get the numeric code and auto-color features, you need to write two provider controls; one implementing auto-color and one implementing the numeric mode. On the final form, place only the providers that you really need. This way, no unnecessary code is loaded. If you need to combine two or more extensions you can do it by binding more providers to the same control.

Provider controls prove to be extremely useful in another scenario as well. Consider again the extension that changes the background color of a TextBox control. What if you want to add that feature to RichTextBox and ListBox controls, too? Without providers, your only option would be to create two new classes.

The beauty of provider controls is that they allow you to extend multiple types of base controls. The ultimate role of providers is to add custom properties to existing controls. Extended properties show up in the Visual Studio .NET property grid. At run time the Windows Forms infrastructure yields control to the provider for the actual implementation of the extended properties. In doing so, the extender control gets a reference to the extendee and can check the type and the name as well as read and update the state.

The ToolTip Provider Control

Before I discuss how to build custom provider controls, let's review the predefined extenders that come with the Windows Forms toolbox: the ToolTip, HelpProvider, and ErrorProvider controls. All of them are displayed in the component tray area of the Visual Studio .NET IDE, as shown in Figure 4. Each provider control can have its own set of properties that affect the way in which the control works. The control-specific properties have nothing to do with the extensions that are added to bound controls.

Figure 4 Provider Control Icons

Figure 4** Provider Control Icons **

The ToolTip control lists properties that let you configure the timing of the popup text, and the ErrorProvider control lets you change the icon used to represent the error message and make the text blink on demand. Finally, the HelpProvider control needs a compiled help file to work.

Figure 5 ToolTip Property

Figure 5** ToolTip Property **

Now that you've added a few extender controls to the form, select any other control in the form and scroll to its set of properties. You may be surprised to find that all controls in the form now feature an extra ToolTip property (see Figure 5). The exact name of the property in the grid is "ToolTip on toolTip1," where toolTip1 is the name of the extender control. Set this property for a textbox and build the project. The control on the form should now display a tooltip. Select a button and repeat the operation. The button also has a "ToolTip on toolTip1" property which, when set, causes a tooltip to appear when the mouse pointer hovers over the button. This means that the ToolTip extender control can extend virtually any control you may have on a form with the sole exception of another ToolTip control. Each control that can appear on a form is automatically given a ToolTip property which, if set, causes a popup description to appear at run time.

In this way, all controls that belong to the form share the same ToolTip object, which maintains an internal table of control/caption pairs. Like many other Windows Forms controls, the ToolTip control is a wrapper built around a Win32® window-based control. A Win32 tooltip window lives behind the managed ToolTip object. This window is created when the managed ToolTip object is instantiated and configured for style and delay. Next, a couple of internal tables are added—one to store control/caption associations and one to store control/region pairs.

If you're familiar with Win32 ToolTip controls you know that a single instance can be used to display popup descriptions at different screen locations. In other words, the way in which the ToolTip control works in Windows is significantly different from HTML tooltips, which are attributes bound to an individual element. The Visual Basic® 6.0 model is like the Web scenario. Visual Basic 6.0, in fact, gives each control a ToolTipText property. Don't judge this behavior by its appearance, however. The Visual Basic 6.0 mechanism is not really different from the Windows Forms model. In the Visual Basic 6.0 IDE, the ToolTipText property is an extended property that the ActiveX® container adds to any hosted control. Under the hood of this seemingly simple programming model, only one Win32 tooltip window is created and used to display text for each constituent element that features a non-empty caption. How can you get a tooltip window to display text whenever the mouse pointer stops over a given control? It's simple—use regions.

A tooltip region is a rectangular area that the tooltip is called to monitor. Whenever the mouse enters the area and stops there for the specified period of time, the ToolTip moves its window there, sets the caption, and pops it up. In Windows Forms, each control that has a non-empty caption also is given an entry in the region table. The region contains the bounds of the control area.

HelpProvider and ErrorProvider

The HelpProvider control gets into the game when the user presses the F1 key while the focus is on a form control. HelpProvider adds three properties to each control: HelpNavigator, HelpKeyword, and HelpString. HelpNavigator determines what happens when the user requests some help. The property accepts values from the HelpNavigator enumeration and provides access to specific elements of the help file. HelpKeyword indicates the keyword or the topic to be searched, while the HelpString is displayed through a tooltip, as shown in Figure 6. The HelpString property is independent from any help file and displays the tooltip when F1 is pressed.

Figure 6 Some Help Text

Figure 6** Some Help Text **

The ErrorProvider displays an optionally blinking icon beside any control with an error message (see Figure 7). The properties added to each control respect the graphical styles of the icon—IconAlignment and IconPadding. By default, the icon is placed to the right of the control and centered vertically. No pixels are left as padding between the border of the control and the icon unless you set IconPadding to a nonzero value.

Figure 7 Error Icon

Figure 7** Error Icon **

The ErrorProvider control adds a third entry to each control's property grid—Error. The Error property contains the error message that the control will display. Unlike tooltips, which are seldom determined at run time, error messages are a dynamic property. This reveals an important difference between extender controls and inherited classes. The properties added to each control—say, the Error message—are not really new properties that you can set programmatically. You can set extended properties at design time because Visual Studio .NET performs this trick, not because real properties are dynamically added to controls. The following code doesn't compile and the reason is obvious—the Error property is not part of the programming interface of a TextBox, but you can set it at design time:

txtAge.Error = "Must be > 18";

So how can you set an error message or a tooltip caption dynamically? You do it using a method on the extender control. The previous line of code must be rewritten as follows:

errorProvider1.SetError(txtAge, 18);

You set the ToolTip text property at run time using nearly identical syntax:

toolTip1.SetToolTip(txtAge, "Enter a major age");

Is there a pattern behind this? An extender control must supply a pair of GetXxx/SetXxx methods for each Xxx property it adds to extendees. Using these methods, a control can access both properties at run time. The prototype of such methods is shown here:

public string GetError(Control control); public void SetError(Control control, string value);

In these signatures the string type changes according to the actual type of the extended property.

Overall, extender controls are important for a couple of reasons. They provide an alternative model to extend existing controls which is orthogonal to class inheritance. Second, using extender controls wisely can significantly limit the amount of code you need to write for each form. What is the common foundation upon which extender controls must base their functionality? It's the IExtenderProvider interface.

The IExtenderProvider Interface

IExtenderProvider defines only one method—CanExtend. The interface is defined as follows:

public interface IExtenderProvider { bool CanExtend(object extendee); }

Visual Studio .NET calls CanExtend to determine which objects in a container should receive the extender properties. Any component that provides extender properties must implement IExtenderProvider:

public class SimpleTextBoxExtender : Component, IExtenderProvider { ••• }

Aside from the fact that it implements the IExtenderProvider interface, an extender is primarily a control. For this reason, in addition to implementing the interface you must make it inherit the base functionality of a control. However, if you inherit it from Control, then the control can't be dropped in the component tray area—only on the form. To make it show in the component tray, use Component as the base class. In both cases, though, the overall behavior of the extender doesn't change much.

An extender provider class must be marked with a [ProvideProperty] attribute. The constructor of the ProvideProperty attribute class takes two arguments. The first parameter is the name of the property to add and the second is the type of the object to which you provide the property.

The following code snippet defines an extender that adds the SelectedBackColor property to all textboxes on the form:

[ProvideProperty("SelectedBackColor", typeof(TextBox))] class SimpleTextBoxExtender: Component, IExtenderProvider { ••• }

Note that you can also declare the property as an extended property for virtually any component—using IComponent or Control instead of TextBox in the attribute. In this case, the implementation typically will include features that make it usable only with a specific category of controls.

You implement the CanExtend method so that it returns true for each control to which the extender wants to add properties. Here's a possible implementation for a simple TextBox extender:

public bool CanExtend(object extendee) { return (extendee is TextBox); }

A similar implementation also catches any control that inherits from the TextBox class, including the custom classes I built earlier. Figure 8 shows the source code of a very simple extender control. It implements a background color change when the control is focused. In particular, the extender defines the pair of GetSelectedBackColor and SetSelectedBackColor methods. They can be called by any textbox to programmatically get or set the background color. In addition, the control features a public property, SelectedBackColor, which appears in the property grid and provides the default color. Notice that the property won't show up in the property grid if you don't explicitly implement it using the get/set accessors:

private Color m_SelectedBackColor; public Color SelectedBackColor { get {return m_SelectedBackColor;} set {m_SelectedBackColor = value;} }

Figure 8 Extender Control

namespace Samples { [ProvideProperty("SelectedBackColor", typeof(TextBox))] public class SimpleTextBoxExtender : Component, IExtenderProvider { public SimpleTextBoxExtender() { InitializeComponent(); // Use a hashtable to track selected colors for // each extended control } public bool CanExtend(object target) { return (target is TextBox); } private Color m_SelectedBackColor; public Color SelectedBackColor { get {return m_SelectedBackColor;} set {m_SelectedBackColor = value;} } private Color backupBackColor; public Color GetSelectedBackColor(Control control) { return SelectedBackColor; } public void SetSelectedBackColor(Control control, Color selColor) { TextBox t = (TextBox) control; SelectedBackColor = selColor; t.GotFocus += new EventHandler(TextBox_GotFocus); t.LostFocus += new EventHandler(TextBox_LostFocus); } private void InitializeComponent() { SelectedBackColor = Color.Cyan; } private void TextBox_GotFocus(object sender, EventArgs e) { TextBox t = (TextBox) sender; backupBackColor = t.BackColor; t.BackColor = SelectedBackColor; } private void TextBox_LostFocus(object sender, EventArgs e) { TextBox t = (TextBox) sender; t.BackColor = backupBackColor; } } }

If you use this control in a sample application, it works just fine. However, when coded this way the control is too simple and not particularly functional. The main drawback is that the background color is unique for all textboxes. Furthermore, hooking events is a delicate operation that needs more attention. Try using any of the derived textboxes with the extender and you'll see that confusion results. Serious extenders need to maintain a table of bound controls and store settings individually.

Writing a Custom Extender Control

Let's enhance the simple TextBox extender to provide better support to each extendee. The extender will now add two properties—the background and foreground colors (see Figure 9).

Figure 9 Adding Background and Foreground Color

[ProvideProperty("SelectedBackColor", typeof(TextBox))] [ProvideProperty("SelectedForeColor", typeof(TextBox))] public class TextBoxExtender : Component, IExtenderProvider { private Hashtable Extendees; public TextBoxExtender() { InitializeComponent(); // Use a hashtable to track selected colors // for each extendee this.Extendees = new Hashtable(); } ••• }

Each extendee control is assigned an information class that's structured as follows:

public class TextBoxInfo { public Color SelectedBackColor; public Color OldBackColor; public Color SelectedForeColor; public Color OldForeColor; public bool EventsWired; }

This class tracks the colors to use, but also the colors to restore and wires the GotFocus and LostFocus events only once, which is indicated by the EventsWired property. The code for the Get/Set pairs gets slightly more complicated. Let's tackle the Get accessor first:

public Color GetSelectedBackColor(Control control) { // Retrieve related info TextBox t = (TextBox) control; TextBoxInfo info = (TextBoxInfo) Extendees[t]; return info.SelectedBackColor; }

The control parameter references the actual extendee control—a TextBox in this case. Once you get a reference to the underlying control, you could also filter out controls based on the name or any other property you can access. You use the control reference as the key to access the hashtable and retrieve the corresponding TextBoxInfo structure. After that, returning the background color to use is child's play.

Who's creating the entry in the hashtable and when? The Set accessor of one of the properties is responsible for inserting the control in the table. The control won't be added if a similar entry already exists in the hash table. Figure 10 shows the source code of the SetSelectedBackColor method.

Figure 10 SetSelectedBackColor Method

public void SetSelectedBackColor(Control control, Color selColor) { TextBoxInfo info; TextBox t = (TextBox) control; if (!Extendees.ContainsKey(t)) info = new TextBoxInfo(); else info = (TextBoxInfo) Extendees[t]; // Store the new value info.SelectedBackColor = selColor; // If not already done, wire events up if (!info.EventWired) { t.GotFocus += new EventHandler(TextBox_GotFocus); t.LostFocus += new EventHandler(TextBox_LostFocus); info.EventWired = true; } // Add to the table if (!Extendees.ContainsKey(t)) Extendees[t] = info; }

Extending the ToolTip Extender

The ToolTip class is sealed and can't be inherited to add more features such as the balloon style. In Windows XP and newer operating systems, the ToolTip window has been given the TTS_BALLOON style to display the caption in a balloon-style popup window. At first, deriving a new extender class from the existing ToolTip provider and adding support for the balloon style seems like a relatively easy task. The first problem you run into is that the ToolTip class is sealed; that is, it's not further inheritable. However, by using aggregation instead of inheritance you can devise a custom wrapper extender that exploits the underlying capabilities of the ToolTip class.

The idea is to create a new instance of the ToolTip class in the extender's constructor and call the GetToolTip and SetToolTip methods in the get and set accessors of the new ToolTip property.

[ProvideProperty("MyText", typeof(TextBox))] public class MyToolTip : Component, IExtenderProvider { private ToolTip _toolTip; public MyToolTip() { toolTip = new ToolTip(); } ••• }

The MyToolTip class embeds a dynamically created instance of the ToolTip class as a private property and exposes a MyText extended property. The implementation of this property passes through the implementation of the text property on the original ToolTip class:

public string GetMyText(Control control) { return _toolTip.GetToolTip(control); } public void SetMyText(Control control, string caption) { // TODO :: Enter your changes... _toolTip.SetToolTip(control, caption); }

By implementing this code, you end up with your own custom extender, making it work on top of the existing ToolTip component. A good reason for extending the ToolTip is to use a customized ToolTip control. Balloon and multiline styles are not supported by Windows Forms ToolTips and the only way to add these features is through a custom ToolTip extender. Unfortunately, of the three built-in extenders the one you need—the ToolTip component—is sealed and can't be inherited. That's why you must resort to the trick based on control aggregation.

However, the main obstacle on the road to full customization of the Windows Forms ToolTips lives elsewhere. The ToolTip control inherits from Component, not from Control. There's controversy over which way is better. If you inherit a class from Component, the class behaves like a control tray component, which would be great for extenders. If you derive the class from Control, the resulting component can't be placed in the IDE tray area. However, it will inherit the Handle property from the base class.

The Handle property represents the HWND handle of the underlying Win32 control. Since Windows Forms are based extensively on Win32 controls, they end up creating a window whenever a Control object is instantiated. To customize the appearance and behavior of a control, you need to resort to Win32 styles and messages. And how can you set them if you can't access the underlying window handle?

The managed ToolTip control does have a Handle property defined as an IntPtr member. Recall that IntPtr is the .NET Framework type that maps a Win32 handle such as HWND, HGLOBAL, or HKEY. The difficulty lies in the fact that the ToolTip's Handle property is declared as private and is thus inaccessible due to its protection level. The ToolTip class could be further extended using aggregation, but the extensions you can really add in practice are limited to anything you can do without knowing the HWND of the underlying ToolTip window.

Summing It Up

Extender controls represent an alternate route to control inheritance. By writing extenders you can achieve two goals. First, you can implement a finer level of granularity and decide what features to add to a class without creating a new class and inheriting features at run time. Second, you can add the same features to many control types using a single extension class. You could add properties to textboxes and comboboxes using a unique extender; if you used inheritance, that would require two distinct classes. In addition, while inheritance represents a pure coding approach, extenders try to shift some of the required burden at design time. Extenders reduce the amount of code you need to write and promote a model of programming that is more declarative and attribute based.

Remember, of course, extenders do not replace inheritance, but rather complement it and contribute to making your programming toolset richer than ever.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Programming Microsoft ASP.NET (Microsoft Press, 2003), he spends most of his time teaching classes on ADO.NET and ASP.NET and speaking at conferences. Get in touch with Dino at cutting@microsoft.com.