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

Fixing the IExtenderProvider in Visual Studio's ASP.NET designer

0.00/5 (No votes)
4 Jan 2005 1  
An implementation of a custom CodeDomSerializer for repairing the IExtenderProvider in Visual Studio's ASP.NET designer.

Introduction

Provider controls are a design time method of extending existing controls on the designer surface based on the IExtenderProvider interface. Visual Studio .NET 2003 also supports the IExtenderProvider interface in its Windows Forms designer. It, however, does not properly support the use of the IExtenderProvider in its ASP.NET design time environment.

Properties set on an IExtenderProvider are serialized into code statements in the code-behind file. Figure 1 displays the type of code which should be present for a fictitious ASP.NET ToolTip provider. This code is not generated correctly, resulting in a code-behind file resembling Figure 2. Because the code isn�t generated, it will not be compiled. Thus the logic provided by the IExtenderProvider isn�t present at runtime.

ToolTipExtender Extender1;
Button MyButton;

private void InitializeComponent()
{
    this.Load += new EventHandler(
        this.Page_Load);
    this.Extender1.SetToolTip(
        MyButton, "SomeTooltip");
}
ToolTipExtender Extender1;
Button MyButton;

private void InitializeComponent()
{
    this.Load += new EventHandler(
        this.Page_Load);
}


Figure 1 - Required code

Figure 2 - Actual code

This article proposes a method to remove Visual Studio's limitations towards the IExtenderProvider in its ASP.NET designer. A limited understanding of the IExtenderProvider interface is required for proper understanding of this article.

Perhaps one of the reasons why the IExtenderProvider isn�t supported in Visual Studio's ASP.NET designer can be derived from a report on their feedback centre. This report can be found at Microsoft.

Behind the scenes of the designer

In order to solve the problem with the IExtenderProvider, first, a basic understanding needs to be achieved of what's going on behind the scenes of the Visual Studio designer. Visual Studio offers two basic methods of editing a Page or UserControl, one is a direct view of the HTML and code-behind file, the other is the visual designer. The designer displays the HTML file after the server controls in the HTML file have been compiled. This means the designer contains an object graph corresponding with the items declared in the code-behind file. When a property is changed on one of the components on the designer surface, either the HTML file is updated to reflect that change, or the corresponding object is updated. When you switch from the designer to the code view, the objects in the designer are serialized into code statements, which are placed in the 'Web Form Designer Generated Code' section. If you make changes in the code file and switch back to the designer, the code is deserialized into an object graph again (sometimes, you�ll need to refresh the designer in order to see the changes made in the code-behind file). When switching to and from the designer, a process takes place called serialization. CodeDom forms an intermediate layer in this serialization process. The CodeDom provides various types corresponding with common code elements and offers classes to perform operations on CodeDom. The following figure displays how CodeDom is used in the process of switching to and from the designer.

Figure 3 - Background process when switching between design- and code view.

Examining the problem

Now that it�s clear what is going on between the Visual Studio designer and the source code that it designs, it can now be determined what the problem with the IExtenderProvider might be. It will be interesting to see which part of the IExtenderProvider isn�t functioning properly. Figure 3 shows that there are two different processes to examine, each consisting of two steps. The first process, switching from code view to the designer, entails parsing the code and deserializing the parsed statements into objects. The second process, switching back to the code view, consists of serializing the objects into CodeDom and generating code from those CodeDom statements.

The code accompanying this article contains a broken ToolTip provider which can be utilized to examine the problem. The provider is able to extend Button classes and should be placed on a Page or UserControl containing a Button in order to test with it.

Switching from code view to the designer

The first problem to examine is switching from the code view to the designer. The Button and ToolTip should be on the Page and the Button should not have a ToolTip set.

Using the code view, a line of code is inserted to the designer section calling the �SetToolTip� method for the Button. When the switch from the code view to the design view is made, the text set using the code view can be observed in the properties of the Button. This means, the deserialization process is functioning correctly and doesn�t need a fix.

If you are unsure this method actually works, try changing the background color of the Button in the code-behind file. Switch to the designer and refresh the page to prove the correctness of this method.

Switching from designer to the code view

The second test is easy enough, just by setting a text on the ToolTip property of the Button and switching to the code view, it can be observed that the code for the ToolTip isn�t generated. This part will have to be addressed by the fix. Notice that there are two parts to the serialization process; building a CodeStatementCollection using a CodeDomSerializer and generating source code from the collection using a CodeGenerator.

