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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (39投票s)

2009年4月28日

CPOL

16分钟阅读

viewsIcon

109052

downloadIcon

1061

本文介绍如何使用简单的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”。然后您将看到类似以下的内容

TemplateFile.png

您的模板文件显示在解决方案资源管理器中,以及它生成的文件。如果您检查模板文件的属性,您会发现它有一个“自定义工具”与之关联,即TextTemplatingFileGenerator。这个类负责调用代码生成引擎,该引擎转换T4文件并生成相关文件。此外,如果您右键单击T4文件,您会注意到一个新的菜单选项:“运行自定义工具”,允许您按需执行您的模板。

CustomTool.png

模板本身在保存更改时执行,并在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绑定在一起,这在很大程度上是一个手动过程,尽管可以实现ElementNameRelativeSource绑定的近似效果。您还必须为每个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 - 一个布尔属性,指示是否添加属性更改回调。请注意,如果此属性为falsedependencyObject上的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:MaximumMinimum,它们绑定到这些TextBoxes。当其中任何一个文本更改时,会进行一个简单的检查,以确保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声明的任何更改。

示例项目

本文附带两个示例项目

结论

在本文中,我演示了一种在WPF和Silverlight中生成DP(包括常规DP和附加DP)的技术。我使用这项技术已经有一个月了,发现它极大地提高了我的生产力。此外,我很高兴发现了T4模板的秘密;它们是我最喜欢的工具!虽然我似乎发现T4几乎能解决所有问题……我确信这会过去的……

希望您喜欢这篇文章。如果您使用此模板,并想到任何有用的补充,请在下面的评论区告诉我。

© . All rights reserved.