Click here to Skip to main content
15,881,882 members
Articles / Web Development / ASP.NET
Article

Extender provider components in ASP.NET: an IExtenderProvider implementation

Rate me:
Please Sign up or sign in to vote.
4.13/5 (8 votes)
4 Oct 200410 min read 71.8K   709   39   3
A workaround for the broken Visual Studio support for extender providers in ASP.NET

Contents

Introduction

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.

What's wrong with Visual Studio?

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:

C#
[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, DoMagic. Visual Studio automatically adds an InitializeComponent method to the code behind the page:

C#
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:

C#
private void InitializeComponent()
{
  this.MyProvider1 = new MyProvider();
  this.MyProvider1.SetDoMagic (this.Label1, true);
  this.MyProvider1.SetDoMagic (this.TextBox1, true);
}

But instead it says:

C#
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 workaround

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:

C#
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.

Using the component

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:

C#
[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:

C#
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:

C#
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 InitialiseHandlers method:

C#
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:

C#
  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 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:

C#
private void Page_Load (object sender, System.EventArgs e)
{
  MyProvider1.SetDoMagic (this, true);
}

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:

C#
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:

C#
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):

C#
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:

C#
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-timeRun-time
Extender provider is createdDesignTimeInitialised
After PropertyData assignmentPropertyDataLoadedPropertyDataLoaded
After VSDesigned assignmentDesignTimeRunTime

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.

History

  • July 26, 2004 - Written.
  • October 3, 2004: Source code updated

Note

A copy of this article can be found on my website.

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


Written By
Netherlands Netherlands
I'm a senior consultant at Capgemini in The Netherlands since 2004. Previously I've worked at ARCADIS (more on the software) and the Dutch branch of Resource Analysis.

The latest versions of the source code of my articles can be found on my website.

Comments and Discussions

 
GeneralDeleting controls can screw things up! Pin
BR123418-Oct-04 12:33
BR123418-Oct-04 12:33 
GeneralAnother way Pin
geneu5-Oct-04 0:46
geneu5-Oct-04 0:46 
GeneralDoesn't work Pin
rendle27-Sep-04 23:17
rendle27-Sep-04 23:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.