Silverlight 5 和 WPF 的静态和类型标记扩展





5.00/5 (8投票s)
一个为 Silverlight 5 和 WPF 实现的扩展静态标记扩展,支持带参数调用静态方法,以及一个 Silverlight 类型标记扩展实现。
引言
本文介绍了一个 Silverlight 5 的自定义 `Static` 和 `Type` 标记扩展实现,这些扩展在 WPF 中已经可用。`Static` 标记扩展除了访问属性和字段之外,还扩展了对调用带参数的静态方法的支持。它还会进行目标属性类型的转换。
背景
与 WPF 相比,Silverlight 标准标记扩展的数量有限。这意味着像 `MultiBinding`、`x:Static` 和 `x:Type` 这样的标准 WPF 标记扩展在 Silverlight 中不可用。然而,从 5.0 版本开始,Silverlight 支持自定义标记扩展,这使得创建功能相同的扩展成为可能。在上一篇文章中,我展示了如何创建一个扩展的 Silverlight `MultiBinding`,在本文中,我将介绍 Silverlight 的 `Type` 和 `Static` 标记扩展实现。与我的 `MultiBinding` 实现一样,`Static` 标记扩展也比原始 WPF 版本具有更丰富的功能。这个 `Static` 实现除了静态属性和字段之外,还支持带参数调用静态方法。
使用代码
下面的所有代码示例都假定您已将 `SilverlightMarkupExtension` 程序集引用添加到项目中,并在 XAML 文件中为 `SilverlightMarkupExtensions` 命名空间分配了 `z` 命名空间前缀。
静态标记扩展
在 XAML 中赋值属性时,可以使用 `Static` 标记扩展来引用静态成员。WPF 的 `x:Static` 扩展支持公共静态属性和字段。一个典型的用法是引用类型化资源文件中的资源键以本地化内容
<TextBlock Text={x:Static demo:Resources.Title} />
其中 `demo:Resources` 是对类型化资源文件的引用,`Title` 是资源属性的名称。Silverlight 的 `Static` 实现几乎以相同的方式支持这一点
<TextBlock Text={z:Static Member=demo:Resources.Title} />
由于 Silverlight 不支持位置参数,我们必须在声明中指定 `Member` 属性。与原始 `x:Static` 扩展一样,您也可以像这样分离类型和成员
<TextBlock Text={z:Static MemberType=demo:Resources, Member=Title} />
如果您想反复引用同一类型中的成员(例如资源),此 `Static` 标记扩展还支持在根 XAML 对象(`UserControl`)上一次性指定成员类型,方法是使用 `Static.DefaultMemberType` 附加属性。
<UserControl … z:Static.DefaultMemberType=”demo:Resources” >
…
<TextBlock Text={z:Static Member=Title />
调用静态方法
除了支持访问静态属性和字段(类似于 WPF 的 `x:Static` 扩展)之外,Silverlight 的 `Static` 标记扩展还可以像这样简单地调用静态方法
<TextBlock Text="{z:Static MemberType=demo:MyModelView, Member='GetSum(7,6.3)'}" />
在此示例中,我们显示了调用接受两个参数的公共静态方法 `MyModelView.GetSum` 的结果。在成员名称后使用逗号分隔的参数列表即可调用静态方法。支持使用 `IConvertible` 接口对标准类型进行基本类型转换。要调用不带参数的方法,只需指定一对空括号。
`Static` 扩展还支持将参数指定为单独的属性,如下所示
<ContentControl Content="{z:Static Member=demo:MyModelView.GetSum, Arg1={z:Static Member='demo:MyModelView.GetSum(6,7)'},Arg2=8}" />
如上所示,我们可以使用此方法将标记扩展的结果作为参数传递。此处显示的结果将是 21 (6+7+8)。目前最多支持三个参数,并且可以轻松地增加对更多参数的支持。
类型标记扩展
在 WPF 中,`x:Type` 标记扩展可用于在 XAML 中为属性赋值时引用命名类型,但在 Silverlight 标准框架中不包含此标记扩展。另一方面,Silverlight 5 在为 `System.Type` 类型属性赋值时支持命名类型,因此在大多数情况下不需要 `Type` 标记扩展。仅当要设置为类型的属性不是 `System.Type` 类型,而是其父类型 `System.Object` 类型时才需要。这里的 Silverlight 实现与 WPF 的对应项 `Type` 和 `TypeName` 具有相同的属性——同一声明中只能使用其中一个。前者是 `System.Type` 类型,可以分配一个命名类型,前面可选地带有命名空间前缀。这是指定类型的首选方式,因为它在设计时进行了检查。相比之下,`TypeName` 是一个 `String` 属性,仅在运行时进行评估,并且必须设置为带有适当命名空间前缀的类型名称。`TypeName` 在某些罕见情况下可能很有用,例如当类型名称指定为 XAML 资源并通过 `StaticResource` 标记扩展引用时,或者其他标记扩展提供了类型名称,例如 `Static`。
在下面的示例中,我们使用 `Type` 标记扩展将类型引用作为参数传递给一个方法,该方法直接在 XAML 中使用 `Static` 标记扩展调用
<ComboBox ItemsSource="{z:Static Member=demo:Sex.GetValues, Arg1={z:Type Type=demo:Sex}}"
SelectedItem="{z:Static Member=demo:Sex.Unknown}" />
`Sex` 在这里是一个枚举类型。我们调用 `Enum.GetValues` 静态方法,并传递一个参数来指定返回值的类型。结果将是一个包含枚举值的组合框。我们还使用 `Static` 在定义初始选定项时引用一个特定的枚举值 `Unknown`。
在 WPF 中使用
`Static` 和 `Type` 标记扩展都可以在 WPF XAML 代码中使用,这使得可以直接将为 Silverlight 编写的 XAML 迁移到 WPF。新的 `Static` 实现还可以独立用于 WPF XAML,因为它具有调用方法的扩展能力。与内置的 `x:Static` 相比的另一个优势是 Silverlight 的实现尝试在返回结果之前将其转换为属性类型。这在您有一个定义为 `String` 类型的资源文件项但想将其应用于数字属性的情况下非常有用。在下面的示例中,我们有一个资源 `TitleWidth`,它必须是 `String` 类型,被赋值为 `"100"
`,并尝试将其用于数字属性 `Width`
<ContentPresenter Content="{x:Static demo:Resources.Title}" Width="{x:Static demo:Resources.TitleWidth}" />
<ContentPresenter Content="{z:Static Member=demo:Resources.Title}" Width="{z:Static Member=demo:Resources.TitleWidth}" />
使用内置的 `x:Static` 标记扩展(如第一行所示)将导致运行时错误,消息为“Set property 'System.Windows.FrameworkElement.Width' threw an exception. '100' is not a valid value for property 'Width'”。只需将其替换为我的 `z:Static` 标记扩展即可解决此问题,因为它支持自动类型转换。
有限的设计时支持
不幸的是,在使用 `Static` 和 `Type` 标记扩展时,结果不会直接显示在 Visual Studio Silverlight 设计器中,因为自定义标记扩展的 `ProvideValue` 在 Silverlight 设计模式下不会被调用。然而,这与 WPF 设计器中调用 `ProvideValue` 的行为不一致。无论如何,`Static` 和 `Type` 扩展使用的服务在 WPF 和 Silverlight 设计模式下都不可用,这使得在大多数情况下无法在设计时解析值。
有些人利用了(奇怪的?)事实,即自定义标记扩展的 `ToString()` 在 Silverlight 设计模式下会被调用,以提供设计时值。然而,这似乎仅在 `ContentPresenter` 上下文中才成立,因此不适用于所有属性。
实现细节
两个标记扩展都使用通过传递给 `ProvideValue` 方法的服务提供者提供的标准服务。为了解析类型名称(可能带有命名空间前缀),使用了 `IXamlTypeResolver` 服务。下面显示了 `Type` 的 `ProvideValue` 方法的完整实现
public override Object ProvideValue(IServiceProvider serviceProvider)
{
if (Type == null)
{
// Excluded some parameter checking
IXamlTypeResolver resolver =
serviceProvider.GetService(typeof(IXamlTypeResolver)) as IXamlTypeResolver;
if (resolver == null) DependencyProperty.UnsetValue;
Type = resolver.Resolve(TypeName);
}
return Type;
}
`Static` 标记扩展还使用 `IXamlTypeResolver` 服务来解析 `Member` 属性中指定的类型名称。此外,它还使用 `IProvideValueTarget` 服务来确定目标属性类型。通过这种方式,`Static` 尝试将从调用的静态方法返回的值转换为正确的类型
// Try to convert return value to target property type.
if (serviceProvider != null)
{
IProvideValueTarget pvt =
serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (pvt != null)
{
PropertyInfo targetProperty = pvt.TargetProperty as PropertyInfo;
if (targetProperty != null)
{
returnValue = ConvertToType(targetProperty.PropertyType, returnValue,
Thread.CurrentThread.CurrentUICulture);
}
#if !SILVERLIGHT
else // In WPF a DependencyProperty is used for dependency properties.
{
DependencyProperty propertyMetaData = pvt.TargetProperty as DependencyProperty;
if (propertyMetaData != null)
{
returnValue = ConvertToType(propertyMetaData.PropertyType,
returnValue, Thread.CurrentThread.CurrentUICulture);
}
}
#endif
}
}
最后,使用 `IRootObjectProvider` 服务来查找设置在 XAML 根对象(通常是 `UserControl`)上的 `DefaultMemberType` 附加属性
// Look for Static.DefaultMemberType set on root object.
if (serviceProvider != null)
{
IRootObjectProvider rop =
serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider;
if (rop != null)
{
DependencyObject rootObject = rop.RootObject as DependencyObject;
if (rootObject != null)
{
memberType = GetDefaultMemberType(rootObject);
}
}
}
结论
这项工作展示了如何使用类似于 WPF 中基本标记扩展的标记扩展来扩展 Silverlight 5,以及如何使用可用的各种服务。结合之前介绍的 `MultiBinding` 实现,我们开始构建一个包含有用 Silverlight 标记扩展的小型实用库。调用静态方法可能很有用,但我们真正想要的是能够调用实例方法。因此,我开始基于 `MultiBinding` 创建一个 `MethodBinding` 标记扩展。这将是我下一篇文章的主题,但如果您好奇,可以提前在此文章提供的源代码中预览。
历史
- 2012 年 2 月 19 日 – 使用 Visual Studio 2010 SP1 和 Silverlight 5 开发并测试的初始版本。