格式化 .NET 程序集摘要文档
使用 XSLT 创建 .NET 程序集的在线摘要文档
一些闲言碎语作为序言
我恐怕这篇文章不会揭示任何伟大的编程秘诀。我之所以发布它,是因为虽然市面上似乎有大量的类似工具,但它们要么需要花钱,要么虽然免费但功能相当受限,我想(希望)其他 .NET 开发人员会发现这个工具要么立即有用,要么是他们开发自己的(改进版)的一个好起点。
如果您对 XSLT 完全陌生,您可能会发现递归用法以及如何从代码向模板传递全局参数的演示很有用,否则这一切都相当普通。
背景
所有版本的 C# 以及更高版本的 VB.NET 都允许在类型、方法和属性上添加摘要注释块,并且这些摘要注释块可以在编译期间输出为 XML 文件(如果请求了文档文件生成)。可以通过 IDE(项目 / 属性)请求文档文件生成,或在命令行指定 /doc 标志。
将编译器生成的 XML 转换为符合本地标准的显示格式的工作留给了程序员。本文概述了使用 Framework 3.5 中可用的工具构建一个简单的摘要 HTML 文档生成器。
编译器在源代码中识别以下标签
Tag | 备注 |
<summary> |
对项目的简短描述。 |
<remarks> |
关于项目更详细的描述或额外评论。 |
<value> |
描述属性的 IO(输入/输出)。 |
<param> |
提供关于方法参数的注释。什么是有效的,什么无效,等等。 |
<returns> |
关于方法返回值的注释。 |
<exception cref="..."> |
列出方法、类型或属性抛出的异常。<exception cref="System.ArgumentException"> 当 Fred 为 null 时抛出。</exception> |
<example> |
此标签旨在包含与示例相关的文本。但是,由于我的“本地标准”是将一两行示例代码放在这里,而不是放在 <c> 和 <code> 元素中,因此此实用程序将此元素的内容格式化为代码而不是文本。 |
<code> |
用于多行代码示例。 |
<c> |
用于行内代码示例。 |
<see cref="{a.n.other_member}">...</see> |
用于行内交叉引用。 |
<seealso cref="{a.n.other_member}">...</seealso> |
用于单独的“请参阅”部分。 |
<list> |
用于项目符号列表。 |
提供的实用程序不处理上面列出的较少使用的元素,但它允许您在 <remarks>
部分嵌入简单的 HTML 格式,如下所示。
/// <summary>
/// Apply an XSL transform to a well formed XML string
/// returning the transform output as a string.
/// </summary>
/// <param name="xmlToTransform">A string containing well formed XML.</param>
/// <param name="xslTemplatePath">Fully specified XSLT file path</param>
/// <param name="paramList">A name, value dictionary of global template parameters. Can be empty but not null.</param>
/// <returns>A well formed XML string.</returns>
/// <example>
///string template = Server.MapPath(@"~/XSL/ToTypeHierarchyXML.xsl");
///string transformOutput = Library.XML.ApplyTransform(source, template, new Dictionary(string, string));
/// </example>
/// <exception cref="System.Xml.Xsl.XsltException"></exception>
/// <exception cref="System.Xml.XmlException">
/// Method rethrows any XML or XSLT exceptions it encounters.
/// </exception>
/// <remarks>
/// <ol type="1">
/// <li>The template file must exist and the process must have read access.</li>
/// <li>This and other methods are not intended for use with large XML documents.</li>
/// <li>Not intended for use with large XML documents.</li>
/// </ol>
/// </remarks>
public static string ApplyTransform(string xmlToTransform,
string xslTemplatePath,
Dictionary<string,string> paramList)
程序集的每个组件都由一个 <member>
块表示,方法和属性的所有权通过使用完全限定名而不是 XML 结构来显示。每个完全限定成员名都给出了一个单字母前缀以指示其分类。
成员前缀包括
前缀 | 组 | 注释 |
N | 命名空间 | |
T | 类型 | 包括类、结构、委托和枚举以及接口。 |
M | 方法 | |
P | 属性 | |
E | 事件 | |
F | 字段 | 此实用程序忽略。 |
下面的摘录来自编译器生成的 XML 文档文件。
<?xml version="1.0"?>
<doc>
<assembly>
<name>Documenter</name>
</assembly>
<members>
:
<member name="T:Documenter.Library.XML">
<summary>
Group XML appropriate methods.
</summary>
</member>
<member name="M:Documenter.Library.XML.FileTransform(System.String,System.String)">
<summary>
Apply a transform to a file returning a string
</summary>
<param name="filePath"></param>
<param name="xslTemplate"></param>
<returns></returns>
</member>
:
<member name="T:Documenter.Library.ForTesting.myHandler">
<summary>
Delegate : Here to generate an event member for test purposes.
</summary>
<param name="alpha">first parameter</param>
<param name="beta">second parameter</param>
<returns>True if method invocation succeeds.</returns>
</member>
:
</members>
</doc>
从上面的片段中,您会注意到
- 方法的返回值或属性返回值未包含,除非编码人员明确将其放入
<returns>
元素中。 - 类型的性质(类、结构、委托、枚举等)不可用。
- 成员的范围(public、private 等)不可用。
- 方法的参数类型给出为逗号分隔的字符串。
- 委托的参数类型不可用,仅有参数描述。
经过一些实验(以及不少咒骂)表明,编译器生成的这种“扁平”格式不适合直接使用 XSLT 1.0(Framework 3.5 不支持 XSLT 2.0)生成输出,因此文档生成过程分为两个步骤进行。
- 应用 XSLT 模板将扁平格式转换为更具层次结构的格式。
- 将第二个 XSLT 模板应用于转换后的 XML 以生成 HTML 页面。
使用的模板是
TypeSelect.xsl | 允许我们显示单个类型(类、结构等)的文档。 |
ToTypeHierarchyXML.xsl | 生成更具层次结构的格式。 |
ToHTML.xsl | 从类型层次结构转换生成的中间 XML 生成输出 HTML。 |
TypeSelect
这使用了一个简单的 for-each 来识别类型的 <member>
元素以及它可能包含的任何嵌套类型,然后使用这些元素生成“扁平”源 XML 的缩减副本。
ToTypeHierarchy
此模板有三个值得一提的部分
- 主扫描
- unadornedName
- toNodes
主扫描
编译器生成的 XML 的扁平特性意味着处理它的最简单方法是使用嵌套的 for-each 迭代器(注意;for-each 不是索引 for 循环)。这几乎肯定不是最快也不是最优雅的解决方案,但易于实现和理解。
这种方法的一个副作用是,如果所有者类型(类/接口/结构)没有摘要块,那么它的任何方法都不会被文档化。
unadornedName
成员(方法、属性、事件)名称最初是使用一个或多个 substring-after()
方法调用提取的,但非常非常偶尔,成员名称的前一两个字符会被剥离。非常不满意。
XSLT 1.0 中没有内置的字符串分隔符分割器,所以我们必须自己创建一个。复杂因素是 xsl:variables 是只写一次、多读的,并且没有等效的索引 for 循环。标准方法是使用递归。
此模板接收一个完全限定的成员名称,例如“M:Documenter.Library.Extensions.DefaultValue
”,并返回不带命名空间前缀的成员名称。术语“返回”的用法略有误导,最好将递归模板视为延迟将元素或属性写入输出流,直到达到所需的终点。
toNodes
此模板接收一个 CSV 格式的参数类型列表。与 unadornedName
不同,我们有兴趣在每个阶段写入输出流,而不仅仅是在最后。当遇到每个参数类型时,会写入一个 <param>
节点。如果输入字符串不为空,则会递归调用此模板。
一个有趣的(读作烦人的)细节是在后期发现的。
如果您有一个方法签名
public static string ApplyTransform(string xmlToTransform,
string xslTemplate,
Dictionary<string,string> paramList)
您最终会得到中间 XML 形式为
String,String,Dictionary{System.String,System.String}
因此,为了避免分割泛型类型的参数列表,有必要将“{
”和“}
”作为 toNodes 中的转义字符。结果是一个嵌套的 <xsl:choose>
结构来处理这种情况。
尽管如此,toNodes 和 unAdorned 在我看来都是非常可重用的。
类型层次结构输出 - 中间 XML
转换后的输出具有以下一般布局:
<assembly name="...">
<type name="...">
<typeHeader>
<summary> a summary comment</summary>
<!--
Delegates and other paramterised types will also have param,
value and returns elements.
-->
<param name="firstArg">The first argument.</param>
<param name="secondArg">The second argument.</param>
</typeHeader>
<!-- method comment -- >
<method name="..." paramTypes="...">
<summary>... </summary>
<paramType typeName="..." />
<paramType typeName="..." />
<param name="..." />
<param name="..." />
<returns>...</returns>
<remarks>...</remarks>
<example>...</example>
<exception cref="">...</exception>
</method>
<!-- property comment -- >
<property name="..." paramTypes="...">
<summary>... </summary>
<paramType typeName="..." />
<paramType typeName="..." />
<param name="..." />
<param name="..." />
<returns>...</returns>
<remarks>...</remarks>
<example>...</example>
<exception cref="">...</exception>
</property>
<!-- event comment -- >
<event name="..." paramTypes="...">
<summary>... </summary>
<paramType typeName="..." />
<paramType typeName="..." />
<param name="..." />
<param name="..." />
<returns>...</returns>
<remarks>...</remarks>
<example>...</example>
<exception cref="">...</exception>
</event>
<!-- T:Assembly.Namespace.Class.AType-->
<nestedType xref="Assembly.Namespace.Class.AType"
name="AType"
summary="A nested type (struct, class, enum, delegate)." />
</type>
</assembly>
方法示例
<type name="Library.Extensions">
<!-- M:Documenter.Library.Extensions.DefaultValue(System.String,System.String) -->
<method name="DefaultValue" paramTypes="System.String,System.String">
<summary>Deal with null strings.</summary>
<paramType typeName="System.String" />
<paramType typeName="System.String" />
<param name="s" />
<param name="defaultValue" />
</method>
注释
- 上面的示例中没有出现
<returns>
和<remarks>
元素,因为它们在方法的源 XML 中是空的。 - paramTypes 被保留为成员类型的 CSV 字符串属性,以便在需要时使用 XSLT 1.0 以单个字符串的形式显示参数类型。
<paramType>
和<param>
元素的分离。这主要有两个原因:- 逗号分隔的参数类型列表字符串始终是最新的。
- 创建后,源
<summary>
块中的<param>
节点不会在方法签名更改时自动更新,因此<param>
元素的数量可能少于(或多于)<paramType>
元素。 应该注意的是,GIGO(Garbage In, Garbage Out)适用于
<param>
节点。如果它们没有按参数列表顺序给出或缺失,生成的输出将反映这一点。
ToHTML
此模板只有一个重要部分:paramType
模板。
paramType
此模板负责匹配参数名称和参数类型。在此模板中,最重要的几行是:
<xsl:variable name="position" select="position()"/>
<xsl:variable name="paramName" select="../param[$position]/@name"/>
第一行记录了当前 <paramType>
节点在当前成员的 <paramType>
节点序列中的位置(基于 1 的索引)。下一行从当前 <paramType>
的父成员(方法等)的 <param>
节点序列中检索参数名称。如果成员的摘要块是最新的,则将存在 1:1 的对应关系。这种对应关系意味着我们可以通过 <paramType>
节点缺少匹配的 <param>
节点来检测新参数的添加,而无需更新成员的 <summary>
块。不幸的是,无法识别参数的删除或重命名。
还值得注意的是 position()
调用与节点访问的分离。使用单行
<xsl:variable name="paramName" select="../param[position()]/@name"/>
...导致为每个 <paramType>
检索 <param>
序列中的第一个节点,而不管 <paramType>
的索引位置。这是出乎意料的;position()
应该“报告上下文项在序列中的位置”。当用于索引 param[]
序列时,它似乎将上下文解释为 <param>
而不是 <paramType>
。通过在第一行将值检索为 $position
,我们确保使用了正确的上下文。
<!-- Lay out parameters where we have parameter types available. -->
<xsl:template match="paramType">
<span class="typeName">
<!-- Mark reference types with (out) -->
<xsl:choose>
<xsl:when test="contains(@typeName, '@')">
<xsl:value-of select="normalize-space(substring-before(@typeName,'@'))"/>
<xsl:value-of select="' (out) '"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="normalize-space(@typeName)"/>
<xsl:text disable-output-escaping="yes"> </xsl:text>
</xsl:otherwise>
</xsl:choose>
</span>
<xsl:variable name="position" select="position()"/>
<span class="parameterName">
<!-- If the summary block is up to date show the parameter name
otherwise note that the block is out of date. -->
<xsl:variable name="paramName" select="../param[$position]/@name"/>
<xsl:choose>
<xsl:when test="string-length($paramName) = 0">
<span class="remarks">{ Summary block needs updating. }</span>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="normalize-space($paramName)"/>
</xsl:otherwise>
</xsl:choose>
</span>
<!-- Write out any remarks for this parameter -->
<div class="indentedRemarks">
<xsl:value-of select="../param[$position]"/>
</div>
</xsl:template>
文档生成
一旦我们的模板创建了输出,那就再简单不过了...
void onGenerate(object sender, EventArgs e)
{
string templatePath = null;
Dictionary<string, string> searchParams = new Dictionary<string,string>();
string typeName = fqClassName.Text;
// Get the content of the document file.
HttpPostedFile f = FileUpload1.PostedFile;
byte[] buffer = new byte[f.InputStream.Length];
f.InputStream.Read(buffer, 0, buffer.Length);
System.Text.Encoding enc = new System.Text.UTF8Encoding();
string documentation = enc.GetString(buffer).Trim();
if (string.IsNullOrEmpty(documentation))
Response.Write(@"Couldn't upload the XML. Try again.");
else
{
// If we're only interested in one type then extract it and its constituents
// into a mini version of the source XML.
if (!string.IsNullOrEmpty(typeName))
{
searchParams.Add("typeNameSought", typeName);
templatePath = Server.MapPath(@"~/XSL/SelectType.xsl");
documentation = Library.XML.ApplyTransform(documentation,
templatePath,
searchParams);
}
// Now turn the flattish compiler output into something
// with a bit more of a hierarchy about it then...
templatePath = Server.MapPath(@"~/XSL/ToTypeHierarchyXML.xsl");
documentation = Library.XML.ApplyTransform(documentation,
templatePath,
new Dictionary<string, string>());
// ...turn the hierarchical XML into HTML before...
templatePath = Server.MapPath(@"~/XSL/ToHTML.xsl");
documentation = Library.XML.ApplyTransform(documentation,
templatePath,
new Dictionary<string, string>());
// ...pushing it back to the user.
Response.Write(documentation);
}
Response.End();
}
注释
如果源 XML 中在开头的 <XML ... > 标签之前存在非打印字符,则在尝试转换时将抛出无效 XML 异常。VB.NET 似乎就存在这种情况。
Library.XML
类只是对一些标准的 .NET CompiledTransform
调用进行了封装,MSDN 对它们的用法有很好的解释。
从浏览器将转换运行到网页具有多种优势:
- 无需每个人都拥有本地副本的实用程序。
- 如果本地文档标准发生变化,每个人都会自动使用最新版本的模板。
- 印刷品的诱惑或需求大大减少。
- 文档始终与上次编译器运行一样新。
使用该实用工具

