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






4.67/5 (20投票s)
2005年1月5日
10分钟阅读

70187

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 提供了两种编辑 Page
或 UserControl
的基本方法:一种是直接查看 HTML 和代码隐藏文件,另一种是可视化设计器。设计器在 HTML 文件中的服务器控件被编译后显示。这意味着设计器包含一个对应于代码隐藏文件中声明项的对象图。当设计器表面上某个组件的属性发生更改时,HTML 文件会被更新以反映该更改,或者相应的对象会被更新。当您从设计器切换到代码视图时,设计器中的对象会被序列化为代码语句,这些语句被放置在“Web Form Designer Generated Code”部分。如果您在代码文件中进行更改并切换回设计器,代码会被反序列化为对象图(有时,您需要刷新设计器才能看到代码隐藏文件中所做的更改)。在切换设计器和代码视图时,会发生一个称为序列化的过程。CodeDom 在此序列化过程中充当一个中间层。CodeDom 提供了与常见代码元素对应的各种类型,并提供了执行 CodeDom 操作的类。下图显示了 CodeDom 在切换设计器和代码视图过程中是如何使用的。
图 3 - 在设计视图和代码视图之间切换时的后台进程。
检查问题
现在我们清楚了 Visual Studio 设计器和它所设计源代码之间发生了什么,现在就可以确定 IExtenderProvider
的问题可能是什么了。弄清楚 IExtenderProvider
的哪个部分功能不正常会很有趣。图 3 显示了两个不同的过程需要检查,每个过程都包含两个步骤。第一个过程,从代码视图切换到设计器,包括解析代码并将解析后的语句反序列化为对象。第二个过程,切换回代码视图,包括将对象序列化为 CodeDom 并从这些 CodeDom 语句生成代码。
本文附带的代码包含一个损坏的 ToolTip provider,可用于检查问题。该 provider 能够扩展 Button 类,并应放置在包含 Button
的 Page
或 UserControl
上进行测试。
从代码视图切换到设计器
第一个要检查的问题是从代码视图切换到设计器。Button
和 ToolTip
应该在 Page
上,并且 Button
不应该设置 ToolTip。
使用代码视图,在设计器部分插入一行代码,调用 Button
的“SetToolTip
”方法。当从代码视图切换到设计视图时,可以看到在 Button
的属性中设置的文本。这意味着,反序列化过程工作正常,不需要修复。
如果您不确定此方法是否有效,请尝试在代码隐藏文件中更改 Button
的背景颜色。切换到设计器并刷新页面以证明此方法的正确性。
从设计器切换到代码视图
第二个测试很容易,只需在 Button
的 ToolTip
属性上设置文本并切换到代码视图,就可以观察到 ToolTip 的代码没有生成。这部分需要通过修复来解决。请注意,序列化过程有两个部分:使用 CodeDomSerializer
构建 CodeStatementCollection
,以及使用 CodeGenerator
从集合生成源代码。
IExtenderProvider
需要一个方法调用来设置其中一个提供的属性。这使得 CodeGenerator
造成问题的可能性不大。由于 CodeGenerator
与语言相关联,它将始终能够写入与 CodeStatement
对应的方法调用。
IExtenderProvider
需要一个方法调用来设置其中一个提供的属性。这使得 CodeGenerator
造成问题的可能性不大。由于 CodeGenerator
与语言相关联,它将始终能够写入与某个 CodeStatement
对应的方法调用。
扩展 Visual Studio 设计器
使用自定义 CodeDomSerializer
扩展 Visual Studio 的设计时环境时,需要处理两件事:构建序列化器并将它附加到 IExtenderProvider
组件。我们来逐一了解。
通用的 CodeDomSerializer
将支持 Visual Studio 的 CodeDomSerializer
必须足够通用,可以应用于所有 IExtenderProvider
组件。这意味着,它不知道属性的名称和类型。这些信息必须通过反射来获取。
需要一个 CodeDomSerializer
的子类来实现两个方法;Serialize
和 Deserialize
。Serialize
方法接收一个对象,并将其序列化为 CodeStatementCollection
。Deserialize
方法则相反,它从集合中的语句创建对象。由于 Serialize
方法已损坏,我们先来处理 Deserialize
方法。
反序列化 IExtenderProvider
此方法不需要大量工作,因为原始的 Deserialize
方法并未损坏。基类将 Deserialize
方法声明为 abstract
,因此必须实现它。
由于 IExtenderProvider
组件是在设计时放置在 Page
或 UserControl
上的,因此 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
。这种类型的表达式可用于引用类型。值类型,如 struct
或 enum
,则需要不同的序列化方法。无法使用“this
”指针来引用 Color.Black
或 BorderStyle.3D
值。String
类是该规则的一个例外。String
是一个引用类型,但应与原始类型(如 Integer
或 Character
)进行相同的序列化。应该序列化字符串的实际值,而不是 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 限制的正确方法。