使用 T4 模板进行声明式代码片段自动化






4.77/5 (15投票s)
本文介绍了一种通过属性与类关联来自动化代码片段的技术。这导致了一种声明式的方法来生成样板代码。
下载 CodesnippetAutomation.zip - 730.67 KB
目录
[来自 Flickr 的图片,采用 CC 许可,来自 ralphbijker]
概述
本文介绍了一种自动化代码片段的机制。使用 T4 模板为每个代码片段生成属性。当将属性应用于类时,将根据提供给属性的参数,在部分类中生成代码片段代码。这种方法允许您更改代码片段参数的值并重新生成代码,它还消除了类中重复的样板代码,有利于更简洁、声明式地描述类的功能。
引言
几年前,当我开始使用 WPF 和 Silverlight 时,这些框架让我感到沮丧的一件事就是定义依赖属性所需的代码有多么冗长(依赖属性是一种特殊的属性,可以进行动画处理、支持继承等...)。为了解决这个问题,我提出了一个解决方案,该解决方案使用 T4 模板和 ENV.DTE 来生成包含基于属性的依赖属性定义的类。例如,要向类添加 ItemsSource
属性,只需添加如下属性:
[DependencyPropertyDecl("ItemsSource", typeof(IEnumerable), null,
"Gets or sets a collection used to generate the content of the JumpList")]
public partial class JumpList : Control
{
public JumpList()
{
this.DefaultStyleKey = typeof(JumpList);
}
}
这将生成以下代码
public partial class JumpList
{
#region ItemsSource
/// <summary>
/// Gets or sets a collection used to generate the content of the JumpList.
/// This is a Dependency Property.
/// </summary>
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
/// <summary>
/// Identifies the ItemsSource Dependency Property.
/// <summary>
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable),
typeof(JumpList), new PropertyMetadata(null, OnItemsSourcePropertyChanged));
private static void OnItemsSourcePropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
JumpList myClass = d as JumpList;
myClass.OnItemsSourcePropertyChanged(e);
}
partial void OnItemsSourcePropertyChanged(DependencyPropertyChangedEventArgs e);
#endregion
}
我发现这节省了大量时间,并且在我工作的每个 Silverlight / WPF 项目中都使用了相同的代码。
虽然依赖属性是样板代码的一个相当极端的例子,但绝非唯一的一个。最近,我开始了一个项目,该项目有一个相当广泛的模型层。该层包含许多实现 INotifyPropertyChanged
和引发事件的属性的类。同样,我发现自己编写了大量的样板代码。“标准”的样板代码方法是使用代码片段,我发现自己为我们在模块中使用的各种代码模式添加了新的代码片段。这加速了开发,但仍然生成了大量不适合重构的代码,并且无助于提高代码的可读性。
如果我能将代码片段的多功能性与声明式代码生成的便利性结合起来,这将是我所有样板代码问题的绝佳解决方案。本文介绍了我提出的解决方案。
本文及其提供的代码使用了 T4 模板,这是一个内置于 Visual Studio 的从模板生成源代码的机制,即生成代码的代码。要快速了解 T4 模板,我建议您阅读我 早些时候在 codeproject 上的文章。
您无需了解 T4 模板的所有细节即可使用此代码生成技术。您可能只需要知道 T4 模板是扩展名为 ".tt" 的文本文件,当其内容更改、解决方案构建或单击以下按钮时,它们就会被执行
因此,如果您使用的是本文描述的技术,每次添加或删除类中的属性并希望更新生成的代码时,只需单击上面的按钮或构建项目。
从代码片段到属性
使用 XSLT 转换代码片段
过程的第一步是将每个代码片段转换为可以与类关联的属性。该属性应具有反映代码片段属性的属性。例如,以下片段是我为实现 INotifyPropertyChanged
的类添加 CLR 属性而创建的一个片段
<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>Define a Property with Change Notification</Title>
<Shortcut>PropertyINPC</Shortcut>
<Description>Code snippet for a property which raises INotifyPropertyChanged</Description>
<Author>Colin Eberhardt</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>type</ID>
<ToolTip>Property Type</ToolTip>
<Default>string</Default>
</Literal>
<Literal>
<ID>summary</ID>
<ToolTip>Summary Documentation</ToolTip>
<Default>Gets / sets the property value</Default>
</Literal>
<Literal>
<ID>property</ID>
<ToolTip>Property Name</ToolTip>
<Default>MyProperty</Default>
</Literal>
<Literal>
<ID>field</ID>
<ToolTip>Backing Field</ToolTip>
<Default>_myproperty</Default>
</Literal>
<Literal>
<ID>defaultValue</ID>
<ToolTip>Field default value</ToolTip>
<Default>null</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[
/// <summary>
/// Field which backs the $property$ property
/// </summary>
private $type$ $field$ = $defaultValue$;
public static readonly string $property$Property = "$property$";
/// <summary>
/// $summary$
/// </summary>
public $type$ $property$
{
get { return $field$; }
set
{
if ($field$ == value)
return;
$field$ = value;
OnPropertyChanged($property$Property);
}
}
$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
要以声明方式使用此代码片段,我们需要一个具有这些属性的属性,即 type、property、field 和 defaultValue。有许多技术可以用于执行此转换,例如,您可以使用 Linq to XML 查询上面的 XML,并以编程方式构造(作为字符串的)属性。然而,我个人偏爱 XSLT,每当我需要转换 XML 文档时,因为模板化方法使得可视化转换输出更加容易。XSLT 最常用于 XML 到 XML 的转换,尽管通过设置“输出方法”,您可以将 XML 转换为任何形式的文本输出。例如,您可以使用它将 XML 转换为 SQL、C# 或 CSV,它确实是一种强大的语言!
以下简单的 XSLT 文档将代码片段转换为属性
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:s="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet"
exclude-result-prefixes="msxsl">
<xsl:output method="text"/>
<xsl:template match="/">
using System;
namespace Snippets
{
<xsl:apply-templates select="//s:CodeSnippet"/>
}
</xsl:template>
<!-- matches a CodeSnippet element generating an attribute -->
<xsl:template match="s:CodeSnippet">
/// <summary>
/// <xsl:value-of select="s:Header/s:Description"/>
/// </summary>
[AttributeUsage(AttributeTargets.Class , AllowMultiple = true)]
public class Snippet<xsl:value-of select="s:Header/s:Shortcut"/> : Attribute
{
<!-- generate the attribute properties -->
<xsl:apply-templates select="//s:Declarations/s:Literal"/>
<!-- add the snippet code to the attribute -->
<xsl:apply-templates select="//s:Code"/>
}
</xsl:template>
<!-- generates a string property for codesnippet literal -->
<xsl:template match="s:Literal">
/// <summary>
/// <xsl:value-of select="s:ToolTip"/>
/// </summary>
public string <xsl:value-of select="s:ID"/> = "<xsl:value-of select="s:Default"/>";
</xsl:template>
<xsl:template match="s:Code">
<!-- escape any quotes in the snippet -->
<xsl:variable name="escaped">
<xsl:call-template name="escapeQuot">
<xsl:with-param name="text" select="."/>
</xsl:call-template>
</xsl:variable>
<!-- add a method that returns the snippet -->
/// <summary>
/// Gets the code snippet
/// </summary>
public string GetSnippet()
{
return @"<xsl:value-of select="$escaped" />";
}
</xsl:template>
<!-- relpaces single quotes with double-quotes -->
<xsl:template name="escapeQuot">
<xsl:param name="text"/>
<xsl:choose>
<xsl:when test="contains($text, '"')">
<xsl:variable name="bufferBefore" select="substring-before($text,'"')"/>
<xsl:variable name="newBuffer" select="substring-after($text,'"')"/>
<xsl:value-of select="$bufferBefore"/>
<xsl:text>""</xsl:text>
<xsl:call-template name="escapeQuot">
<xsl:with-param name="text" select="$newBuffer"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
上面的转换非常简单,第一个模板匹配文档根,输出命名空间和 using 语句。选择任何子 CodeSnippet
元素以在命名空间的范围内输出其转换后的内容。请注意,由于 CodeSnippets
XML 文件通过 xmlns 属性应用了默认命名空间,因此我们必须在元素名称前加上相同的命名空间前缀才能成功匹配它们。
匹配 CodeSnippet
元素的模板输出属性类,并选择 XPath “//s:Declarations/s:Literal”
,它匹配节点集,其中每个 Literal 元素在代码片段 XML 文档中都有一个节点,即代码片段的属性。模板还选择包含代码片段本身的代码的元素。
正如您所见,XSLT 方法非常优雅;您提供离散的模板,每个模板将 XML 节点转换为所需的目标格式。输出的结构清晰可见,直接在此 XSLT 文件中重现。模板通过 apply-templates 元素连接在一起,这些元素定义了下一个要匹配的节点集,作为 XPath 查询。
上面 XSLT 中唯一有点丑陋的部分是转义代码片段中定义的代码中的引号的代码,以便它可以包含在 C# 逐字字符串中。XSLT 在转换 XML 文档结构方面非常出色,但在转换内容方面却不那么出色。如您在上面的示例中看到的 escapeQuote
模板那样,对字符串执行简单的查找和替换需要递归。
使用上述代码片段作为输入运行此 XSLT 转换的结果是以下属性
using System;
namespace Snippets
{
/// <summary>
/// Code snippet for a property which raises INotifyPropertyChanged
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class SnippetPropertyINPC : Attribute
{
/// <summary>
/// Property Type
/// </summary>
public string type = "string";
/// <summary>
/// Property Name
/// </summary>
public string property = "MyProperty";
/// <summary>
/// Backing Field
/// </summary>
public string field = "_myproperty";
/// <summary>
/// Field default value
/// </summary>
public string defaultValue = "null";
/// <summary>
/// Gets the code snippet
/// </summary>
public string GetSnippet()
{
return @"
/// <summary>
/// Field which backs the $property$ property
/// </summary>
private $type$ $field$ = $defaultValue$;
public static readonly string $property$Property = ""$property$"";
/// <summary>
/// Gets / sets the $property$ value
/// </summary>
public $type$ $property$
{
get { return $field$; }
set
{
if ($field$ == value)
return;
$field$ = value;
OnPropertyChanged($property$Property);
}
}
$end$";
}
}
}
使用上面的 XSLT,可以为任何代码片段生成相应的属性。
使用 T4 自动生成代码片段
每次要生成属性时,都可以手动执行上面的 XSLT,但是,如果您可以简单地将 .snippet 文件放入项目中并自动生成属性,那将使生活更轻松。
顺便说一句,我最初曾研究过是否可以通过 Visual Studio API 找到用户的所有代码片段。然而,每个用户通常都有自己定义的代码片段集,这在用户之间共享代码时会引起问题。我决定一个更好的方法是要求将代码片段添加到项目中,这样您就可以确保项目上的每个协作者都共享相同的代码片段代码。
在示例项目中,如果您将一个代码片段添加到 Snippets 文件夹,然后单击指示按钮运行所有 T4 模板,您会发现会生成一个相应的 C# 文件,其中包含上一节中描述的 XSLT 转换的输出。
为了实现这一点,我使用了 Env.DTE,这是一个用于 Visual Studio 自动化的 API。此 API 允许您探索 Visual Studio 项目,查找其中包含的类和其他文件。我发现它是一个与 T4 结合使用的绝佳工具,其他人也这么认为。例如,请参阅 Daniel Vaughan 的出色文章,其中描述了一种使用 T4 & Env.DTE 生成类元数据(例如属性名称等)的技术。
我创建了我自己的一套实用程序,允许您执行 Linq 风格的查询来搜索项目文件/类。我不会在这里详细介绍,有关更多详细信息,请参阅我 关于依赖属性代码生成的早期文章。
下面的 T4 模板会查找此模板所属的 Env.DTE 项目,然后查询所有 ProjectItems 以查找扩展名为 .snippet 的项目。然后它执行 GenerateAttributes
方法,该方法运行 XSLT 转换并将生成的输出添加到项目中
<#@ template language="C#" hostSpecific="true" debug="true" #>
<#@ output extension="cs" #>
<#@ include file="Util.tt" #>
<#@ include file="EnvDTE.tt" #>
<#
var project = FindProjectHost();
// capture the generated output so far, and use for each class file
Includes = this.GenerationEnvironment.ToString();
this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length);
// generate the snippet attributes
GenerateAttributes(project);
#>
<#+
/// <summary
/// Generates attributes for all codesnippets within the given project.
/// </summary>
public void GenerateAttributes(Project project)
{
// extract the path
int lastSlash = project.FileName.LastIndexOf(@"\");
string projectPath = project.FileName.Substring(0,lastSlash);
// find all the ProjectItems which are code snippets
var snippets = GetProjectItems(project).Where(item => item.FileNames[0].EndsWith("snippet"));
// apply the XSLT file which generates attributes
foreach(ProjectItem item in snippets)
{
string filename = item.FileNames[0];
string attributeFilename = filename.Substring(0, filename.Length - 8) + ".cs";
RunTransform(projectPath + @"\CodeGen\SnippetToAttribute.xslt",
filename, attributeFilename, project);
}
}
#>
RunTransform
的代码如下所示,这是我在几个 Env.DTE/T4 项目中使用过的另一个实用程序方法
/// <summary
/// Executes the given transform on the given source, adding the
/// generated output to the given project.
/// </summary>
public void RunTransform(string transformPath, string sourcePath,
string outputPath, Project project)
{
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load(transformPath);
XDocument source = XDocument.Load(sourcePath);
StringWriter strWriter = new StringWriter();
var args = new XsltArgumentList();
transform.Transform(source.CreateReader(), args, strWriter);
WriteLine(strWriter.ToString());
SaveOutput(outputPath, project);
}
/// <summary
/// Adds the given file to the given project.
/// </summary>
public void SaveOutput(string outputFileName, Project project)
{
// write all of the generated output to a file
string templateDirectory = Path.GetDirectoryName(Host.TemplateFile);
string outputFilePath = Path.Combine(templateDirectory, outputFileName);
File.WriteAllText(outputFilePath, this.GenerationEnvironment.ToString());
// clear the generated output
this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length);
// add to the project
project.ProjectItems.AddFromFile(outputFilePath);
}
SaveOutput
方法也是工具箱的一个有用补充,它将 T4 模板的输出保存到文件中并将其添加到项目中。对于生成多个类的模板来说,它非常有用,允许您将它们拆分到多个文件中。
[来自 Flickr 的图片,采用 CC 许可,来自 RogueSun Media]
从属性到代码!
声明式代码片段
我们将从一个简单的例子开始,一个实现 INotifyPropertyChanged
的类,并有一个在其 setter 中引发 PropertyChanged
事件的单个属性。我们将使用上一节中的 SnippetPropertyINPC
,并添加一个用于实现 INotifyPropertyChanged
本身的片段。以下片段被添加到项目中
<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>Implementation of INotifyPropertyChanged</Title>
<Shortcut>INotifyPropertyChanged</Shortcut>
<Description>Implementation of INotifyPropertyChanged</Description>
<Author>Colin Eberhardt</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Code Language="csharp">
<![CDATA[
#region INotifyPropertyChanged Members
/// <summary>
/// Occurs when a property changes
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises a PropertyChanged event
/// </summary>
protected void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
#endregion
$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
当 T4 模板运行时,它会生成以下属性
/// <summary>
/// Implementation of INotifyPropertyChanged
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class SnippetINotifyPropertyChanged : Attribute
{
/// <summary>
/// Gets the code snippet
/// </summary>
public string GetSnippet()
{
return @"
#region INotifyPropertyChanged Members
/// <summary>
/// Occurs when a property changes
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises a PropertyChanged event
/// </summary>
protected void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
#endregion
$end$";
}
}
然后,我们可以使用这些属性来“声明”一个类实现了 INotifyPropertyChanged
并拥有一个引发此事件的属性
[SnippetINotifyPropertyChanged]
[SnippetPropertyINPC(field = "_height", type = "int", property = "Height", defaultValue = "1")]
public partial class SomeViewModel : INotifyPropertyChanged
{
public SomeViewModel()
{
}
}
请注意,这是一个部分类,Visual Studio 广泛使用的一种语言功能,用于将设计器生成的代码与我们自己的代码分开。这里将其用于生成与上述类对应的另一个部分类,其中包含与属性对应的代码。也可以有类的一个以上的两个部分定义,允许您将此代码生成技术用于已经具有设计器生成对应项的类。
在下一节中,我们将了解代码生成的工作原理...
生成代码
我们将使用上面描述的技术,Linq-to-Env.DTE,来查找我们项目中具有一个或多个代码片段属性的类。
<#@ template language="C#" hostSpecific="true" debug="true" #>
<#@ output extension="cs" #>
<#@ import namespace="System.Text.RegularExpressions"#>
<#@ include file="Util.tt" #>
<#@ include file="EnvDTE.tt" #>
<#@ include file="Includes.tt" #>
<#
var project = FindProjectHost();
// capture the generated output so far, and use for each class file
Includes = this.GenerationEnvironment.ToString();
this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length);
int lastSlash = project.FileName.LastIndexOf(@"\");
string projectPath = project.FileName.Substring(0,lastSlash);
AllElements = GetProjectItems(project).SelectMany(item => GetCodeElements(item)).ToList();
// iterate over the files in the project
foreach(ProjectItem projectItem in GetProjectItems(project))
{
// find any classes that have a 'snippet' attribute
var classes = GetCodeElements(projectItem)
.Where(el => el.Kind == vsCMElement.vsCMElementClass)
.Cast<CodeClass>()
.Where(cl => Attributes(cl).Any(at => at.Name.StartsWith("Snippet")));
foreach(var clazz in classes)
{
// generate the snippet
GenerateClass(clazz);
SaveOutput(projectPath + @"\CodeGen\Generated\" + projectItem.Name, project);
}
}
#>
<#+
public List<CodeElement> AllElements { get; set; }
public string Includes { get; set; }
#>
上面的 T4 模板首先将 Includes.tt
的输出捕获到一个字符串中,该字符串用于在每个生成文件的开头添加“using”部分。然后是一个 Linq 查询,该查询查找任何具有以“Snippet”开头的属性的类。对于其中的每一个,都会调用 GenerateClass
方法。然后捕获生成的输出,并使用前面描述的 SaveOutput
实用程序方法将其保存到文件,该方法会保存到文件并将其添加到项目中。
GenerateClass
方法会添加样板代码、命名空间、部分类,然后迭代所有代码片段属性,为每个属性调用 GenerateSnippet
方法
<#+
/// <summary
/// Generates a class with snippets
/// </summary>
private void GenerateClass(CodeClass clazz)
{
string classNamespace = clazz.Namespace.Name;
string className = clazz.FullName.Substring(clazz.FullName.LastIndexOf(".")+1);
string classVisiblity = GetClassVisiblityString(clazz);
#>
<#= Includes #>
namespace <#= classNamespace #>
{
<#= classVisiblity #> partial class <#= className #>
{
<#+
// iterate over all the 'snippet' attributes
var attributes = Attributes(clazz).Where(at => at.Name.StartsWith("Snippet"));
foreach(var attribute in attributes)
{
GenerateSnippet(attribute);
}
#>
}
}
<#+
}
#>
GenerateSnippet
方法是乐趣开始的地方
<#+
/// <summary
/// Generates the given snippet
/// </summary>
private void GenerateSnippet(CodeAttribute attribute)
{
// locate the attribute class
CodeClass attributeClass = AllElements.Where(el => el.Kind == vsCMElement.vsCMElementClass)
.Cast<CodeClass>()
.Where(d => d.Name==attribute.Name).First();
var snippetFields = Members(attributeClass).Where(m => m.Kind == vsCMElement.vsCMElementVariable);
var values = new Dictionary<string, string>();
foreach(CodeElement field in snippetFields)
{
var text = GetElementText(field);
// extract the default values from the snippet attribute
Regex regex = new Regex("= \"(.*?)\"");
Match match = regex.Match(text);
var defaultValue = match.Groups[1].Value;
values[field.Name] = defaultValue;
// extract instance values from the CodeAttribute
regex = new Regex(field.Name + @"\s*=\s*(@""(?:[^""]|"""")*""|""(?:\\.|[^\\""])*"")");
match = regex.Match(attribute.Value);
if (match.Success)
{
string literalValue = match.Groups[1].Value;
if (!literalValue.StartsWith("@"))
{
literalValue = literalValue.Substring(1, literalValue.Length - 2);
values[field.Name] = StringFromCSharpLiteral(literalValue);
}
else
{
literalValue = literalValue.Substring(2, literalValue.Length - 3);
values[field.Name] = StringFromVerbatimLiteral(literalValue);
}
}
}
// extract the snippet
var snippetMethod = Members(attributeClass).Where(m => m.Name=="GetSnippet").Single();
var snippetText = GetElementText(snippetMethod);
var firstQuote = snippetText.IndexOf("\"");
var lastQuote = snippetText.IndexOf(@"$end$");
snippetText = snippetText.Substring(firstQuote + 1, lastQuote - firstQuote - 1);
snippetText = snippetText.Replace("\"\"", "\"");
foreach(var value in values)
{
snippetText = snippetText.Replace("$"+value.Key+"$", value.Value);
}
#><#=snippetText#><#+
}
#>
此方法定位属性本身,然后使用 Linq 提取代码片段的字段。对于每个字段,我们从属性中提取默认值。这利用了以下 Env.DTE 实用程序方法,该方法捕获 CodeElement
的文本
<#+
/// <summary>
/// Extracts the code that the given element represents
/// </summary>
public string GetElementText(CodeElement element)
{
var sp = element.GetStartPoint();
var ep = element.GetEndPoint();
var edit = sp.CreateEditPoint();
return edit.GetText(ep);
}
#>
以下正则表达式用于从与类关联的属性中提取字段实例值
field.Name + @""(?:[^""]|"""")*""|""(?:\\.|[^\\""])*"")"
上面的表达式匹配字符串文字和逐字字符串,是的,我确实需要一些帮助才能找到正确的表达式(谢谢 StackOverflow!)。如果表达式匹配,则使用 StringFromCSharpLiteral
或 StringFromVerbatimLiteral
方法通过解析转义字符串来提取值,从而得到与编译器解释字段值时相同的结果。再次感谢 Google 和 Istvan 提供这些有用的方法!
最后,代码片段本身从属性的 GetSnippet 方法中提取,并替换代码片段中的字段标记。
重新审视我们的类
[SnippetINotifyPropertyChanged] [SnippetPropertyINPC(field = "_height", type = "int", property = "Height", defaultValue = "1")] public partial class SomeViewModel : INotifyPropertyChanged { public SomeViewModel() { } }当 T4 模板执行时,会生成以下部分类
最后,代码片段本身从属性的 GetSnippet
方法中提取,并替换代码片段中的字段标记。
重新审视我们的类
[SnippetINotifyPropertyChanged]
[SnippetPropertyINPC(field = "_height", type = "int", property = "Height", defaultValue = "1")]
public partial class SomeViewModel : INotifyPropertyChanged
{
public SomeViewModel()
{
}
}
using System.ComponentModel;
namespace CodeSnippetAutomation
{
public partial class SomeViewModel
{
#region INotifyPropertyChanged Members
/// <summary>
/// Occurs when a property changes
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises a PropertyChanged event
/// </summary>
protected void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
#endregion
/// <summary>
/// Field which backs the Height property
/// </summary>
private int _height = 1;
public static readonly string HeightProperty = "Height";
/// <summary>
/// Gets / sets the Height value
/// </summary>
public int Height
{
get { return _height; }
set
{
if (_height == value)
return;
_height = value;
OnPropertyChanged(HeightProperty);
}
}
}
}
请注意,INotifyPropertyChanged
的代码片段会添加事件和一个用于调用事件的受保护方法,但它不会将接口添加到生成的类中。因此,我们必须手动将接口添加到我们的类中。但是,在拥有部分对应项中的接口实现的同时,指示类实现某个接口是完全可以接受的。
使代码片段便于代码生成
有时我们需要对代码片段进行一些修改,以使其适合代码生成。这样做的一个主要原因是,如果我们从代码片段生成代码,我们就不能“调整”输出,因为下次生成代码时会被覆盖。例如,使用手动代码片段,您可能会发现自己正在根据特定用途调整生成的代码,对其进行微调。对于代码生成,每个生成的“实例”都必须相同。
如果我们以上面详细的示例为例,一个用于生成引发更改通知的属性的代码片段,通常需要在属性更改时执行一些代码。使用常规的手动代码片段,我们会编辑生成的输出。为了支持代码生成中的此要求,我们必须在我们的代码片段代码中构建扩展点。
幸运的是,部分方法有一个有用的技巧——部分方法。部分方法是在部分类中定义的 void 方法,但没有实现。然后,您可以选择在其他部分类对应项中提供部分方法的实现。请注意,这是完全可选的——它不是接口风格的合同。如果未提供部分方法的实现,编译器实际上会删除对部分方法的调用,因此部分方法必须是 void。
我们可以按如下方式修改代码片段
<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
...
<Snippet>
...
<Code Language="csharp">
<![CDATA[
/// <summary>
/// Field which backs the $property$ property
/// </summary>
private $type$ $field$ = $defaultValue$;
public static readonly string $property$Property = "$property$";
/// <summary>
/// $summary$
/// </summary>
public $type$ $property$
{
get { return $field$; }
set
{
if ($field$ == value)
return;
$field$ = value;
On$property$Changed(value);
OnPropertyChanged($property$Property);
}
}
/// <summary>
/// Invoked when the value of $property$ changes
/// </summary>
partial void On$property$Changed($type$ value);
$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
这意味着我们现在可以向我们的类添加代码,该类具有如下的声明式代码片段生成
[SnippetINotifyPropertyChanged]
[SnippetPropertyINPC(field ="_foo", property ="Foo", defaultValue = "\"FOO\"")]
public partial class SomeOtherViewModel
{
partial void OnFooChanged(string value)
{
// invoked when the Foo property changes
}
}
这意味着我们现在可以向我们的类添加代码,该类具有如下的声明式代码片段生成
[来自 Flickr 的图片,采用 CC 许可,来自 MrB-MMX]
详细示例
前面的章节描述了使声明式代码片段自动化工作的技术和机制。在本节中,我将把这些机制应用到一个实际的例子中。我没有创建一些使用代码生成技术的示例项目,而是认为从一个现有项目开始会更有信息量,该项目比虚构的示例拥有更大的代码库。我选择的项目是“SilverTrack”,一个基于 Silverlight 的 遥测应用程序,已在此处发布到 codeproject。SilverTrack 使用 Model-View-ViewModel UI 模式,这通常会导致大量样板代码。它还使用了自定义控件和用户控件,再次增加了额外的样板代码。
代码生成的第一个步骤是添加 CodeGen
文件夹,该文件夹会将各种模板添加到项目中。我还添加了本文前面介绍的代码片段。还有一个用于依赖属性的进一步代码片段 "dp.snippet"
,稍后将进行介绍。
SilverTrack 有许多 ViewModel
类,每个类都包含许多具有更改通知的属性。从 TelemetryChannelViewModel
开始,我删除了六个属性,并将它们替换为代码片段属性。我还删除了对“基本”视图模型的引用,该模型只是实现了 INotifyPropertyChanged
,并用一个合适的代码片段替换了它(这让您可以自由地创建更有意义的继承层次结构)。
其中两个属性的 setters 中有逻辑,这些逻辑被替换为如下所示的部分方法
[SnippetINotifyPropertyChanged]
[SnippetPropertyINPC(property="SelectedSecondaryIndex", type="int", field="_selectedSecondaryIndex", defaultValue="1",
summary="The Index of the selected series in the secondary combo box.")]
[SnippetPropertyINPC(property="SelectedPrimaryIndex", type="int", field="_selectedPrimaryIndex", defaultValue="1",
summary="The Index of the selected series in the primary combo box.")]
[SnippetPropertyINPC(property="Behaviour", type="BehaviourManager", field="_behaviour",
summary="The Behaviour Manager which contains the trackball and the XAxisZoomBehaviour.")]
[SnippetPropertyINPC(property="XAxisVisible", type="bool", field="_xAxisVisibile", defaultValue="false",
summary="Whether this chart's X-Axis is visible.")]
[SnippetPropertyINPC(property="LivePrimaryChartDataSeries", type="DataSeries<DateTime, double>", field="_livePrimaryData",
summary="The Live Updating DataSeries that is always displayed on the chart's primary y-axis.")]
[SnippetPropertyINPC(property="LiveSecondaryChartDataSeries", type="DataSeries<DateTime, double>", field="_liveSecondaryData",
summary="The Live Updating DataSeries that is always displayed on the chart's secondary y-axis.")]
public partial class TelemetryChannelViewModel : INotifyPropertyChanged
{
#region partial methods
partial void OnSelectedSecondaryIndexChanged(int value)
{
ModifySecondaryChannel(ParentTelemetryViewModel.Channels[SelectedSecondaryIndex]);
}
partial void OnSelectedPrimaryIndexChanged(int value)
{
ModifyPrimaryChannel(ParentTelemetryViewModel.Channels[SelectedPrimaryIndex]);
}
#endregion
...
}
上述操作的净结果是消除了大部分无趣的样板代码,代码生成会创建一个相应的部分类,如下所示。生成的类包含 229 行代码,对于 13 行属性定义来说相当不错,当然对于将来的重构和维护也更好。
SilverTrack 还包含许多定义依赖属性的控件。WPF / Silverlight 的依赖属性语法非常冗长,这促使我创建了最初的非代码片段式代码生成方法。
下面是一个用于依赖属性的合适代码片段
<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>Defines a DependencyProperty</Title>
<Shortcut>DependencyProperty</Shortcut>
<Description>Defines a DependencyProperty</Description>
<Author>Colin Eberhardt</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>type</ID>
<ToolTip>Property Type</ToolTip>
<Default>string</Default>
</Literal>
<Literal>
<ID>summary</ID>
<ToolTip>Summary Documentation</ToolTip>
<Default>Gets / sets the property value</Default>
</Literal>
<Literal>
<ID>property</ID>
<ToolTip>Property Name</ToolTip>
<Default>MyProperty</Default>
</Literal>
<Literal>
<ID>containerType</ID>
<ToolTip>Containing type</ToolTip>
<Default>Control</Default>
</Literal>
<Literal>
<ID>defaultValue</ID>
<ToolTip>Property default value</ToolTip>
<Default>null</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[
/// <summary>
/// $summary$ This is a dependency property
/// </summary>
public $type$ $property$
{
get { return (double)GetValue($property$Property); }
set { SetValue($property$Property, value); }
}
/// <summary>
/// Defines the $property$ dependnecy property.
/// </summary>
public static readonly DependencyProperty $property$Property =
DependencyProperty.Register("$property$", typeof($type$), typeof($containerType$),
new PropertyMetadata($defaultValue$, new PropertyChangedCallback(On$property$PropertyChanged)));
/// <summary>
/// Invoked when the $property$ property changes
/// </summary>
partial void On$property$PropertyChanged(DependencyPropertyChangedEventArgs e);
private static void On$property$PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
$containerType$ control = d as $containerType$;
control.On$property$PropertyChanged(e);
}
$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
以 GForceControl
为例,该控件定义了两个依赖属性。现在可以用以下属性和在属性更改时调用的部分方法替换它们
[SnippetDependencyProperty(property = "Lateral", type = "double", containerType = "GForceControl",
summary = "Lateral G-Force", defaultValue = "0.0")]
[SnippetDependencyProperty(property = "Long", type = "double", containerType = "GForceControl",
summary = "Longitudinal G-Force", defaultValue = "0.0")]
public partial class GForceControl : Control
{
/// <summary>
/// Sets the gPoint's X Value to the Lateral Value, if gPoint is not null.
/// </summary>
partial void OnLateralPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (gPoint != null)
gPoint.X = Lateral;
}
/// <summary>
/// Sets the gPoint's Y Value to the Long Value, if gPoint is not null.
/// </summary>
partial void OnLongPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (gPoint != null)
gPoint.Y = Long;
}
...
}
依赖属性代码片段生成包含 DependencyObject
和 System.Windows
命名空间中其他类的代码。因此,包含添加到每个生成类顶部的代码的 Includes.tt
模板已更新,以包含这些命名空间
<#@ template language="C#" #>
<# // the following template outputs code that is added to the start of every generated file #>
using System.ComponentModel;
using Visiblox.Charts;
using System.Windows;
using System;
运行 T4 模板会生成一个包含依赖属性样板代码的 73 行生成类
结论
我花了大量时间来开发这项技术并编写这篇文章。它使用了有趣的技术组合,如 Linq、XML、Env.DTE 和 T4,创造了我认为真正非常有用的东西。样板代码是每个程序员的痛点,它在最初编写代码时会减慢我们的速度,会阻碍可读性,并且如果我们将来需要重构,会进一步减慢我们的速度。
在本文中,我说明了声明式代码生成方法如何消除与属性引发更改通知、依赖属性定义和 INotifyPropertyChanged
实现相关的所有样板代码。这为我们留下了一个简单的类功能声明。然而,这只是几个例子,还有更多的样板代码……大多数设计或架构模式都涉及一定程度的样板代码。
希望您觉得这篇文章既有趣又有用。即使您不使用它描述的代码片段自动化技术,也许您会发现 T4 + Env.DTE 这一非常有趣的技术组合有其他新颖的用途。
本文的源代码下载包含两个项目;第一个项目是一个最小化的示例,包含几个微不足道的类,以展示基本原理。第二个是 SilverTrack,其中该技术在实际应用程序中得到了更广泛的应用。