Custom Extender Providers
Extender providers were first introduced in Chapter 4 as interesting components that extend other controls. Examples are the ToolTipProvider, which displays a tooltip next to other controls; the ErrorProvider, which displays an error icon; and the HelpProvider, which invokes context-sensitive Help on a control's behalf when the F1 key is pressed. Providers tend to be specialized solutions, and you may design dozens of custom controls before even contemplating a custom provider.
Nonetheless, custom providers can achieve some remarkable tricks. In this section, I demonstrate two extender providers, one that mimics the old-fashioned MFC behavior of menu Help text, and another that displays a clickable Help icon. Both of these classes are found in the ExtenderProviderControls project provided with the online samples. The test applications can be found in the ExtenderProviderHost project.
Tip
To create an extender provider, it's easiest to create the custom provider class in a class library project, compile it into a DLL file, and then reference the DLL file from another project by choosing Customize Toolbox. (In fact, this approach is generally the easiest way to integrate inherited controls.) When you add the reference to the extender provider assembly, any associated extender control automatically appears in the ToolBox.
The Menu Text Provider
The MenuTextProvider extends ordinary menus by associating each item with a unique Help string. When the user hovers over a menu item, the MenuTextProvider displays the appropriate Help string. This is a common user interface convention I've mentioned before, and while it's not very useful for the average user, it does provide a good introduction to extender providers.
Choosing a base class
The first step when creating an extender provider is to create a class that implements the IExtenderProvider interface and uses the ProvideProperty attribute (both of these types are found in the System.ComponentModel interface). This can be any type of class, including a user control, inherited control, or just a basic Component class that doesn't derive from any control. The type of class depends on the type of provider you are creating.
A control-based provider, like the MenuTextProvider, uses a dedicated control to display information in a specific location on a form. In this example, the MenuTextProvider inherits from the StatusBar class. This means you can add the MenuTextProvider to any form, and it will act as an ordinary status bar and update its display to provide the appropriate text automatically. Another possible approach would be to derive the provider from the StatusBarPanel class. You could then add it to an existing status bar.
Choosing the object to extend
Once you've decided what type of provider you are creating, your next decision is to determine the type of object that you are extending. Many providers extend any type of Windows control, while some are limited to specific classes. To specify the appropriate type of object, you need to handle the IExtenderProvider.CanExtend() method. In this method, you look at the supplied type of object, and then make a decision about whether or not it can be extended by your provider. To make this decision you can evaluate any information about the target, including the type (the most common criteria), whether it is hosted in another control or on a form, and even its name. You return true if the object can be extended.
The MenuTextProvider only extends the MenuItem object. Here's the code that enforces this restriction:
public class MenuTextProvider : StatusBar, IExtenderProvider
{
public bool CanExtend(object extendee)
{
if (extendee.GetType() == typeof(MenuItem))
{
return true;
}
else
{
return false;
}
}
}
Providing an extended property
The next step is to identify the property that will be assigned to all extended controls. You do this by adding a ProvideProperty attribute just before your class declaration. The ProvideProperty attribute identifies the property name and the data type.
[ProvideProperty("HelpText", typeof(string))]
public class MenuTextProvider : StatusBar, IExtenderProvider
Once you've specified a property in this fashion, you need to provide corresponding Get and Set methods that perform the actual work when the property is changed. These members are preceded with "Get" or "Set" and use the same name you identified in the ProvidePoperty attribute. These methods must be public.
public void SetHelpText(object extendee, string value)
{
// (Code omitted.)
}
public string GetHelpText(object extendee) As String
{
// (Code omitted.)
}
Note that the GetProperty() method accepts a reference to the target and the SetProperty() method accepts a reference to the target and a value for the property. Keep in mind that a single instance of your extender can be reused to extend dozens of controls (and, conversely, two similar providers can extend the same control). This means that you need to keep track of all the extended controls in a collection. Our examples use the Hashtable class for this purpose, because it allows the object reference to be used as a key. (Remember, MenuItem objects are not controls, and do not have a unique Name property that can be used as a key).
The completed provider
To complete the MenuTextProvider, create a collection to store the Help text values for every extended control, and add the implementation logic for the SetHelpText() and GetHelpText() methods.
When the Help text is set, the provider registers to receive the Select event from the MenuItem and stores the Help text in the collection under the name of the control. When the Select event occurs, the Help text is retrieved and displayed in the status bar panel. We could just as easily monitor different events (like key presses, as the HelpProvider control does).
Here's the complete code:
using System;
using System.Windows.Forms;
using System.ComponentModel;
using System.Collections;
[ProvideProperty("HelpText", typeof(string))]
public class MenuTextProvider : StatusBar, IExtenderProvider
{
public bool CanExtend(object extendee)
{
if (extendee.GetType() == typeof(MenuItem))
{
return true;
}
else
{
return false;
}
}
private Hashtable helpText = new Hashtable();
public void SetHelpText(object extendee, string value)
{
// Specifying an empty value removes the extension.
if (value == ")
{
helpText.Remove(extendee);
MenuItem mnu = (MenuItem)extendee;
mnu.Select -= new EventHandler(MenuSelect);
}
else
{
helpText[extendee] = value;
MenuItem mnu = (MenuItem)extendee;
mnu.Select += new EventHandler(MenuSelect);
}
}
public string GetHelpText(object extendee)
{
if (helpText[extendee] != null)
{
return helpText[extendee].ToString();
}
else
{
return string.Empty;
}
}
private void MenuSelect(object sender, System.EventArgs e)
{
this.Text = helpText[sender].ToString();
}
}
Note
With extender providers, calling a Set method with an empty string is assumed to mean removing the extension. In the preceding example, this call causes the MenuHelpProvider to detach its event handler.You can set the Help text for a menu item with the SetHelpText() method (see Figure 7-17):
menuTextProvider1.SetHelpText(mnuNew,
" Create a new document and abandon the current one.");

Figure 7-17: The MenuTextProvider in action
The Help Icon Provider
In many ways, the next example is a more typical provider because it extends other controls without being a control itself. Instead, it derives from the System.ComponentModel.Component class.
The HelpIconProvider retrieves a reference to the form that contains the control and adds a miniature PictureBox control with a question mark icon in it. It also registers for the DoubleClick event for the picture box. If this occurs, a Help file is launched, with the specified context identifier for the control. The name of the Help file is global to the provider, and specified through a standard HelpFile property. To further refine the control, you could handle more events from the dynamically generated picture box, perhaps tailoring the mouse cursor when it is positioned over it.
using System;
using System.Windows.Forms;
using System.ComponentModel;
using System.Collections;
using System.Drawing;
[ProvideProperty("HelpID", typeof(string))]
public class HelpIconProvider : Component, IExtenderProvider
{
private Hashtable contextID = new Hashtable();
private Hashtable pictures = new Hashtable();
private string helpFile;
public bool CanExtend(object extendee)
{
if (extendee.GetType() == typeof(Control))
{
// Ensure the control is attached to a form.
if (((Control)extendee).FindForm() == null)
{
return false;
}
else
{
return true;
}
}
else
{
return false;
}
}
public string HelpFile
{
get
{
return helpFile;
}
set
{
helpFile = value;
}
}
public void SetHelpID(object extendee, string value)
{
Control ctrl = (Control)extendee;
// Specifying an empty value removes the extension.
if (value == ")
{
contextID.Remove(extendee);
// Remove the picture.
PictureBox pic = (PictureBox)pictures[extendee];
pic.DoubleClick -= new EventHandler(PicDoubleClick);
pic.Parent.Controls.Remove(pic);
pictures.Remove(extendee);
}
else
{
contextID[extendee] = value;
// Create new icon.
PictureBox pic = new PictureBox();
pic.Image = Image.FromFile("Help.gif");
// Store a reference to the related control in the PictureBox.
pic.Tag = extendee;
pic.Size = new Size(16, 16);
pic.Location = new Point(ctrl.Right + 10, ctrl.Top);
ctrl.Parent.Controls.Add(pic);
// Register for DoubleClick event.
pic.DoubleClick += new EventHandler(PicDoubleClick);
// Store a reference to the help icon so we can remove it later.
pictures[extendee] = pic;
}
}
public string GetHelpID(object extendee)
{
if (contextID[extendee] != null)
{
return contextID[extendee].ToString();
}
else
{
return String.Empty;
}
}
public void PicDoubleClick(object sender, EventArgs e)
{
// Invoke help for control.
Control ctrlRelated = (Control)((Control)sender).Tag;
Help.ShowHelp(ctrlRelated, helpFile, HelpNavigator.Topic,
contextID[ctrlRelated].ToString());
}
}
It's important to note that if you don't have a valid Help file and context identifier, nothing will happen when you click the Help icon. For this reason, the code download for this sample includes a message box that pops up to let you know the event has been detected. You will find out much more about the Help class this control uses to invoke the Help engine in Chapter 14.
To invoke this control, just specify a global Help file for the provider and set a Help context ID for a specific control. Figure 7-18 shows the HelpIconProvider in action.

Figure 7-18: A HelpIconProvider extending two text boxes
private void HelpIconHost_Load(object sender, System.EventArgs e)
{
helpIconProvider1.HelpFile = "myhelp.hlp";
helpIconProvider1.SetHelpID(TextBox1, "10001");
helpIconProvider1.SetHelpID(TextBox2, "10002");
}
Note
If you experience any trouble adding a control to your form, it is often because the reference is out of date. To correct this, remove the control reference, and then add the control from the Toolbox, which will automatically recopy the latest control assembly.One limitation with this provider is that it reads the image it displays from a file. That means that every client who uses the provider control also requires the Help icon picture in the project directory directory. The next chapter demonstrates a better approach that embeds the picture as a resource, so it can't be lost.