The IExtenderProvider requires a method call to set one of the provided properties. This makes it unlikely that the CodeGenerator is causing the problems. Since the CodeGenerator is tied to a language, it will always be able to write method calls corresponding with a CodeStatement.

The IExtenderProvider requires a method call to set one of the provided properties. This makes it unlikely that the CodeGenerator is causing the problems. Since the CodeGenerator is tied to a language, it will always be able to write method calls corresponding with some CodeStatement.

Extending the Visual Studio designer

When extending the design time environment of Visual Studio using a custom CodeDomSerializer, two things have to be addressed; building the serializer and attaching it to an IExtenderProvider component. Let�s run through both things.

A generic CodeDomSerializer

The CodeDomSerializer which will support Visual Studio will have to be generic enough to be applied to all IExtenderProvider components. This means, no knowledge of the names and types of properties is available. This knowledge has to be obtained by using Reflection.

A subclass from CodeDomSerializer is required to implement two methods; Serialize and Deserialize. The Serialize method receives an object to serialize into a CodeStatementCollection. The Deserialize method does the opposite, creating an object from the statements in the collection. Since the Serialize method is broken, let�s tackle the Deserialize method first.

Deserializing an IExtenderProvider

This method doesn�t need exhaustive work, because the original Deserialize method isn�t broken. The base class declares the Deserialize method as abstract, so it will have to be implemented.

Because IExtenderProvider components are placed on a Page or UserControl at design time, the provider has to be a subclass of Component. There is a derived CodeDomSerializer specialized for serializing Components. Using the objects passed into the Deserialize method, a reference to a CodeDomSerializer for the Component class can be obtained. Given the fact that building a custom CodeDomSerializer is not something which is required often, the CodeDomSerializer for the Component class will be sufficient in ninety-eight percent of the cases. Figure 4 displays the code required to call the Component serializer.

public override object Deserialize(
    IDesignerSerializationManager manager,
    object codeDomObject)
{
    CodeDomSerializer baseSerializer =
        (CodeDomSerializer)manager.GetSerializer(
            typeof(Component),
            typeof(CodeDomSerializer));

    return baseSerializer.Deserialize(
        manager, codeDomObject);
}

Figure 4 - Implementation of the Deserialize method.

Serializing an IExtenderProvider

The serialization process will take more steps than deserialization. Since the IExtenderProvider could provide properties to every component on the designer surface, it is required to run through each component. Let's start with creating the first part of the required steps, the overridden Serialize method. Figure 5 displays this method.

Because this serializer should only be applied to IExtenderProvider components, verification is made to ensure the serializer is applied correctly. Next, the CodeDomSerializer for the base class is used to serialize all the normal properties which the IExtenderProvider may contain. This leaves only the extended properties for the customized serialization process. A reference to the collection of components on the designer surface can be obtained using the service model; the IDesignerHost service contains a reference to these components.

public override object Serialize(
    IDesignerSerializationManager manager,
    object value)
{
    if(!(value is IExtenderProvider)){
        throw new ArgumentException();
    }

    CodeDomSerializer baseSerializer =
        (CodeDomSerializer)manager.GetSerializer(
                value.GetType().BaseType,
                typeof(CodeDomSerializer));

    object codeObject =
        baseSerializer.Serialize(manager, value);

    try{
        CodeStatementCollection statements =
            (CodeStatementCollection)codeObject;
        IDesignerHost host =
            (IDesignerHost)manager.GetService(
            typeof(IDesignerHost));
        ComponentCollection components =
            host.Container.Components;

        SerializeExtender(manager,
            (IExtenderProvider)value,
            components, statements);
    }
    catch(Exception ex){
    }
    return codeObject;
}

Figure 5 - Implementation of the Serialize method.

The SerializeExtender method needs to make sure whether a specific property / component combination needs to have a code statement serialized. The method is displayed in Figure 6.

void SerializeExtender(
    IDesignerSerializationManager manager,
    IExtenderProvider provider,
    ComponentCollection components,
    CodeStatementCollection codeObject)
{
    ProvidePropertyAttribute[] properties =
        GetProvidedProperties(provider);
    foreach(IComponent component in components)
    {
        if(provider.CanExtend(component))
        {
            foreach(ProvidePropertyAttribute attribute
                in properties)
            {
                object currentValue =
                    ReflectionHelper.GetCurrentValue(
                    provider, attribute, component);
                bool hasDefault =
                    ReflectionHelper.HasDefaultValue(
                    provider, attribute);
                object defaultValue =
                    ReflectionHelper.GetDefaultValue(
                    provider, attribute);
                if( !hasDefault || Object.Equals(
                    defaultValue, currentValue) == false)
                {
                    CodeExpression exp =
                        CreateExpression(
                            manager, provider,
                            attribute, component,
                            currentValue);
                    codeObject.Add(exp);
                }
            }
        }
    }
}

