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

使用 T4 + DTE 进行声明式依赖项属性定义

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2009 年 8 月 18 日

CPOL

4分钟阅读

viewsIcon

27873

这篇博文介绍了一种通过属性声明式地指定 WPF / Silverlight 依赖属性的技术。

这篇博文介绍了一种通过属性声明式地指定 WPF / Silverlight 依赖属性的技术,如下例所示:

[DependencyPropertyDecl("Maximum", typeof(double), 0.0)]
[DependencyPropertyDecl("Minimum", typeof(double), 0.0)]
public partial class RangeControl : UserControl
{
    ...
}

在设计时,T4 模板会读取这些声明并生成所需的代码。更多信息,请继续阅读…

几个月前,我在 CodeProject 上写了一篇关于使用 T4 模板生成依赖属性 的技术文章。简而言之;使用这项技术,您需要提供一个 XML 格式的类及其依赖属性的描述,然后运行 T4 模板,就会生成包含这些属性的部分类——不再需要编写 DP 的样板代码!对于不熟悉 T4 的朋友们来说,它是 Visual Studio 内置的一个代码生成引擎,入门请参考我之前的 CodeProject 文章

几天前,Daniel Vaughan 发表了一篇关于使用 T4 模板 生成项目元数据 的博文。他演示了如何解决实现 INotifyPropertyChanged 时硬编码属性名称字符串的长期存在的问题。他的解决方案同时使用了 T4 和 DTE——一个自动化库,允许您以编程方式访问项目和解决方案中的项。Daniel 的博文启发了我重新审视我的 DP 代码生成解决方案,以便简化它并消除对 XML 文件的需求。

我的第一步是定义一个自定义属性,该属性可用于指示一个类需要模板来生成 DP。

[AttributeUsage(AttributeTargets.Class , AllowMultiple = true)]
public class DependencyPropertyDecl : Attribute
{
    public DependencyPropertyDecl(string name, Type type, object defaultValue)
    {
        this.name = name;
        this.type = type;
        this.defaultValue = defaultValue;
    }
 
    public string name;
    public Type type;
    public object defaultValue;
}

使用 DTE,应该可以检查项目中的所有类,识别出存在此属性的类并生成所需的 DPs。然而,查看 DTE API,这并非易事,虽然 API 定义了 CodeElement 的 'base' 接口,CodeClassCodeNamespace 等派生自它,但却没有一个统一的 'Children' 概念,项目和解决方案也不是 CodeElements。换句话说,为了找到解决方案中的所有类,您必须遍历一个异构的类和属性集合。

为了简化查找项目中具有 DependencyPropertyDecl 属性的类的任务,我创建了一个简单的 Linq-to-DTE 实现。

public IEnumerable<CodeElement> CodeElementsInProjectItems(ProjectItems projectItems)
{
    foreach (ProjectItem projectItem in projectItems)
    {
        foreach(CodeElement el in CodeElementsInProjectItem(projectItem))
        {
            yield return el;
        }
    }
}
 
public IEnumerable<CodeElement> CodeElementsInProjectItem(ProjectItem projectItem)
{
    FileCodeModel fileCodeModel = projectItem.FileCodeModel;
 
    if (fileCodeModel != null)
    {
        foreach (CodeElement codeElement in fileCodeModel.CodeElements)
        {
            //WalkElements(codeElement, null);
            foreach(CodeElement el in CodeElementDescendantsAndSelf(codeElement))
            {
                yield return el;
            }
        }
    }
 
    if (projectItem.ProjectItems != null)
    {
        foreach (ProjectItem childItem in projectItem.ProjectItems)
        {
            foreach (CodeElement el in CodeElementsInProjectItem(childItem))
            {
                yield return el;
            }
        }
    }        
} 
 
public IEnumerable<CodeElement> CodeElementsDescendants(CodeElements codeElements)
{
    foreach(CodeElement element in codeElements)
    {
        foreach (CodeElement descendant in CodeElementDescendantsAndSelf(element))
        {
            yield return descendant;                
        }
    }
}
 
public IEnumerable<CodeElement> CodeElementDescendantsAndSelf(CodeElement codeElement)
{
    yield return codeElement;
 
    CodeElements codeElements;
 
    switch(codeElement.Kind)
    {        
 
        /* namespaces */
        case vsCMElement.vsCMElementNamespace:
        {
            CodeNamespace codeNamespace = (CodeNamespace)codeElement;                                        
            codeElements = codeNamespace.Members;
            foreach(CodeElement descendant in CodeElementsDescendants(codeElements))
            {
                yield return descendant;                
            }
            break;
        }
 
        /* Process classes */
        case vsCMElement.vsCMElementClass:
        {            
            CodeClass codeClass = (CodeClass)codeElement;            
            codeElements = codeClass.Members;
            foreach(CodeElement descendant in CodeElementsDescendants(codeElements))
            {                
                yield return descendant;                
            }            
            break;    
        }        
    }    
}

