使用 T4 模板生成 WPF 和 Silverlight 依赖项属性






4.95/5 (39投票s)
本文介绍如何使用简单的T4模板从XML声明生成依赖属性。文章还包含一个简短的T4入门指南。
目录
概述
本文介绍了一种代码生成技术,用于生成定义了依赖属性的WPF/Silverlight类。代码生成是通过一个相对简单的T4模板完成的,该模板读取对类及其相关依赖属性的XML描述。T4模板是Visual Studio内置的代码生成器;因此,此技术不需要任何第三方框架或库;您可以直接复制代码,创建XML文件,即可为您生成类!
本文的目的是双重的;首先,它对T4模板进行了非常简要的介绍,这似乎是一个“秘密”;其次,它为冗长且易错的依赖属性声明问题提供了一个实际的解决方案。
引言
我个人认为,Silverlight和WPF开发中最令人沮丧的方面之一就是与依赖属性框架打交道。虽然框架本身在概念上很出色,但依赖属性的实现却非常丑陋!
以以下依赖属性声明为例
public double Maximum
{
get { return (double)GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(double),
typeof(RangeControl), new PropertyMetadata(0.0, OnMaximumPropertyChanged));
private static void OnMaximumPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
RangeControl myClass = d as RangeControl;
myClass.OnMaximumPropertyChanged(e);
}
private void OnMaximumPropertyChanged(DependencyPropertyChangedEventArgs e)
{
// do something
}
上面的代码定义了一个依赖属性(DP)Maximum
,提供了类型和默认值、一个CLR属性包装器以及一个在属性更改时调用的方法。我们有21行代码,代码量很大,但作用却微乎其微!此外,依赖属性声明可能非常容易出错;如果指定不正确,由此产生的错误可能会被轻易地忽略和未报告,浪费宝贵的时间...
一些人创建了代码片段来帮助自动化这些样板代码的构建。在Silverlight Contrib项目中有一些可用于Silverlight的代码片段,并且Dr. WPF提供了大量可用于WPF的代码片段。代码片段在一定程度上有所帮助,它们提供了一种更快捷的方式来向类中添加新的DP;但是,如果您需要修改DP声明,例如添加更改处理程序或将其移至类层次结构中,您将需要使用更手动的方法。
在我描述我的方法之前,我想简要解释一下为什么依赖属性(DP)必须如此。一个明显的初学者问题是,为什么CLR属性不能像DP一样工作?区别在于CLR属性被编译器理解,它们是语言的一部分,而DP纯粹是框架的一部分,而不是语言的一部分。编译器不理解DP,因此没有提供快捷方式。
那么,如果我们不能改变语言本身,我们能做什么呢?我曾考虑过应用面向切面编程,增强已编译的IL,以便为带有属性标记的CLR添加DP;然而,无论我如何看待这个问题,我都找不到一个合适的解决方案。这让我转向了代码生成,基本思想是用一些简洁的描述来生成我的DP。这项研究让我发现了Visual Studio最鲜为人知的秘密之一:T4模板(T4 = Text Template Transformation Toolkit!)。
T4快速入门
Hello World
T4模板在Visual Studio 2008中可用;但是,当您选择“添加=>新建项”时,它不会列出。要添加新模板,只需创建一个新的空文本文件,然后将其扩展名更改为“tt”。然后您将看到类似以下的内容
您的模板文件显示在解决方案资源管理器中,以及它生成的文件。如果您检查模板文件的属性,您会发现它有一个“自定义工具”与之关联,即TextTemplatingFileGenerator
。这个类负责调用代码生成引擎,该引擎转换T4文件并生成相关文件。此外,如果您右键单击T4文件,您会注意到一个新的菜单选项:“运行自定义工具”,允许您按需执行您的模板。
模板本身在保存更改时执行,并在Visual Studio编译您的项目之前执行。因此,在上面的示例中,HelloWorld.cs,我们的生成文件,就像任何其他“cs”文件一样被编译。但是,Visual Studio对您的模板与其他文件或模板之间的依赖关系没有了解。因此,如果您的模板依赖于某些外部文件(如XML文件),当此文件更改时,您将必须选择“运行自定义工具”来运行T4模板的模板引擎并更新输出文件。
这是一个非常简单的例子。以下模板(具有几乎ASP.NET风格的语法)创建一个.cs文件,而模板语言本身是C#。
<#@ output extension="cs" #>
<#@ template language="C#" #>
public class HelloWorld
{
public void DoSomething()
{
<#
for(int i=0; i<5; i++)
{
#>
this.Write("Hello World #<#= i #>");
<#
}
#>
}
}
注意:CodeProject语法高亮器无法识别T4模板代码,因此它对模板代码进行了一些略显奇怪和随机的高亮显示!
这是输出
public class HelloWorld
{
public void DoSomething()
{
this.Write("Hello World #0");
this.Write("Hello World #1");
this.Write("Hello World #2");
this.Write("Hello World #3");
this.Write("Hello World #4");
}
}
注意:上述代码在Silverlight项目中将无法正常工作;有关原因及解决方法,请参阅“其他问题”部分。
模板结构
模板由四个主要部分组成
- 语句块 - 包含在以下格式中:
<# StatementCode #>
。此代码由模板引擎执行,是您表达模板逻辑的地方。 - 表达式块 - 包含在以下格式中:
<#= ExpressionBlock #>
。求值块由模板引擎执行,其结果将添加到生成的文件中。 - 指令 - 包含在以下格式中:
<#@ Directive #>
。这些为模板引擎提供信息,例如输出文件扩展名、语句块中使用的语言、任何引用的程序集以及导入的命名空间。 - 文本块 - 这是未包含在上述其他块中的纯文本。此文本将直接复制到输出中。
T4模板非常像脚本,它们不包含类或其他面向对象概念。唯一可用的代码重用机制是类特征块,其语法如下:<#+ FeatureBlock #>
,它们本质上是辅助函数。以下是一个简单的例子
<#@ template language="C#"#>
<# HelloWorld(); #>
<#+
private void HelloWorld()
{
this.Write("Hello World");
}
#>
这里定义了一个包含单个辅助函数HelloWorld
的类特征块(当然,您也可以在一个块中定义多个辅助函数),并且它只调用一次。
实用工具
Visual Studio的Intellisense无法识别T4文件;此外,也没有任何形式的上下文高亮显示。但是,使用免费的Clarius Visual T4 Editor Community Edition可以实现上下文高亮显示。您也可以购买专业版,其中包括Intellisense。如果您只实现简单的模板,Intellisense的缺失并不是一个主要障碍;但是,如果我要实现更雄心勃勃的内容,我肯定会考虑购买专业版。
如果您正在考虑进行严肃的T4开发,我还会推荐查看T4 Toolbox。这个开源CodePlex项目为基本的Visual Studio T4支持增加了许多增强功能,包括“添加=>新建项”对话框中的模板选项、OO模板、单元测试框架、一些现成模板以及更多其他功能。
我特意没有使用T4 Toolbox来生成我的依赖属性代码。这并不是因为我讨厌T4 Toolbox,它确实简化了T4模板的编码;我不使用它的原因是,我希望任何人都能使用我简单的WPF/Silverlight模板,而无需下载任何其他库或框架。
互联网上关于T4信息最好的资源之一是Oleg Sych的博客。他在使用T4模板方面发布了大量的迷你文章,内容从简单的入门和教程到高级概念。
在Silverlight项目中使用T4模板及其他问题...
Visual Studio使用您的项目引用来在您的项目内运行T4模板。对于大多数.NET项目类型,这都能很好地工作;但是,Silverlight的.NET程序集不包含T4模板执行所需的所有类。
为了在Silverlight项目中使用T4模板,您需要使用assembly
指令,该指令将程序集引用添加到您的模板中,仅用于执行T4模板,如下所示
<#@ assembly name="C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.dll" #>
<#@ assembly name="C:\Program Files\Reference Assemblies\Microsoft\
Framework\v3.5\System.Core.dll" #>
<#@ assembly name="C:\Program Files\Reference Assemblies\
Microsoft\Framework\v3.5\System.Xml.Linq.dll" #>
<#@ assembly name="C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Xml.dll" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml.Linq" #>
System.dll程序集是简单T4模板所需的唯一程序集;但是,在我后面将在本文中使用的LINQ to XML时,我发现其他三个是必需的。另外请注意import
指令,它与C#文件中的using
关键字的作用相同。
另一个需要注意的地方是,如果您想从T4模板加载文件,您必须牢记工作目录。如果您只想引用另一个模板文件,可以使用include
指令
<#@ include file="DependencyObjectTemplate.tt" #>
但是,如果您想加载XML文件,例如,您必须引用文件的完整路径。这是因为您的T4模板的工作目录是Visual Studio的工作目录,而不是您当前项目的位置。
依赖属性代码生成
生成部分类
T4模板显然是生成DP代码的一个好选择;但是,我们如何将这些属性添加到我们想要添加其他逻辑(如方法、字段和属性)的类中呢?答案是简单地使用Visual Studio本身用来将手动编写的代码与设计器生成的代码结合起来的机制——部分类。
如果我们能在T4模板中创建DP的合适表示,那么为类生成所有DP的过程将非常简单,如下所示
public partial class <#= className #>
{
<#
foreach(var dp in dps)
{
string propertyName = dp.Name;
string propertyType = dp.Type;
string defaultValue = dp.DefaultValue;
#>
#region <#= propertyName #>
public <#= propertyType #> <#= propertyName #>
{
get { return (<#= propertyType #>)GetValue(<#= propertyName #>Property); }
set { SetValue(<#= propertyName #>Property, value); }
}
public static readonly DependencyProperty <#= propertyName #>Property =
DependencyProperty.Register("<#= propertyName #>", typeof(<#= propertyType #>),
typeof(<#= className #>), new PropertyMetadata(<#= defaultValue #>));
#endregion
<#
} // end foreach dps
#>
}
上面的代码注册了一个带有相关CLR包装器的DP,并给出了一个默认值。我将在下一节中考虑如何添加更改通知。这就留下了如何以简洁简单的方式指定DP的问题。我决定最简单的方法是将每个类在XML文件中定义DP,模板可以加载XML文件,使用LINQ对其进行查询,然后生成所需的局部类。
用于DP规范的XML架构
出于多种原因,我通常喜欢为我的XML文件定义XML架构。首先,拥有一个架构来验证您的XML实例文档意味着您的解析器不需要那么谨慎。其次,对于Visual Studio XML编辑器,当一个架构与XML实例文档关联时,您可以通过Intellisense获得元素和属性名称的自动完成;这意味着您不必过多担心使用冗长且描述性的元素/属性名称。
这是我们DP规范的简单XML架构
<?xml version="1.0" encoding="utf-8"?>
<xs:schema
targetNamespace="http://www.scottlogic.co.uk/DependencyObject"
elementFormDefault="qualified"
xmlns="http://www.scottlogic.co.uk/DependencyObject"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="dependencyObjects" type="dependencyObjectsType"/>
<xs:complexType name="dependencyObjectsType">
<xs:sequence>
<xs:element name="dependencyObject"
type="dependencyObjectType"
maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="dependencyObjectType">
<xs:sequence>
<xs:element name="dependencyProperty"
type="dependencyPropertyType" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="type" type="xs:string" use="required"/>
<xs:attribute name="base" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="dependencyPropertyType">
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="type" type="xs:string" use="required"/>
<xs:attribute name="defaultValue" type="xs:string" use="required"/>
</xs:complexType>
</xs:schema>
下面是一个示例实例文档,它描述了一对类的DP:一个RangeControl
和一个AmountControl
<?xml version="1.0" encoding="utf-8" ?>
<dependencyObjects
xmlns="http://www.scottlogic.co.uk/DependencyObject"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dependencyObject name="SilverlightTemplates.RangeControl"
base="UserControl">
<dependencyProperty type="double" defaultValue="0.0"
name="Maximum"/>
<dependencyProperty type="double" defaultValue="0.0"
name="Minimum"/>
</dependencyObject>
<dependencyObject name="SilverlightTemplates.AmountControl"
base="UserControl">
<dependencyProperty type="double" defaultValue="0.0"
name="Amount"/>
</dependencyObject>
</dependencyObjects>
我们可以定义一个辅助方法(在类特征块中)来加载此XML文件,并为我们XML文件中的指定类生成部分类
<#@ output extension="cs" #>
<#@ template language="C#v3.5" #>
<#@ assembly name="C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.dll" #>
<#@ assembly name="C:\Program Files\Reference Assemblies\
Microsoft\Framework\v3.5\System.Core.dll" #>
<#@ assembly name="C:\Program Files\Reference Assemblies\
Microsoft\Framework\v3.5\System.Xml.Linq.dll" #>
<#@ assembly name="C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Xml.dll" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml.Linq" #>
using System;
using System.Windows;
using System.Windows.Controls;
using System.ComponentModel;
<#+
private void GenerateClass(string classFullName, string xmlFileLocation)
{
string classNamespace = classFullName.Substring(0, classFullName.LastIndexOf('.'));
string className = classFullName.Substring(classFullName.LastIndexOf('.') + 1);
XNamespace ns = "http://www.scottlogic.co.uk/DependencyObject";
XDocument xmlFile = XDocument.Load(xmlFileLocation);
var dps = from dp in xmlFile.Descendants(ns + "dependencyProperty")
where dp.Parent.Attribute("type").Value == classFullName
select dp;
var depObj = (from c in xmlFile.Descendants(ns + "dependencyObject")
where c.Attribute("type").Value == classFullName
select c).Single();
string baseType = depObj.Attribute("base").Value;
#>
namespace <#= classNamespace #>
{
public partial class <#= className #> : <#= baseType #>
{
<#+
foreach(var dp in dps)
{
string propertyName = dp.Attribute("name").Value;
string propertyType = dp.Attribute("type").Value;
string defaultValue = dp.Attribute("defaultValue").Value;
#>
#region <#= propertyName #>
<#+
GenerateCLRAccessor(propertyType, propertyName);
GenerateDependencyProperty(className, propertyType, defaultValue, propertyName);
#>
#endregion
<#+
} // end foreach dps
#>
}
}
<#+
}
private void GenerateCLRAccessor(string propertyType, string propertyName)
{
#>
public <#= propertyType #> <#= propertyName #>
{
get { return (<#= propertyType #>)GetValue(<#= propertyName #>Property); }
set { SetValue(<#= propertyName #>Property, value); }
}
<#+
}
private void GenerateDependencyProperty(...)
{
...
}
#>
GenerateClass
函数接受两个参数:要生成的类的完全限定名,以及XML文件的位置(已指定其完整路径)。它从文件创建X-DOM,然后使用LINQ查询来定位我们命名类的dependencyObject
XML元素以及它包含的dependencyProperty
元素。部分类在正确的命名空间内构建,然后迭代DP集合以输出每个依赖属性。
GenerateCLRAccessor
辅助函数构建DP的CLR包装器,而GenerateDependencyProperty
函数构建依赖属性本身。请注意,GenerateDependencyProperty
函数未显示,因为它基本上与之前的示例相同。
为了使用上述模板,我们创建一个非常简单的模板文件来引用它并调用GenerateClass
函数
<#@ include file="DependencyObject.tt" #>
<#
GenerateClasses("SilverlightTemplates.RangeControl",
@"C:\Projects\...\DependencyObjects.xml");
#>
属性变更通知
上面的示例演示了如何为类的给定类型生成依赖属性。然而,一个常见的需求是在依赖属性更改时添加回调。这作为依赖属性元数据的一部分进行指定;回顾我们最初的示例DP,它的用法如下
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(double),
typeof(RangeControl), new PropertyMetadata(0.0, OnMaximumPropertyChanged));
private static void OnMaximumPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
RangeControl myClass = d as RangeControl;
myClass.OnMaximumPropertyChanged(e);
}
private void OnMaximumPropertyChanged(DependencyPropertyChangedEventArgs e)
{
// do something
}
在上面的示例中,DP声明提供了一个名为OnMaximumPropertyChanged
的方法,该方法将在每次属性更改时被调用。该方法必须是静态的;因此,为了将此更改事件转发到我们类的正确实例,我们必须转换DependencyObject
参数,然后调用非静态的OnMaximumPropertyChanged
方法。
我们也可以通过T4模板消除这种“样板”代码。问题是,我们确实希望非静态的OnMaximumPropertyChanged
方法位于我们手动编写的RangeControl
类中,在其中实现我们的业务逻辑;然而,它是在我们生成的类中调用的。我们可以添加一个虚拟方法;但是,这需要我们实现一个子类才能覆盖它并添加行为。
幸运的是,.NET框架已经为此问题提供了解决方案,即部分方法。在定义部分类时,可以定义部分方法,这些方法可以从部分类内部调用。您可以在手动编写的类中定义这些部分方法的实现,从而允许您生成的类直接调用您手动编写的类的方法。真正巧妙之处在于,如果您不实现部分方法,调用该方法的代码在类编译时将完全消失!当然,这会对部分方法的签名施加一些限制;例如,出于显而易见的原因,它们必须返回void
。
实现INotifyPropertyChanged
在WPF中,绑定框架比其Silverlight对应物稍强大,允许您通过值转换器将DP绑定在一起,从而实现各种有趣的绑定。但是,Silverlight的功能稍弱;如果您想将两个DP绑定在一起,这在很大程度上是一个手动过程,尽管可以实现ElementName
和RelativeSource
绑定的近似效果。您还必须为每个DP实现INotifyPropertyChanged
并引发PropertyChanged
事件。
在附加源代码中并将在用户指南部分进一步描述的T4模板上,dependencyObject
元素有一个notifyPropertyChanged
属性,如果设置为true
,它将为该类生成INotifyPropertyChanged
实现,并在DP更改时为每个DP引发PropertyChanged
事件。同样,更多的样板代码被消除了!
完整的模板和XML架构
前面的部分描述了如何使用T4模板生成依赖属性。以下部分详细介绍了我的已完成的依赖对象生成模板,该模板增加了附加属性、注释和WPF支持等功能。
您可以直接复制代码并按照说明开始,或者下载示例项目,在实际的WPF或Silverlight项目中查看模板的使用情况。
依赖对象生成模板
注意:在WPF项目中使用时,可以删除程序集引用指令。
<#@ output extension="cs" #>
<#@ template language="C#v3.5" #>
<#@ assembly name="C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.dll" #>
<#@ assembly name="C:\Program Files\Reference Assemblies\
Microsoft\Framework\v3.5\System.Core.dll" #>
<#@ assembly name="C:\Program Files\Reference Assemblies\
Microsoft\Framework\v3.5\System.Xml.Linq.dll" #>
<#@ assembly name="C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Xml.dll" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml.Linq" #>
using System;
using System.Windows;
using System.Windows.Controls;
using System.ComponentModel;<#+
/// <summary>
/// Generates all the classes defined within the given XML file
/// </summary>
private void GenerateClasses(string xmlFileLocation)
{
XNamespace ns = "http://www.scottlogic.co.uk/DependencyObject";
XDocument xmlFile = XDocument.Load(xmlFileLocation);
var depObjs = from c in xmlFile.Descendants(ns + "dependencyObject")
select c;
foreach(var depObj in depObjs)
{
GenerateClass(depObj.Attribute("type").Value, xmlFileLocation);
}
}
/// <summary>
/// Generates an implementation of INotifyPropertChanged
/// </summary>
private void GenerateINotifyPropertChangedImpl()
{
#>
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
<#+
}
/// <summary>
/// Generates a handler for the DP change event
/// </summary>
private void GenerateChangeEventHandler(string className, string propertyName,
bool propertyChangedCallback, bool classRaisesPropertyChanged)
{
string raisePropertyChanged = classRaisesPropertyChanged ?
string.Format("myClass.OnPropertyChanged(\"{0}\");", propertyName) : "";
#>
private static void On<#= propertyName #>PropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
<#= className #> myClass = d as <#= className #>;
<#= raisePropertyChanged #>
myClass.On<#= propertyName #>PropertyChanged(e);
}
partial void On<#= propertyName #>PropertyChanged(
DependencyPropertyChangedEventArgs e);
<#+
}
/// <summary>
/// Generates a DP definition
/// </summary>
private void GenerateDependencyProperty(string className, string propertyType,
string defaultValue, string propertyName, bool changedCallback,
bool isAttached, string metadata, string summary)
{
string propertyMetadata;
string changedCallbackMethodName = changedCallback ? "On" +
propertyName + "PropertyChanged" : "null";
// if DP metadata is provided, create an instance of FrameworkPropertyMetadata,
// this is WPF specific
if (!string.IsNullOrEmpty(metadata))
{
propertyMetadata = string.Format(
"new FrameworkPropertyMetadata({0}, {1}, {2})",
defaultValue, metadata, changedCallbackMethodName);
}
else
{
propertyMetadata = string.Format("new PropertyMetadata({0}, {1})",
defaultValue, changedCallbackMethodName);
}
string registerMethod = isAttached ? "RegisterAttached" : "Register";
#>
/// <summary>
/// Identifies the <#= propertyName #> Dependency Property.
/// <summary>
public static readonly DependencyProperty <#= propertyName #>Property =
DependencyProperty.<#= registerMethod #>("<#= propertyName #>",
typeof(<#= propertyType #>),
typeof(<#= className #>), <#= propertyMetadata #>);
<#+
}
/// <summary>
/// Generates a CLR accessor for a DP
/// </summary>
private void GenerateCLRAccessor(string typeConverter, string propertyType,
string propertyName, string summary)
{
string typeConverterDefinition = typeConverter!= null ?
"[TypeConverter(typeof(" + typeConverter + "))]" : "";
if (!string.IsNullOrEmpty(summary))
GeneratePropertyComment(summary);
#>
<#= typeConverterDefinition #>
public <#= propertyType #> <#= propertyName #>
{
get { return (<#= propertyType #>)GetValue(<#= propertyName #>Property); }
set { SetValue(<#= propertyName #>Property, value); }
}
<#+
}
private void GenerateAttachedPropertyAccessor(string propertyName, string propertyType)
{
#>
// <#= propertyName #> attached property accessors
public static void Set<#= propertyName #>(UIElement element,
<#= propertyType #> value)
{
element.SetValue(PlottedPropertyProperty, value);
}
public static <#= propertyType #> Get<#= propertyName #>(UIElement element)
{
return (<#= propertyType #>)element.GetValue(<#= propertyName #>Property);
}
<#+
}
/// <summary>
/// Generates a comment block for a CLR or DP
/// </summary>
private void GeneratePropertyComment(string summary)
{
#>
/// <summary>
/// <#= summary #>. This is a Dependency Property.
/// </summary><#+
}
/// <summary>
/// Generates a class along with its associated DPs
/// </summary>
private void GenerateClass(string classFullName, string xmlFileLocation)
{
string classNamespace = classFullName.Substring(0, classFullName.LastIndexOf('.'));
string className = classFullName.Substring(classFullName.LastIndexOf('.') + 1);
XNamespace ns = "http://www.scottlogic.co.uk/DependencyObject";
XDocument xmlFile = XDocument.Load(xmlFileLocation);
var dps = from dp in xmlFile.Descendants(ns + "dependencyProperty")
where dp.Parent.Attribute("type").Value == classFullName
select dp;
var depObj = (from c in xmlFile.Descendants(ns + "dependencyObject")
where c.Attribute("type").Value == classFullName
select c).Single();
bool classRaisesPropertyChanged =
depObj.Attribute("notifyPropertyChanged")!=null &&
(depObj.Attribute("notifyPropertyChanged").Value ==
"1" || depObj.Attribute("notifyPropertyChanged").Value == "true");
string baseType = depObj.Attribute("base").Value;
#>
namespace <#= classNamespace #>
{
public partial class <#= className #> :
<#= baseType #><#+ if(classRaisesPropertyChanged){ #>,
INotifyPropertyChanged<#+ } #>
{
<#+
foreach(var dp in dps)
{
string propertyName = dp.Attribute("name").Value;
string propertyType = dp.Attribute("type").Value;
string summary = dp.Attribute("summary")!=null ?
dp.Attribute("summary").Value : null;
string metadata = dp.Attribute("metadata")!=null ?
dp.Attribute("metadata").Value : null;
string defaultValue = dp.Attribute("defaultValue").Value;
string typeConverter = dp.Attribute("typeConverter")!=null ?
dp.Attribute("typeConverter").Value : null;
bool propertyChangedCallback =
dp.Attribute("propertyChangedCallback")!=null &&
(dp.Attribute("propertyChangedCallback").Value ==
"1" || dp.Attribute("propertyChangedCallback").Value == "true");
bool isAttached = dp.Attribute("attached")!=null &&
(dp.Attribute("attached").Value == "1" ||
dp.Attribute("attached").Value == "true");
#>
#region <#= propertyName #>
<#+
GenerateCLRAccessor(typeConverter, propertyType, propertyName, summary);
bool handleDPPropertyChanged =
propertyChangedCallback || classRaisesPropertyChanged;
GenerateDependencyProperty(className, propertyType, defaultValue,
propertyName, handleDPPropertyChanged,
isAttached, metadata, summary);
if (handleDPPropertyChanged)
{
GenerateChangeEventHandler(className, propertyName,
propertyChangedCallback, classRaisesPropertyChanged);
}
if (isAttached)
{
GenerateAttachedPropertyAccessor(propertyName, propertyType);
}
#>
#endregion
<#+
} // end foreach dps
if (classRaisesPropertyChanged)
{
GenerateINotifyPropertChangedImpl();
}
#>
}
}
<#+
}
#>
XML架构
<?xml version="1.0" encoding="utf-8"?>
<xs:schema
targetNamespace="http://www.scottlogic.co.uk/DependencyObject"
elementFormDefault="qualified"
xmlns="http://www.scottlogic.co.uk/DependencyObject"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="dependencyObjects" type="dependencyObjectsType"/>
<xs:complexType name="dependencyObjectsType">
<xs:sequence>
<xs:element name="dependencyObject"
type="dependencyObjectType"
maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="dependencyObjectType">
<xs:sequence>
<xs:element name="dependencyProperty"
type="dependencyPropertyType" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="type"
type="xs:string" use="required"/>
<xs:attribute name="notifyPropertyChanged"
type="xs:boolean" use="optional"/>
<xs:attribute name="base" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="dependencyPropertyType">
<xs:attribute name="summary" type="xs:string" use="optional"/>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="type" type="xs:string" use="required"/>
<xs:attribute name="typeConverter" type="xs:string" use="optional"/>
<xs:attribute name="defaultValue" type="xs:string" use="required"/>
<xs:attribute name="propertyChangedCallback" type="xs:boolean" use="optional"/>
<xs:attribute name="notifyPropertyChanged" type="xs:boolean" use="optional"/>
<xs:attribute name="attached" type="xs:boolean" use="optional"/>
<xs:attribute name="metadata" type="xs:string" use="optional"/>
</xs:complexType>
</xs:schema>
快速用户指南
该模板有两个入口点:GenerateClass
,它从引用的XML文件生成特定类;以及GenerateClasses
,它生成XML文件中的所有类。我通常为我的项目中的所有类(对于简单项目)或命名空间(对于更复杂的项目)创建一个单一的XML文件。这有助于将所有生成的代码保持在一个地方,并且还消除了大量简单的4行模板的需要,这些模板仅引用DP生成模板并调用GenerateClass
。本节简要描述了XML架构中的各种元素和属性。在结构上,XML文件可以包含一个或多个dependencyObject
元素实例,每个实例包含一个或多个dependencyProperty
元素。dependencyObject
元素具有以下属性
type
- 生成类的完全限定名。base
- 生成类的超类的名称(如果它在同一命名空间中,则不需要完全限定)。notifyPropertyChanged
- 如果此布尔属性设置为true
,则该类将实现INotifyPropertyChanged
,并在任何DP更改时引发PropertyChanged
事件。
dependencyProperty
元素具有以下属性
name
- DP的名称。type
- DP的类型。defaultValue
- 依赖属性的默认值。summary
- DP的描述,将用于文档化CLR包装器。typeConverter
- 要与CLR包装器关联的类型转换器。例如,如果指定转换器typeConverter="MyConverter"
,则会将以下属性与CLR属性关联:[TypeConverter(typeof(MyConverter))]
。propertyChangedCallback
- 一个布尔属性,指示是否添加属性更改回调。请注意,如果此属性为false
且dependencyObject
上的notifyPropertyChanged
属性也为false
,则不会生成PropertyChangedCallback
。attached
- 指示这是一个附加DP。metadata
- (仅限WPF)此DP的框架元数据;例如:metadata="FrameworkPropertyMetadataOptions.Inherits"
。
最后,如果我需要额外的using
语句(除了在DependeycObjectTemplate.tt文件中定义的语句),我通常会将它们添加到调用GenerateClasses
函数的简单模板中
<#@ include file="DependencyObject.tt" #>
// additional using statements go here ...
using System.Collections.Generic;
<#
GenerateClasses(@"C:\Projects\...\DependencyObjects.xml");
#>
实践示例
例如,本节将演示开发一个简单的Silverlight范围控件,该控件包含一对文本框,用于指示最大和最小范围值。该控件有两个DP:Maximum
和Minimum
,它们绑定到这些TextBox
es。当其中任何一个文本更改时,会进行一个简单的检查,以确保Maximum
> Minimum
;如果不是这种情况,则会交换这两个值。
这是我们RangeControl
类的XML描述
<?xml version="1.0" encoding="utf-8" ?>
<dependencyObjects
xmlns="http://www.scottlogic.co.uk/DependencyObject"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dependencyObject type="SilverlightTemplates.RangeControl"
base="UserControl" notifyPropertyChanged="true" >
<dependencyProperty type="double" defaultValue="0.0"
summary="The maximum range value"
name="Maximum" propertyChangedCallback="true"/>
<dependencyProperty type="double" defaultValue="0.0"
summary="The maximum range value"
name="Minimum" propertyChangedCallback="true"/>
</dependencyObject>
</dependencyObjects>
这是使用我们通用的DependencyObjectTemplate.tt生成类的T4模板
<#@ include file="DependencyObjectTemplate.tt" #>
<#
GenerateClass("SilverlightTemplates.RangeControl",
@"C:\Projects\...\RangeControl.xml");
#>
生成的类如下所示
using System;
using System.Windows;
using System.Windows.Controls;
using System.ComponentModel;
namespace SilverlightTemplates
{
public partial class RangeControl : UserControl, INotifyPropertyChanged
{
#region Maximum
/// <summary>
/// The maximum range value. This is a Dependency Property.
/// </summary>
public double Maximum
{
get { return (double)GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
/// <summary>
/// Identifies the Maximum Dependency Property.
/// <summary>
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(double),
typeof(RangeControl), new PropertyMetadata(0.0, OnMaximumPropertyChanged));
private static void OnMaximumPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
RangeControl myClass = d as RangeControl;
myClass.OnPropertyChanged("Maximum");
myClass.OnMaximumPropertyChanged(e);
}
partial void OnMaximumPropertyChanged(DependencyPropertyChangedEventArgs e);
#endregion
#region Minimum
/// <summary>
/// The maximum range value. This is a Dependency Property.
/// </summary>
public double Minimum
{
get { return (double)GetValue(MinimumProperty); }
set { SetValue(MinimumProperty, value); }
}
/// <summary>
/// Identifies the Minimum Dependency Property.
/// <summary>
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register("Minimum", typeof(double),
typeof(RangeControl), new PropertyMetadata(0.0, OnMinimumPropertyChanged));
private static void OnMinimumPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
RangeControl myClass = d as RangeControl;
myClass.OnPropertyChanged("Minimum");
myClass.OnMinimumPropertyChanged(e);
}
partial void OnMinimumPropertyChanged(DependencyPropertyChangedEventArgs e);
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
这是我们的XAML
<UserControl x:Class="SilverlightTemplates.RangeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel x:Name="LayoutRoot"
Background="White" Orientation="Horizontal">
<TextBox Name="minTextBox" Width="50"/>
<TextBlock Text=" : " VerticalAlignment="Center"/>
<TextBox Name="maxTextBox" Width="50"/>
</StackPanel>
</UserControl>
最后,我们的XAML代码隐藏文件如下
namespace SilverlightTemplates
{
public partial class RangeControl : UserControl
{
public RangeControl()
{
InitializeComponent();
// bind the text boxes to the dependency properties of this user control
var maxBinding =
new Binding("Maximum") { Source = this, Mode = BindingMode.TwoWay };
maxTextBox.SetBinding(TextBox.TextProperty, maxBinding);
var minBinding =
new Binding("Minimum") { Source = this, Mode = BindingMode.TwoWay };
minTextBox.SetBinding(TextBox.TextProperty, minBinding);
}
/// <summary>
/// If max is less than min, swap their values
/// </summary>
private void Swap()
{
if (Maximum < Minimum)
{
double swap = Minimum;
Minimum = Maximum;
Maximum = swap;
}
}
partial void OnMaximumPropertyChanged(DependencyPropertyChangedEventArgs e)
{
Swap();
}
partial void OnMinimumPropertyChanged(DependencyPropertyChangedEventArgs e)
{
Swap();
}
}
}
从这个简单的例子可以看出,生成的类的大小是我们代码隐藏实现的两倍多!此外,通过简单地更改我们的RangeControl.xml文件并重新生成,可以快速应用我们DP声明的任何更改。
示例项目
本文附带两个示例项目
- SilverlightRangeControlWithCodeGen.zip - 该项目包含上面示例的
RangeControl
。 - WPFPieChartWithCodeGen.zip - 这是我的饼图控件的修改版本,用于使用此模板。它演示了属性元数据和附加属性。
结论
在本文中,我演示了一种在WPF和Silverlight中生成DP(包括常规DP和附加DP)的技术。我使用这项技术已经有一个月了,发现它极大地提高了我的生产力。此外,我很高兴发现了T4模板的秘密;它们是我最喜欢的工具!虽然我似乎发现T4几乎能解决所有问题……我确信这会过去的……
希望您喜欢这篇文章。如果您使用此模板,并想到任何有用的补充,请在下面的评论区告诉我。