|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Contents
IntroductionVisual 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
This article assumes you have a basic understanding of the way provider
components work. If you haven't encountered the
The
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. What's wrong with Visual Studio?Let's say that we want to write an extender provider,
[ProvideProperty("DoMagic", typeof(System.Web.UI.Control))]
public class MyProvider : System.ComponentModel.Component, IExtenderProvider
{
// IExtenderProvider implementation
public bool CanExtend (object AExtendee)
{
return (AExtendee is System.Web.UI.Control)
}
// Retrieve the value of the DoMagic property for a control
public bool GetDoMagic (object AExtendee)
{
if (_Values.ContainsKey (AExtendee))
{
return _Values[AExtendee];
}
else
{
return false;
}
}
// Set the value of the DoMagic property for a control
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))
{
// Here the magic happens
}
}
// The provider component stores the property values
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, private void InitializeComponent()
{
this.MyProvider1 = new MyProvider();
}
Add a label and a textbox to the page and set the value of
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 The workaroundThe 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 private void InitializeComponent()
{
this.MyProvider1 = new MyProvider();
this.MyProvider1.PropertyData = @"AAEAA -- many more characters -- Cw==";
this.MyProvider1.VSDesigned = this;
}
The The second property, The Using the componentThe source code file ExtenderProvider.cs declares a base component
[ProvideProperty ("DoMagic", typeof(System.Web.UI.Control))]
public class MyProvider : GoodHeavens.ComponentModel.ExtenderProviderComponent
{
The properties declared by a The base class protected override bool CanExtendControl (System.Web.UI.Control AExtendee)
{
// Your logic goes here; return true to allow extension
}
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)
{
// GetControlPropertyValue arguments: control to extend,
// property name, default value
return (bool )Engine.GetControlPropertyValue (AExtendee, "DoMagic", false);
}
public void SetDoMagic (System.Web.UI.Control AExtendee, bool AYes)
{
// SetControlPropertyValue arguments: control to
// extend, property name, value, isdefault
// isdefault is a boolean that indicates
// whether the value is the default value.
Engine.SetControlPropertyValue (AControl, "DoMagic", AYes, !AYes);
}
At run-time you need to hook into events for the page generation process;
override the protected override void InitialiseHandlers ()
{
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)
{
// Here the magic happens
}
}
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
private void Page_Load (object sender, System.EventArgs e)
{
MyProvider1.SetDoMagic (this, true);
}
A walkthrough of the engine codeThe engine Property storageThe 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 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 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
Visual Studio hacksThe two public 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
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 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 Lifecycle managementThe behaviour of the component is different at design-time and run-time. We
keep track of the stage we are in (the
The first time Visual Studio initialises the component using the default
constructor. Once the As you may remember, if we allow the Support for extender providersApart from the The The History
NoteA copy of this article can be found on my website.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||