- 浏览所需的文档文件。
- 如果您对特定类型感兴趣,请输入其完全限定名,否则留空以获取类中的所有类型。
- 点击 [生成] 按钮。
- 阅读输出...

那些黄色的标题栏?啊,BeOS。那是一个真正的操作系统...
兴趣点
我偶然发现了一些可能值得指出的观点,如果您是 XSL 的新手。
不要害怕使用 <xsl:variable>
,这样做可能不是好的风格,但它们可以使事情的阅读(和编写)变得容易得多,尤其是当您有深度嵌套的字符串函数调用时。
如果字符串不包含分隔符字符串或字符,substring-before 会返回一个空字符串。这没有帮助。有几种方法可以解决这个问题,我两种都用过。要么使用 <xsl:choose>
块,请参阅 unAdorned name 中的示例。Choose 块有点冗长。更直接的方法是使用 concat() 来确保找到了分隔符。
substring-before( concat( @name, '(' ) , '(' )
使用 substring-after 也可以采用类似的方法... substring-after( concat( '(', @name ) , '(' )
问题
- 为什么类 XXYZ 或方法 HH32A 没有显示在生成的文档中?
- 因为编写 XXYZ 和 HH32A 代码的人是个懒惰的家伙,没有为它们包含摘要块。
- 有一个 bug。
- 为什么这个实用程序不为字段生成文档?
- 因为大多数时候这会使输出变得杂乱无章,超出使用的范围。此实用程序的目的是让程序员尽快了解程序集。请参阅下一条...
- 我为什么要费劲做这个,而不是直接去看源代码?确实可以,但请考虑以下几点:
- 您正在冷启动一个庞大的现有项目,并且期望在几小时内而不是几周内就能开始生产。像这样的实用程序生成的摘要文档提供了对每个类功能的易读概述,以及程序集内各个类之间的连接方式,并且由于参数类型是完全限定的,因此跨程序集也有连接。在脑海中整理这些信息可能需要大量的代码探索。
- 您正在进行一个大型项目的中期阶段(没有因表现良好而获得假释),并且遇到了需要新库方法的情况。如果您有摘要文档,那么检查是否存在执行所需操作的方法会容易得多。
- 您正在完成一个主要的新模块,或者要换一份新工作(或者,更好的是,您继承了希尔达姑妈的大笔遗产,不再需要为代码矿工辛勤工作),并且被您的...要求为同事制作一份交接指南。
[ ] Project Manager [ ] Team Leader, [ ] Evil Overlord (tick all that apply)
不用担心。您可以直接将 PM、TL 或 EO 指向此类实用程序。再加上 IDE 中的一些类图放入您最喜欢的文字处理器中,就能很好地满足这一要求。
- 您是项目中的 PM、TL 或 EO,并且刚刚失去了一位长期工作的团队成员,并且需要在整个项目崩溃之前让您剩余的
废物珍贵员工了解他或她的专业领域。 - 您是一名 CMMI 审计员(嘘!嘘!),并且想要执行本地编码标准
猎巫检查。当然,以上所有内容仅在编码人员实际编写有意义的摘要块注释时适用。嗨!
历史
- 2013 年 2 月 - 添加类型选择。
- 2012 年 2 月 - 初版。