Figure 6 - Implementation of the SerializeExtender method.

The first action taken in this method is running through each component and verifying if the provider can extend them. The provider contains a convenient method for this purpose called �CanExtend�. When it is possible to extend the component, all the provided properties are serialized depending on the default value of the property. Comparing the default and actual value uses the Object.Equals method. Instead of Object.Equals, it is possible to use the == operator. However, this results in the wrong comparison being made. The == operator returns true when the left and right hand side point to the same object instance , not when they hold the same value. An integer property for instance; when comparing two boxed integers, the == operator will yield false, but Object.Equals yields true when the integers are the same value.

When it has been determined that the property has no default value, or the actual value differs from the default one, the property / component combination needs to be serialized into a CodeStatement. This is when the final part of the solution comes in to play. Behold the CreateExpression method.

CodeExpression CreateExpression(
    IDesignerSerializationManager manager,
    IExtenderProvider provider,
    ProvidePropertyAttribute attribute,
    IComponent component,
    object currentValue)
{
    CodeExpression targetObject =
        base.SerializeToReferenceExpression(
        manager, provider);

    CodeMethodInvokeExpression methodCall =
        new CodeMethodInvokeExpression(targetObject,
        "Set" + attribute.PropertyName);

    methodCall.Parameters.Add(
        CreateReferencingExpression (
            manager, component));

    methodCall.Parameters.Add(
        CreateReferencingExpression (
            manager, currentValue));
    return methodCall;
}

Figure 7 - Implementation of the CreateExpression method.

Since it takes a method call to set a property on an IExtenderProvider, the CodeMethodInvokeExpression has to be used. This expression requires a CodeDom reference to the object on which to call the method as well as the name of the method to call.

The reference to the target object can be obtained using the CodeDomSerializer base class, which provides convenient methods for this purpose. The IExtenderProvider will have to be a Component in order to be dropped on the designer; a reference expression is therefore in order.

Referencing expressions result in a bit of code starting with the �this� pointer, e.g., this.myComponent. This type of expression can be applied to reference types. Value types such as a struct or enum take a different method of serializing. The Color.Black or BorderStyle.3D value can not be referenced using the �this� pointer. An exception to this rule is the String class. The String is a reference type, but should be serialized in the same way as primitives such as the Integer or Character. The actual value of the string needs to be serialized and not a reference to an instance of String.

The name of the method to call can be derived from the name of the property. The documentation of the IExtenderProvider states that the property name should be prefixed with the �Set� string in order to create the name of the method.

The final requirement for building a correct method call in CodeDom is populating the parameter list of the new expression. A convenient method has been created in order to create the right type of CodeDom expression for the type of object.

CodeExpression CreateReferencingExpression(
    IDesignerSerializationManager manager,
    object value)
{
    Type currentType = value.GetType();
    CodeExpression refExpression = null;
    if(currentType.IsValueType || value is String)
    {
        refExpression =
            base.SerializeToExpression(
                manager, value);
    }
    else
    {
        refExpression =
            base.SerializeToReferenceExpression(
                manager, value);
    }
    return refExpression;
}

Figure 8 - Implementation of the CreateReferencingExpression method.

Binding the serializer to an IExtenderProvider

All that it takes to use the new serializer is the DesignerSerializer attribute. Using this attribute, the serializer used for the component can be specified. The final code example displayed in Figure 9 shows how the attribute should be applied.

[ProvideProperty("ToolTip", typeof(Button)),
DesignerSerializer(typeof(ASPExtenderSerializer), typeof(CodeDomSerializer))]
public class WorkingProvider :
    Component,
    IExtenderProvider
{
}

Figure 9 - An IExtenderProvider which uses the new serializer.

Conclusion

The IExtenderProvider is a useful interface for extending other controls on the designer surface. The code demonstrated in this article provides a solution to remove the shortcoming of the Visual Studio .NET IDE. Through the use of a custom CodeDomSerializer, a proper method of handling the limitations of the Visual Studio IDE has been found.

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