65.9K
CodeProject 正在变化。 阅读更多。
Home

修复 Visual Studio ASP.NET 设计器中的 IExtenderProvider

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (20投票s)

2005年1月5日

10分钟阅读

viewsIcon

70187

downloadIcon

817

一个用于修复 Visual Studio ASP.NET 设计器中 IExtenderProvider 的自定义 CodeDomSerializer 的实现。

引言

Provider 控件是设计时扩展设计器表面上现有控件的一种方法,它基于 IExtenderProvider 接口。Visual Studio .NET 2003 也在其 Windows Forms 设计器中支持 IExtenderProvider 接口。然而,它在 ASP.NET 设计时环境中的 IExtenderProvider 使用方面支持不佳。

IExtenderProvider 上设置的属性会被序列化为代码语句,存储在代码隐藏文件中。图 1 显示了为一个虚构的 ASP.NET ToolTip provider 所需的代码类型。这段代码无法正确生成,导致生成的代码隐藏文件如图 2 所示。由于代码未生成,因此它将无法编译。因此,IExtenderProvider 提供的逻辑在运行时不可用。

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);
}


图 1 - 所需代码

图 2 - 实际代码

本文提出了一种方法来消除 Visual Studio 在其 ASP.NET 设计器中对 IExtenderProvider 的限制。要充分理解本文,需要对 IExtenderProvider 接口有一定的了解。

IExtenderProvider 在 Visual Studio ASP.NET 设计器中不受支持的原因可能源于其反馈中心的一份报告。您可以在 Microsoft 上找到这份报告。

设计器背后的工作原理

为了解决 IExtenderProvider 的问题,首先需要基本了解 Visual Studio 设计器背后发生的事情。Visual Studio 提供了两种编辑 PageUserControl 的基本方法:一种是直接查看 HTML 和代码隐藏文件,另一种是可视化设计器。设计器在 HTML 文件中的服务器控件被编译后显示。这意味着设计器包含一个对应于代码隐藏文件中声明项的对象图。当设计器表面上某个组件的属性发生更改时,HTML 文件会被更新以反映该更改,或者相应的对象会被更新。当您从设计器切换到代码视图时,设计器中的对象会被序列化为代码语句,这些语句被放置在“Web Form Designer Generated Code”部分。如果您在代码文件中进行更改并切换回设计器,代码会被反序列化为对象图(有时,您需要刷新设计器才能看到代码隐藏文件中所做的更改)。在切换设计器和代码视图时,会发生一个称为序列化的过程。CodeDom 在此序列化过程中充当一个中间层。CodeDom 提供了与常见代码元素对应的各种类型,并提供了执行 CodeDom 操作的类。下图显示了 CodeDom 在切换设计器和代码视图过程中是如何使用的。

图 3 - 在设计视图和代码视图之间切换时的后台进程。

检查问题

现在我们清楚了 Visual Studio 设计器和它所设计源代码之间发生了什么,现在就可以确定 IExtenderProvider 的问题可能是什么了。弄清楚 IExtenderProvider 的哪个部分功能不正常会很有趣。图 3 显示了两个不同的过程需要检查,每个过程都包含两个步骤。第一个过程,从代码视图切换到设计器,包括解析代码并将解析后的语句反序列化为对象。第二个过程,切换回代码视图,包括将对象序列化为 CodeDom 并从这些 CodeDom 语句生成代码。

本文附带的代码包含一个损坏的 ToolTip provider,可用于检查问题。该 provider 能够扩展 Button 类,并应放置在包含 ButtonPageUserControl 上进行测试。

从代码视图切换到设计器

第一个要检查的问题是从代码视图切换到设计器。ButtonToolTip 应该在 Page 上,并且 Button 不应该设置 ToolTip。

使用代码视图,在设计器部分插入一行代码,调用 Button 的“SetToolTip”方法。当从代码视图切换到设计视图时,可以看到在 Button 的属性中设置的文本。这意味着,反序列化过程工作正常,不需要修复。

如果您不确定此方法是否有效,请尝试在代码隐藏文件中更改 Button 的背景颜色。切换到设计器并刷新页面以证明此方法的正确性。

从设计器切换到代码视图

第二个测试很容易,只需在 ButtonToolTip 属性上设置文本并切换到代码视图,就可以观察到 ToolTip 的代码没有生成。这部分需要通过修复来解决。请注意,序列化过程有两个部分:使用 CodeDomSerializer 构建 CodeStatementCollection,以及使用 CodeGenerator 从集合生成源代码。

IExtenderProvider 需要一个方法调用来设置其中一个提供的属性。这使得 CodeGenerator 造成问题的可能性不大。由于 CodeGenerator 与语言相关联,它将始终能够写入与 CodeStatement 对应的​​方法调用。

IExtenderProvider 需要一个方法调用来设置其中一个提供的属性。这使得 CodeGenerator 造成问题的可能性不大。由于 CodeGenerator 与语言相关联,它将始终能够写入与某个 CodeStatement 对应的​​方法调用。

扩展 Visual Studio 设计器

使用自定义 CodeDomSerializer 扩展 Visual Studio 的设计时环境时,需要处理两件事:构建序列化器并将它附加到 IExtenderProvider 组件。我们来逐一了解。

