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 Component
s. 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.