Contents
Visual Studio offers an elegant method to add extra properties to all or some
types of controls, in a way that resembles multiple inheritance. Traditionally
you would have to subclass all controls that need an extra property, put them in
a class library and add the subclassed controls to the Toolbox for design-time
support. The alternative is to create a component that implements the
IExtenderProvider
interface. Drop it in the component tray and
Visual Studio shows its properties with all suitable controls on the form. The
WinForms ToolTip provider is an example of a provider component.
This article assumes you have a basic understanding of the way provider
components work. If you haven't encountered the IExtenderProvider
before, read one of the introductory articles first:
The IExtenderProvider
mechanism is usually described for use in
WinForms, and there's a good reason for that. At the bottom of the MSDN library
article it says:
Note The implementation of an extender provider for
Windows Forms controls is different from that for
ASP.NET server
controls.
That's right. It doesn't work for ASP.NET server controls, because the
required design-time support in Visual Studio 2003 is broken. The good news is
that there's a workaround.
This article presents a class that can be used to develop extender providers
for ASP.NET server controls. First we show what's going wrong in Visual Studio
and how we fix it. If you're only interested in the component, skip that part
and read only the section Using the component.
We only discuss extender providers implemented as components. The DefaultButtons
control by Andy Smith is an example of an extender provider based on a web
control.
Let's say that we want to write an extender provider,
MyProvider
, that does something terribly useful. It adds a single
property to all web controls, a boolean called DoMagic
:
[ProvideProperty("DoMagic", typeof(System.Web.UI.Control))]
public class MyProvider : System.ComponentModel.Component, IExtenderProvider
{
public bool CanExtend (object AExtendee)
{
return (AExtendee is System.Web.UI.Control)
}
public bool GetDoMagic (object AExtendee)
{
if (_Values.ContainsKey (AExtendee))
{
return _Values[AExtendee];
}
else
{
return false;
}
}
public void SetDoMagic (object AExtendee, bool AYes)
{
_Values[AExtendee] = AYes;
if (AYes)
{
(AControl as System.Web.UI.Control).PreRender +=
new EventHandler (MagicStuff);
}
}
private void MagicStuff (object sender, EventArgs e)
{
if (GetDoMagic (sender))
{
}
}
private Hashtable _Values = new Hashtable ();
}
Put this component in a class library, compile the library, add the component
to the Toolbox and drop it in the component tray of a new ASP.NET page. All web
controls in the page now have an extra property, DoMagic
. Visual
Studio automatically adds an InitializeComponent
method to the code
behind the page:
private void InitializeComponent()
{
this.MyProvider1 = new MyProvider();
}
Add a label and a textbox to the page and set the value of
DoMagic
to true for both of them. You would expect that the
InitializeComponent
method now reads:
private void InitializeComponent()
{
this.MyProvider1 = new MyProvider();
this.MyProvider1.SetDoMagic (this.Label1, true);
this.MyProvider1.SetDoMagic (this.TextBox1, true);
}
But instead it says:
private void InitializeComponent()
{
this.MyProvider1 = new MyProvider();
this.MyProvider1.SetDoMagic (this, false);
}
Visual Studio does not know how to store the values for provided properties!
It should add a SetDoMagic
call for each control that has a
non-default property value, but instead it adds a single call to
SetDoMagic
for the page object, always passing the default property
value. A little more research shows that it adds a SetXXX
call for
each property XXX
that has been declared by a
ProvideProperty
attribute for a type that matches the page object,
provided that the extender can extend the page object.
The property value storage problem is the only bug in Visual Studio that
affects working with extender providers in ASP.NET pages - all other design-time
support works fine. But it is a critical bug: you can't work with extender
providers unless you find a way to store and initialize the property values
correctly.
The core of our workaround is to use (de)serialization to store all property
values. The InitializeComponent
method will look like:
private void InitializeComponent()
{
this.MyProvider1 = new MyProvider();
this.MyProvider1.PropertyData = @"AAEAA -- many more characters -- Cw==";
this.MyProvider1.VSDesigned = this;
}
The PropertyData
string contains serialized values of the
provided properties (i.e., of the _Values
hash table in the
example). As PropertyData
is a public property of the provider
component, Visual Studio knows how to handle it at design-time.
The second property, VSDesigned
, is needed to deserialize the
PropertyData
string. The string cannot contain object references
(like this.Label1
), so we serialize the name of the controls. In
the deserialization process we need to know the top-level (Page or UserControl)
object to turn the names into control references again. As a component has no
direct access to its owner at run-time, we need an extra property to achieve
that.
The SetDoMagic
calls would follow the assignment of the
properties. As the assignments are incorrect, we don't want them to appear in
the InitializeComponent
method. This is easy: tell Visual Studio
that the Page
or UserControl
object cannot be extended
and the SetDoMagic
calls disappear. As a consequence, it is not
possible to set a provided property for the page object at design-time using the
IDE. If you need to do that, add a SetDoMagic
call by hand.
The source code file ExtenderProvider.cs declares a base component
ExtenderProviderComponent
and a support class
ExtenderProviderEngine
. If your extender provider is a component,
derive a component from ExtenderProviderComponent
and add the
ProvideProperty
attributes:
[ProvideProperty ("DoMagic", typeof(System.Web.UI.Control))]
public class MyProvider : GoodHeavens.ComponentModel.ExtenderProviderComponent
{
The properties declared by a ProvideProperty
attribute must be
serializable. All native .NET types and enumerations are serializable; more
complex types must implement the ISerializable interface. Note that you cannot
add two ProvideProperty
attributes for the same property with
different types - only the top one is seen by Visual Studio - this is also an
undocumented "feature".
The base class ExtenderProviderComponent
already implements the
IExtenderProvider
interface, allowing only web controls to be
extended. If you want to restrict that even further, override the
CanExtendControl
method:
protected override bool CanExtendControl (System.Web.UI.Control AExtendee)
{
}
The base class provides storage for properties. Every property has a (unique)
name. Use that in the Get/Set functions for the provided properties:
public bool GetDoMagic (System.Web.UI.Control AExtendee)
{
return (bool )Engine.GetControlPropertyValue (AExtendee, "DoMagic", false);
}
public void SetDoMagic (System.Web.UI.Control AExtendee, bool AYes)
{
Engine.SetControlPropertyValue (AControl, "DoMagic", AYes, !AYes);
}
At run-time you need to hook into events for the page generation process;
override the InitialiseHandlers
method:
protected override void <A name=InitialiseHandlers>InitialiseHandlers</A> ()
{
foreach (System.Web.UI.Control c in Engine.ControlsWithValue ("DoMagic")
{
c.PreRender += new EventHandler (MagicStuff);
}
}
And finally you have to add the event handlers that implement all that
magical stuff your component is famous for:
private void MagicStuff (object sender, EventArgs e)
{
}
}
Add your component to a class library, add the component to the Toolbox and
you're ready to go. Values for the provided properties can be entered via the
Visual Studio IDE for all web controls except the page or
UserControl
you are designing (the "this
" in the code
behind). If you need to do that, add a line of code to the
Page_Load
method by hand:
private void Page_Load (object sender, System.EventArgs e)
{
MyProvider1.SetDoMagic (this, true);
}
The engine ExtenderProviderEngine
consists of four parts, which
we'll discuss in this order: storage of property values, hacks to make Visual Studio work, object lifecycle management and support
for custom extender providers. The best documentation is the source code itself,
but in this section we want to highlight a few crucial design decisions.
Property storage
The provided properties are identified by a string code. The code may be the
same as the property name, but it does not have to. The code is used as key for
a hash table that has a second hash table as value. That second table has the
extended control as key, and its value is the property value. Only non-default
property values are stored.
The main issue here is serialization. We use a custom class to represent the
hash tables and implement the ISeriazable
interface, because we
found that the Hashtable
class is not serialized correctly at
design time. Our custom class writes the keys as strings, and uses the .NET
serialization mechanism to serialize the values. This is the reason that the
property values must be of a type that is serializable. The serialized data is
base64 encoded and stored in a string:
public string PropertyData
{
get
{
if (HasPropertyData)
{
MemoryStream ms = new MemoryStream ();
BinaryFormatter bf = new BinaryFormatter ();
bf.Serialize (ms, _PropertyList);
return Convert.ToBase64String (ms.ToArray());
}
else
{
return null;
}
}
set
{
MemoryStream ms = new MemoryStream (Convert.FromBase64String (value));
BinaryFormatter bf = new BinaryFormatter ();
_PropertyList = (NamedKeyCollection )bf.Deserialize (ms);
}
}
Normally the control objects are the keys for the hash table. We cannot
serialize object references, so we write the UniqueID
of the
controls. However, if you design a UserControl
, the
UniqueID
also includes the name of the control, and that name is
different at design-time and run-time. We store the name of the control relative
to the UserControl
(or Page
object) in serialization,
so we need to have a reference to the UserControl
in the
deserialization process to turn the names into object references again. This
explains the VSDesigned
property: after deserialization the
controls are still referenced by a name, but once the VSDesigned
property is set we can look up the controls:
public System.Web.UI.Control VSDesigned
{
get
{
return _VSDesigned;
}
set
{
if (value != null)
{
_VSDesigned = value;
ReplaceKeys ();
}
}
}
This serialization procedure leads to the requirement that we can only extend
web controls, as they are the only ones that have an easy-to-use name. It is
possible to get this to work for components as well, using
Site.Name
at design-time and reflection at run-time, but that makes
the code more complex, as it is not that easy to retrieve the Page
or UserControl
object from a component at design time.
The two public properties PropertyData
and
VSDesigned
take care of the publication of the serialized property
values, but there are two more issues: how to find the value for
VSDesigned
, and how to get Visual Studio to save the properties.
We have no control over the creation of the extender provider at design-time.
When it is first created, the default constructor for a component is used. As a
component has no method to retrieve the object it is a owned by, we have to find
another way. We know that after creation the
IExtenderProvider.CanExtend
and the Set/Get methods for the
provided properties are called. If one of those is called for a web control we
can walk up the control hierarchy to find the Page
or
UserControl
object. We find the control that is highest in the
hierarchy that still has a non-empty Site.Name
property (even in
case of a UserControl the top of the hierarchy is a Page
object):
private void CheckVSDesigned (System.Web.UI.Control AExtendee)
{
for (System.Web.UI.Control cComponent = AExtendee;
cComponent != null; cComponent = cComponent.Parent)
{
if (cComponent.Site != null && cComponent.Site.Name.Length > 0)
{
_VSDesigned = cComponent;
}
}
if (_VSDesigned != null)
{
NotifyDesignerOfChange ();
}
}
Thus the value of the VSDesigned
property changes while another
control on the page is being examined. Visual Studio is not smart enough to
recognize this. We do that by calling the NotifyDesignerOfChange
method, which was largely provided by Paul
Easter:
protected void NotifyDesignerOfChange ()
{
IDesignerHost dhDesigner = _Site.GetService
(typeof (IDesignerHost)) as IDesignerHost;
if (dhDesigner != null)
{
IComponentChangeService ccsChanger = dhDesigner.GetService
(typeof (IComponentChangeService)) as IComponentChangeService;
ccsChanger.OnComponentChanging (_Component, null);
ccsChanger.OnComponentChanged (_Component, null, null, null);
}
}
This method also is called if any of the property values might have changed.
In response, Visual Studio examines the properties of the extender provider and
updates the assignments in the page's InitializeComponents
method
if they are changed.
The behaviour of the component is different at design-time and run-time. We
keep track of the stage we are in (the LifeCycleStage
enumeration):
| Design-time | Run-time |
---|
Extender provider is created | DesignTime | Initialised |
After PropertyData assignment | PropertyDataLoaded | PropertyDataLoaded |
After VSDesigned assignment | DesignTime | RunTime |
The first time Visual Studio initialises the component using the default
constructor. Once the PropertyData
and VSDesigned
properties have been set, Visual Studio creates another instance and sets both
properties using the values mentioned in the InitializeComponents
method. At run-time, the InitializeComponents
method is executed,
and the InitialiseHandlers method is called so
that the extender provider can hook in to the page building process.
As you may remember, if we allow the Page
or
UserControl
object to be extended, Visual Studio will add incorrect
SetXXX
calls (because the property value is incorrect) after the
property assignments. We have looked into a way to detect and ignore these
calls. What is needed is a method to get Visual Studio call one of out
component's methods after the SetXXX
calls. At run-time that is
possible: just subscribe to the page's OnInit
event, which is
raised just after the call to InitializeComponents
. The problem is
that this event is not raised when the component's properties are set at
design-time. Thus it seems to be impossible to set up a reliable way to edit the
provided properties for the Page
or UserControl
object
using the IDE.
Apart from the GetControlPropertyValue
and
SetControlPropertyValue
methods to access the property values,
there are other methods that extender providers can use.
The PropertyHasValues
method indicates whether there are
controls that have a non-default value for a particular property. The
HasPropertyData
property tells you whether there are any controls
with a non-default value.
The ControlsWithValue
method returns a collection of all
controls with non-default values (optionally for a particular property). The
collection can be used in a foreach
statement.
History
- July 26, 2004 - Written.
- October 3, 2004: Source code updated
Note
A copy of this article can be found on my website.