通用的 CodeDomSerializer

将支持 Visual Studio 的 CodeDomSerializer 必须足够通用,可以应用于所有 IExtenderProvider 组件。这意味着,它不知道属性的名称和类型。这些信息必须通过反射来获取。

需要一个 CodeDomSerializer 的子类来实现两个方法;SerializeDeserializeSerialize 方法接收一个对象,并将其序列化为 CodeStatementCollectionDeserialize 方法则相反,它从集合中的语句创建对象。由于 Serialize 方法已损坏,我们先来处理 Deserialize 方法。

反序列化 IExtenderProvider

此方法不需要大量工作,因为原始的 Deserialize 方法并未损坏。基类将 Deserialize 方法声明为 abstract,因此必须实现它。

由于 IExtenderProvider 组件是在设计时放置在 PageUserControl 上的,因此 provider 必须是 Component 的子类。有一个派生的 CodeDomSerializer 专门用于序列化 Component。使用传递给 Deserialize 方法的对象,可以获得 Component 类的 CodeDomSerializer 的引用。考虑到构建自定义 CodeDomSerializer 并非经常需要,Component 类的 CodeDomSerializer 在百分之九十八的情况下就足够了。图 4 显示了调用 Component 序列化器所需的代码。

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

    return baseSerializer.Deserialize(
        manager, codeDomObject);
}

图 4 - Deserialize 方法的实现。

序列化 IExtenderProvider

序列化过程将比反序列化需要更多的步骤。由于 IExtenderProvider 可以为设计器表面的所有组件提供属性,因此需要遍历每个组件。让我们开始创建所需步骤的第一部分,即重写的 Serialize 方法。图 5 显示了这个方法。

由于此序列化器仅应用于 IExtenderProvider 组件,因此会进行验证以确保序列化器应用正确。接下来,使用基类的 CodeDomSerializer 来序列化 IExtenderProvider 可能包含的所有常规属性。这样就只剩下扩展属性需要定制化序列化过程了。通过服务模型可以获得设计器表面上组件集合的引用;IDesignerHost 服务包含这些组件的引用。

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;
}

图 5 - Serialize 方法的实现。

SerializeExtender 方法需要检查特定的属性/组件组合是否需要序列化代码语句。该方法显示在图 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);
                }
            }
        }
    }
}

图 6 - SerializeExtender 方法的实现。

此方法采取的第一个操作是遍历每个组件,并验证 provider 是否可以扩展它们。Provider 包含一个方便的方法用于此目的,称为“CanExtend”。当可以扩展组件时,将根据属性的默认值来序列化所有提供的属性。比较默认值和实际值使用 Object.Equals 方法。使用 Object.Equals 而不是 == 运算符。然而,这会导致错误的比较。 == 运算符在左右两边指向同一个对象实例时返回 true,而不是当它们持有相同值时。例如,一个整数属性;当比较两个装箱的整数时,== 运算符将返回 false,但 Object.Equals 在整数值相同时返回 true

当确定属性没有默认值,或者实际值与默认值不同时,就需要将属性/组件组合序列化为 CodeStatement。这时解决方案的最后一部分就派上用场了。请看 CreateExpression 方法。

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;
}

图 7 - CreateExpression 方法的实现。

由于设置 IExtenderProvider 的属性需要方法调用,因此必须使用 CodeMethodInvokeExpression。此表达式需要 CodeDom 对要调用方法的对象以及要调用的方法名的引用。

目标对象的引用可以通过 CodeDomSerializer 基类获得,该基类提供了方便的方法来实现此目的。IExtenderProvider 必须是 Component 才能拖放到设计器上;因此需要一个引用表达式。

引用表达式会生成一些以“this”指针开头的代码,例如 this.myComponent。这种类型的表达式可用于引用类型。值类型,如 structenum,则需要不同的序列化方法。无法使用“this”指针来引用 Color.BlackBorderStyle.3D 值。String 类是该规则的一个例外。String 是一个引用类型,但应与原始类型(如 IntegerCharacter)进行相同的序列化。应该序列化字符串的实际值,而不是 String 实例的引用。

要调用的方法的名称可以从属性名称派生。IExtenderProvider 的文档指出,属性名称应以“Set”字符串作为前缀,以创建方法名称。

构建正确的 CodeDom 方法调用的最终要求是填充新表达式的参数列表。已经创建了一个方便的方法来为对象类型创建正确的 CodeDom 表达式类型。

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;
}

图 8 - CreateReferencingExpression 方法的实现。

将序列化器绑定到 IExtenderProvider

使用新序列化器所需的一切就是 DesignerSerializer 属性。使用此属性,可以指定要为组件使用的序列化器。图 9 中显示的最终代码示例展示了如何应用该属性。

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

图 9 - 使用新序列化器的 IExtenderProvider

结论

IExtenderProvider 是一个有用的接口,用于扩展设计器表面的其他控件。本文演示的代码提供了一个解决方案,可以消除 Visual Studio .NET IDE 的不足。通过使用自定义 CodeDomSerializer,找到了处理 Visual Studio IDE 限制的正确方法。

© . All rights reserved.