Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Getting to know IExtenderProvider

0.00/5 (No votes)
2 Aug 2003 2  
A walkthrough in the creation of a trivial IExtenderProvider component

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 Checkboxes, RadioButtons, and Buttons 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 Panels and hooks the Paint event to draw a custom string in the middle; the second will be applied to all types of Buttons 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.

  1. It inherits from Component and implements IExtenderProvider.
  2. Defines a helper class to contain the values for the two properties we will be adding.
  3. 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.

Adding your properties

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!

Coding the properties

[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.

Hooking up the event handlers

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.

More advanced property management

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 Controls 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.

Problems to be aware of

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);

    // This shouldn't get called at run time,

    // but in case it does don't do anything

    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.

Yet another way

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.

Conclusion

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.

Article History

  • August 3rd, 2003
    • Initial posting

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here