A walkthrough of the engine code
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)
{
// Serialize the data
MemoryStream ms = new MemoryStream ();
BinaryFormatter bf = new BinaryFormatter ();
bf.Serialize (ms, _PropertyList);
return Convert.ToBase64String (ms.ToArray());
}
else
{
return null;
}
}
set
{
// (Code omitted; see source file)
// Deserialize the data
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.
Visual Studio hacks
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)
{
// (Code omitted; see source file)
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 ();
}
// (Code omitted; see source file)
}
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 ()
{
// (Code omitted; see source file)
// Get the designer objects from the Site
IDesignerHost dhDesigner =
_Site.GetService (typeof (IDesignerHost)) as IDesignerHost;
if (dhDesigner != null)
{
IComponentChangeService ccsChanger =
dhDesigner.GetService (typeof (IComponentChangeService))
as IComponentChangeService;
// Raise the OnComponentChanged to tell the designer that our
// component has changed. "_Component" in this case is the
// component that has changed.
// You need to call OnComponentChanging as well!
ccsChanger.OnComponentChanging (_Component, null);
ccsChanger.OnComponentChanged (_Component, null, null, null);
}
// (Code omitted; see source file)
}
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.
Lifecycle management
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.
Support for extender providers
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.