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





5.00/5 (6投票s)
这篇博文介绍了一种通过属性声明式地指定 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
' 接口,CodeClass
、CodeNamespace
等派生自它,但却没有一个统一的 '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 模板!)
GenerateCLRAccessor
、GenerateDependencyProperty
等函数是从我之前基于 XML 的 DP 生成方法中重用的。
为了简单演示这种方法,这里是一个简单的范围控件的完整实现,该控件具有 Maximum
和 Minimum
属性,如果用户输入的 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 模板应生成“通用”INotifyPropertyChanged
或 IEditableObject
实现的属性,当然,更复杂的应用程序特定可能性是无限的。
您可以下载一个包含上述 RangeControl
的示例项目:WpfDeclarativeDpCodeGen.zip。
尝试添加新的 DependencyPropertyDecl
属性,然后重新运行代码生成模板(右键单击 CodeGen/GeneratedObjects.tt -> Run Custom Tool),并检查生成的输出——CodeGen/GeneratedObjects.cs。
最后,感谢 Daniel Vaughan 的启发。
此致,
Colin E.