也许有更简单的方法可以实现这一点,这是我第一次创建递归的 IEnumerable 实现,所以如果我做错了什么,请告诉我!要优雅地通过递归创建类似树状结构的 IEnumerable 实现,请参阅 David Jade 的这篇博文。但是,正如我之前提到的,DTE API 对项目结构的异构性会阻碍这种方法。

无论如何,有了这个棘手的 IEnumerable 实现,我们就可以利用 Linq 的强大功能,简洁地实现“查找解决方案中所有拥有一个或多个 DependencyPropertyDecl 属性的类”,并按如下方式执行所需的代码生成:

// for details of how the DTE 'project' is located see Daniel's blog post 
// or the attached source of this blog.
var elements = CodeElementsInProjectItems(project.ProjectItems);
var classes = elements.Where(el => el.Kind == vsCMElement.vsCMElementClass)
                               .Cast<CodeClass>()
                               .Where(cl => Attributes(cl).Any(at => at.Name=="DependencyPropertyDecl"));
 
foreach(var clazz in classes)
{
    GenerateClass(clazz);
}

GenerateClass 函数是我为基于 XML 描述生成类而编写的函数的简单修改。在此实现中,我们获取每个自定义属性实例的 CodeAttribute.Value 属性,并从中获取 DP 的名称、类型和默认值。

<#+
/// <summary>
/// Generates a class along with its associated DPs
/// </summary>
private void GenerateClass(CodeClass clazz)
{
    string classNamespace = clazz.Namespace.Name;
    string className =  clazz.Name;

    bool classRaisesPropertyChanged = false;

#>

namespace <#= classNamespace #>
{
    public partial class <#= className #> 
    <#+ if(classRaisesPropertyChanged){ #>: INotifyPropertyChanged<#+ } #>
    {
<#+
    var attributes = Attributes(clazz).Where(att => att.Name=="DependencyPropertyDecl");
    foreach(CodeAttribute attribute in attributes)
    {
        string[] attributeValues = attribute.Value.Split(',');

        string propertyName = attributeValues[0].Trim().Replace("\"","");
        string propertyType = attributeValues[1].Trim().Substring(7, attributeValues[1].Length - 9);
        string summary = null;
        string metadata = null;
        string defaultValue = attributeValues[2].Trim();
        string typeConverter = null;
        bool propertyChangedCallback = true;
        bool isAttached = false;
        #>

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

<#+
}
#>

(请原谅缺乏语法高亮,WordPress/CodeProject 不支持 T4 模板!)

GenerateCLRAccessorGenerateDependencyProperty 等函数是从我之前基于 XML 的 DP 生成方法中重用的。

为了简单演示这种方法,这里是一个简单的范围控件的完整实现,该控件具有 MaximumMinimum 属性,如果用户输入的 Minimum 大于 Maximum,则会交换这些值。

<UserControl x:Class="WpfDeclarativeDPCodeGen.RangeControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <StackPanel x:Name="LayoutRoot" Background="White" Orientation="Horizontal">
        <TextBox  Width="50" Text="{Binding Minimum}"/>
        <TextBlock Text=" : " VerticalAlignment="Center"/>
        <TextBox  Width="50" Text="{Binding Maximum}"/>
    </StackPanel>
</UserControl>
[DependencyPropertyDecl("Maximum", typeof(double), 0.0)]
[DependencyPropertyDecl("Minimum", typeof(double), 0.0)]
[TypeConverter(typeof(string))]
public partial class RangeControl : UserControl
{
    public RangeControl()
    {
        InitializeComponent();
    }
 
    /// <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();
    }
}

使用这种方法时,工作流程非常简单:创建您的类,添加 DependencyPropertyDecl 属性,然后执行代码生成模板。输出是一个伴随的偏类,其中包含 DP 代码本身,以及可以实现的偏方法,以便在属性更改时执行逻辑。

我还没有完全将我的基于 XML 的 DP 生成模板移植到这个新的声明式方法,例如,DependencyPropertyDecl 属性还需要指示属性元数据,属性是否为附加属性等……但是,我想分享我的想法,以防我没有时间(或动力)完成最终实现。

我认为这种方法具有远远超出 DP 代码生成的巨大潜力,简单的例子包括指示 T4 模板应生成“通用”INotifyPropertyChangedIEditableObject 实现的属性,当然,更复杂的应用程序特定可能性是无限的。

您可以下载一个包含上述 RangeControl 的示例项目:WpfDeclarativeDpCodeGen.zip

尝试添加新的 DependencyPropertyDecl 属性,然后重新运行代码生成模板(右键单击 CodeGen/GeneratedObjects.tt -> Run Custom Tool),并检查生成的输出——CodeGen/GeneratedObjects.cs

最后,感谢 Daniel Vaughan 的启发。

此致,
Colin E.

© . All rights reserved.