Note: A demo isn't provided because the IExtenderProvider is a design time tool, the runtime value is to be provided by your code which makes use of the design time enhancements.
Table of Contents
What is an IExtenderProvider component?
In my own words I would describe an IExtenderProvider
component as a component which extends (or adds) its own properties to other controls or components in the VS.NET designer. These properties are then used by the component to perform some task. Some examples of IExtenderProviders
in the framework are the ToolTip
and ErrorProvider
controls/components. Just on CodeProject there are at least a couple uses for an IExtenderProvider
, one adds images to menus another modifies Toolbar behavior. I was also working on another to add images to menus, which is what eventually lead to writing this article.
Creating your own IExtenderProvider component
While I was writing another article (to be published at a later date) Nnamdi, suggested that I cover how to create your own IExtenderProvider
component. This discussion was going to be a part of that article, but instead it has grown and is now its own article after I found that there wasn't one dedicated to IExtenderProvider
on CodeProject.
The basics
The IExtenderProvider
interface only has one method, bool CanExtend(object)
, this method tells the designer whether the extended properties should be displayed for the given object. Notice that this interface has no information about said properties, the designer gets the properties by looking for the ProvideProperty
attribute on the component implementing IExtenderProvider
. This method should return true to have the designer use reflection to begin looking for the extended properties.
The ProvideProperty
attribute specifies the property name as well as the type of objects it can be applied to, this just checks whether the type is a derivative of the control selected. If you wanted a property to appear on all Checkbox
es, RadioButton
s, and Button
s you would specify typeof(ButtonBase)
as the type.
Once you have the attribute added you need to write two public methods, these methods act as the Get/Set parts of a regular property and are named Get<Name>
and Set<Name>
. The Get<Name>
method takes in a type of the kind the ProvideProperty
attribute defines and returns the data type of the property. The Set<Name>
method takes two arguments, the first is of the type of specified in the ProvideProperty
attribute, the second is of the same data type as the Get<Name>
returns, this is the value the user wishes to set the property to.
Because the extended properties are applied to all controls of a specific type chances are that you will have to deal with the property being used on multiple instances of the controls. This calls for a Hashtable
being used to maintain the association between the specific instances and the extended property or properties on each instance. I prefer to use a nested class with public members to store the property values, this enables me to only have one Hashtable
in memory. It also makes it easier to add or remove properties from the extender while I create it.
Another interesting point is that you can still apply some of your favorite design time attributes to the extended properties, simply apply that attribute to the Get<Name>
method and the designer should pull it out. Unfortunately the "standard" code to provide a drop-down to let you select an image from an image list doesn't work with an extended property. You could still rewrite the editor so that it was aware of the proper ImageList
control.
A code sample here would probably help out a lot since it can get confusing to talk about this abstractly. In this sample the IExtenderProvider
component will provide two properties: one will be applied to only Panel
s and hooks the Paint
event to draw a custom string in the middle; the second will be applied to all types of Button
s and will simply provide an integer >= -1, suitable for another ImageIndex
type of control, though there will be no effect in doing so.
It is important to note, that while our IExtenderProvider
is adding properties to objects of type Panel
and ButtonBase
, the provider itself can be of any type. The most common are provided as Components
so they can sit in the component tray of the Windows Forms designer.
public class MyExtender : Component, IExtenderProvider
{
private Hashtable properties;
public MyExtender()
{
props = new Hashtable();
}
public MyExtender(IContainer parent) : this()
{
parent.Add(this);
}
bool IExtenderProvider.CanExtend(object o)
{
if( o is Panel || o is ButtonBase )
return true;
else
return false;
}
private Properties EnsurePropertiesExists(object key)
{
Properties p = (Properties) properties[key];
if( p == null )
{
p = new Properties();
properties[key] = p;
}
return p;
}
private class Properties
{
public string MyString;
public int MyInt;
public Properties()
{
MyString = System.Environment.Version.ToString();
MyInt = -1;
}
}
}
First things first, this simple component does three things.
- It inherits from
Component
and implements IExtenderProvider
.
- Defines a helper class to contain the values for the two properties we will be adding.
- Create a private method to ensure the helper class gets added to the
Hashtable
.
This is the basic skeleton you will probably use in your own extenders, this leaves three things for you to do to create your own extender and we will do that now.
The first thing to do is to add the ProvideProperty
attributes to the component
[ProvideProperty("MyString", typeof(Panel))]
[ProvideProperty("MyInt", typeof(ButtonBase))]
public class ....
These attributes specify two things, the name of the extended property and the Type
representing the objects this property will get added to. This attribute can only be specified once for a property so you need to choose the most general Type
that you require. The caveat to this is that the CanExtend
method (from the IExtenderProvider
interface) doesn't know what property it is returning true or false for.
This means that if you want two properties to be provided, one to Button
and CheckBox
types, and the other to the RadioButton
type you'll have to split the provider into two different components. This is because the most specific way that property one can be specified is by typeof(ButtonBase)
, normally this isn't a problem because the CanExtend
method could just return false if the object was a RadioButton
. But CanExtend
doesn't know which property it is being called for, so it has to return true for a RadioButton
which means that property one will inadvertently get added to RadioButtons
.
Now that the designer knows to look for our properties its time to add the code for them!
[Description("This string will be drawn in the center of the panel")]
[Category("Appearance")]
public string GetMyString(Panel p)
{
return EnsurePropertiesExists(p).MyString;
}
public void SetMyString(Panel p, string value)
{
EnsurePropertiesExists(p).MyString = value;
}
The methods are fairly simple to write, but later you'll see that you can do more complex things such as add event handlers. Now for the MyInt
property methods.
[DefaultValue(-1)]
[Category("Something")]
[Description("This is used by some code somewhere to do something")]
public int GetMyInt(ButtonBase b)
{
return EnsurePropertiesExists(b).MyInt;
}
public void SetMyInt(ButtonBase b, int value)
{
if( value >= -1 )
EnsurePropertiesExists(b).MyInt = value;
else
throw new ArgumentOutOfRangeException("MyInt", value,
"MyInt must be greater than or equal to -1");
}
You will notice three things about each set of methods, I apply the attributes that would normally go on the property to the Get method, each method takes as its first argument an instance of the type specified in the ProvideProperty
attribute. The Get method returns and the Set methods second argument is an instance of the type the property should be -- for the first set string
, for the second set int
.
There is another part missing from the code, part of the MyString
property was that it was supposed to draw the string in the middle of the Panel
.
public void SetMyString(Panel p, string value)
{
EnsurePropertiesExists(p).MyString = value;
if( value != null && value.Length > 0 )
{
p.Paint += new PaintEventHandler(PanelPaint);
}
else
{
p.Paint -= new PaintEventHandler(PanelPaint);
}
p.Invalidate();
}
private void PanelPaint(object sender, PaintEventArgs e)
{
string str = EnsurePropertiesExists(sender).MyString;
if( str != null && str.Length > 0 )
{
Panel p = (Panel) sender;
SizeF size = e.Graphics.MeasureString(str, Control.DefaultFont);
float x = (float) (p.Width - size.Width) / 2.0f;
float y = (float) (p.Height - size.Height) / 2.0f;
e.Graphics.DrawString(str, Control.DefaultFont,
SystemBrushes.ControlText, x, y);
}
}
The code above will either add or remove the Paint
event handler depending on whether MyString
is set to a non-null and non-empty string. The second method is responsible for doing the painting, but just as a sanity check I double check to make sure it isn't called with a null or empty string. After the string has changed the panel is then invalidated so that the new string is drawn.
As well as having Get/Set methods you can also control code serialization and have a more advanced reset functionality. Like the Get/Set methods they have special names (these methods apply to all properties, not just the extended ones), bool ShouldSerialize<name>
and void Reset<name>
. The typical implementation (ie when dealing with real properties) takes no arguments, but because extended properties need an object so that it can match the property with the value these methods take the same arguments as the Get method. Unlike the Get/Set methods these methods are only used by the designer, so they can be private methods rather than public ones.
ShouldSerialize<name>
should return false if the current value is the default and return true if it is different. Reset<name>
can only called if ShouldSerialize<name>
returns true. You could also apply the DefaultValue
attribute, but it doesn't fit all cases. For instance you are allowing menu items to have different fonts used on them and you default to SystemInformation.MenuFont
.
Because the system's menu font can be different on each computer you cannot specify the default value in an attribute as the default isn't a constant, but you can use the ShouldSerialize<name>
method to determine whether the selected font is the same as the system's menu font. This is how Control
s seem to always know the system's default font to use without you having to specify the default font yourself.
To extend our example we will have the default value of the MyString
property be set to the version of runtime the application is running under.
private bool ShouldSerializeMyString(Panel p)
{
Properties props = EnsurePropertiesExists(p);
if( GetMyString(p) != System.Environment.Version.ToString() )
return true;
else
return false;
}
private void ResetMyString(Panel p)
{
SetMyString(p, System.Environment.Version.ToString());
}
Now if you do a build of the component and go back to your project you can see two things, first is that you can right-click on the MyString
property and choose Reset to have the runtime version be set as the value. You can also look at the generated code and see that the call to SetMyString
is no longer present, change the MyString
property and go back to the code and you'll find it has been re-added.
There are two implications having a Reset or DefaultValue
option brings. First is that if your property is Reset or the same as the DefaultValue
attribute then no code will be written to set that property for that object. If no code is written for that property then your extender is not aware of that control instance, the code above demonstrates what this means. Go ahead and Reset the MyString
property again, you should see that the string drawn inside the panel changes to the current CLR version. Now close the form editor and re-open it. You'll notice that the string is no longer drawn in the Panel but that the MyString property is set to the CLR version.
Why is this? If we follow the code execution we can see why. In the Form's InitializeComponent
method you'll notice that the call to this.extender.SetMyString
has been eliminated. This was because the value of MyString
is at its default so the designer doesn't think the code is needed. What the designer doesn't know does hurt us! The call to SetMyString
is what hooks up the Paint
event handler and also makes the extender aware of that Panel
instance. Because the extender isn't aware of the panel and the Paint
event handler isn't hooked up, the string never gets drawn in the Panel
.
There are some ways around this, one is to not allow the MyString
property to have a default value or be reset. This ensures that your SetMyString
method is always called, so your extender is always aware of the Panel
. Another thing you can do is to provide another property that can't be reset and doesn't have a default value, this allows your real extended property to Reset, but keeps the extender aware of the Panel
.
I will also modify the code to use this second technique, with a slight twist. ShouldSerialize<name>
will always return true and its value will always be true (in other words the Set<name>
method doesn't modify a property value). Its definitely a hack, but a good hack because it should always work.
We start by adding a new property, I'll call it HookToPanel. This property will always return true, and we will pull the Paint
event logic out of SetMyString
and put it into this property's Set method.
[ProvideProperty("MyString", typeof(Panel))]
[ProvideProperty("HookupToPanel", typeof(Panel))]
....
[Category("Design")]
[DefaultValue(false)]
public bool GetHookupToPanel(Panel p)
{
EnsurePropertiesExists(p);
if( DesignMode )
{
p.Paint -= new PaintEventHandler(PanelPaint);
p.Paint += new PaintEventHandler(PanelPaint);
p.Invalidate();
}
return true;
}
public void SetHookupToPanel(Panel p, bool value)
{
Properties prop = EnsurePropertiesExists(p);
p.Paint -= new PaintEventHandler(PanelPaint);
p.Paint += new PaintEventHandler(PanelPaint);
p.Invalidate();
}
If you recompile the extender then reload the Form you might not see any changes, but if you look at the code you'll see that a new line was added.
this.extender.SetHookupToPanel(this.panel1, true);
This line is what will set up the Paint
event handler, as well as ensure that the handler doesn't get hooked up multiple times.
Rather than hook up the Paint event handler in the SetHookupToPanel
method we can defer it until the end of the InitializeComponent
method by implementing another interface, ISupportInitialize
. When a control or component implements this interface the designer inserts two method calls into the InitializeComponent
method of the designer. The first, appropriately called BeginInit
, is called before any of the properties are set on a control. The second, called EndInit
, is called just before the end of the InitializeComponent
method. We can take advantage of the fact that all properties, including extended properties, are set by this point to hook up our event handlers to the Paint
method.
Begin by changing the SetHookupToPanel
method, all it is responsible for is ensuring that our extender is aware of the Panel
(by having a Properties
class instance in the Hashtable
, keyed to that Panel
).
public void SetHookupToPanel(Panel p, bool value)
{
EnsurePropertiesExists(p);
}
Next implement the ISupportInitialize
interface (don't forget to add it to the class declaration)
void ISupportInitialize.BeginInit()
{
}
void ISupportInitialize.EndInit()
{
if( !DesignMode )
{
foreach(DictionaryEntry de in properties)
{
Panel p = de.Key as Panel;
if( p != null )
{
p.Paint += new PaintEventHandler(PanelPaint);
}
}
}
}
The EndInit
method enumerates through each key/value pair in the Hashtable
, if the key is a Panel
then we can proceed to hook up Paint
event. Notice that I left in the event hookup in the GetHookupToPanel
method, this is done so that the designer reflects our changes when the MyString
property changes. Similarly, the EndInit
method doesn't hook up to the Paint
event unless it isn't in DesignMode
.
In this article you saw how to create a basic IExtenderProvider
component. You then learned some techniques to allow your extended properties to better mimic real properties and how you can combine your component with ISupportInitialize
to cut down on messy code.
IExtenderProvider
components can enhance the design time experience while also providing a nice benefit at runtime. With a little work and a lot of knowledge you can create wonderful drag and drop components that can greatly enhance the runtime behavior of some controls.