为 .NET 程序集创建帮助生成器
Doxie 入门并学习构建自己的
Doxie - .NET 程序集帮助生成器
引言
您辛苦开发了一个开源项目(甚至是商业项目),到了发布的时候,却发现又到了令人头疼的编写代码文档的时候了。遍历每个公共类型、构造函数、方法和属性来编写 XML 注释本身就已经是一项艰巨的任务,然后您还需要将其转换为 HTML 以便在网站上显示并进行样式设置。您可以选择使用 XSL,或者花费数百美元购买昂贵的商业解决方案,如果您需要大量功能,这都是可以的。但如果您预算紧张,尤其是如果您正在开发一个开源项目,那么快速简单的解决方案也许就足够了。Doxie 可能不是最优雅的解决方案,但它确实能完成工作。;-)
演示
您可以在我为另一个名为“Extenso”的项目部署到 GitHub Pages 的工作演示中找到它:https://gordon-matt.github.io/Extenso/。
入门
- 安装 NodeJS
- 全局安装 JSPM:
npm install -g jspm
- 克隆/下载项目
- 恢复 JSPM 包:
jspm install
注意:请在“Doxie”项目的根目录下执行此操作(而不是解决方案的根目录)。
进一步注意:我选择 JSPM 作为默认的构建系统,因为它易于使用。如果您愿意,可以将其替换为 webpack。有关如何操作,请参阅 Aurelia 的文档。
生成帮助文件
Aurelia Web 应用程序只是一个读取 JSON 文件作为其数据的 SPA。我们需要使用提供的“帮助文件生成器”之一来读取您的程序集并生成此 JSON 文件。提供了两个生成器 - 一个是 WinForms 应用程序,允许您选择包含程序集的文件夹以及为其中哪些程序集生成帮助。另一个是控制台应用程序,功能相同,但以防您在 Linux 或 Mac 上运行 - 只需修改 Program.cs 中的路径并运行它。
使用 WinForms 生成器创建帮助文件
要使用帮助文件生成器的 Winforms 版本创建文件,请按照以下步骤操作
- 启动应用程序
- 单击“浏览”按钮并选择包含 .NET 程序集的文件夹
- 选中要为其生成文档的程序集复选框
- 单击“确定”
- 片刻之后,您应该会收到一条成功消息。
生成文件后,您只需将其放置在 Doxie 的 js 文件夹中。
自定义
部署前,请随意修改网站。一些建议
- 更改页脚中的版权文本
- 更改 GitHub 标签中的 URL
- 使用不同的 Bootstrap 主题(请参阅 https://cdnjs.com/libraries/bootswatch)
您甚至可以向 Aurelia 网站添加更多页面,这样生成的帮助文档将只是其中的一部分。只需将帮助内容移到一个新页面,而不是主页,然后创建一个新的主页、关于页面、联系页面,随您喜欢。然后,您只需在需要更新文档时丢弃一个新的 JSON 帮助文件即可。
.NET Core 程序集
对于 .NET Core 程序集,您需要确保所有相关的程序集都与您要从中生成页面的程序集位于同一位置。否则,生成的文档将包含由 FileNotFoundException
引起的错误消息。弄清楚需要复制哪些程序集可能会很麻烦,因此有一个简单的小技巧可以使此过程非常容易
- 向您的解决方案添加一个新项目,并将其命名为
DoxieDummy
或您喜欢的任何名称。 - 引用解决方案中所有您想要生成文档的项目。
- 现在是重要部分:编辑此虚拟项目的 .csproj 文件并添加以下内容:
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
。这将确保所有程序集都被复制到输出目录。下面是一个截图示例 - 现在,您只需要将该目录路径传递给“帮助文件生成器”之一,并告知它应记录其中哪些程序集。
文档生成工作原理
文档生成包含 2 个部分
- 解析 XML 文件,并将其与从程序集及其类型反射获得的数据合并
- 将内存中的数据序列化为 JSON 文件
解析 XML 文件
解析 XML 有多种方法。一种选择是构建一些类来表示各种元素,用必需的属性修饰这些属性,然后使用 XML 序列化将文件内容反序列化到对象中。我最初的解决方案如下
[XmlRoot("doc")]
public class DocCommentsFile
{
public DocCommentsFile()
{
Members = new List<MemberElement>();
}
[XmlElement("assembly")]
public AssemblyElement Assembly { get; set; }
[XmlArray("members")]
[XmlArrayItem("member")]
public List<MemberElement> Members { get; set; }
public static DocCommentsFile Open(string path)
{
string text = File.ReadAllText(path);
return text.XmlDeserialize<DocCommentsFile>();
//return new FileInfo(path).XmlDeserialize<DocCommentsFile>();
}
}
public class AssemblyElement
{
[XmlElement("name")]
public string Name { get; set; }
}
public class MemberElement
{
public MemberElement()
{
Params = new List<ParamElement>();
TypeParams = new List<ParamElement>();
Exceptions = new List<ExceptionElement>();
}
[XmlAttribute("name")]
public string Name { get; set; }
[XmlElement("summary")]
public SummaryElement Summary { get; set; }
[XmlElement("example")]
public string Example { get; set; }
[XmlElement("param")]
public List<ParamElement> Params { get; set; }
[XmlElement("returns")]
public ReturnsElement Returns { get; set; }
[XmlElement("remarks")]
public string Remarks { get; set; }
[XmlElement("typeparam")]
public List<ParamElement> TypeParams { get; set; }
[XmlElement("exception")]
public List<ExceptionElement> Exceptions { get; set; }
}
public class SummaryElement
{
[XmlElement("see")]
public SeeElement See { get; set; }
}
public class ParamElement : IXmlSerializable
{
[XmlAttribute("name")]
public string Name { get; set; }
[XmlText]
public string Description { get; set; }
#region IXmlSerializable Members
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
Description = reader.ReadInnerXml();
}
public void WriteXml(XmlWriter writer)
{
}
#endregion IXmlSerializable Members
}
public class SeeElement
{
[XmlAttribute("cref")]
public string CRef { get; set; }
}
public class TypeParamElement
{
[XmlAttribute("name")]
public string Name { get; set; }
[XmlText]
public string Description { get; set; }
}
public class ExceptionElement : IXmlSerializable
{
[XmlAttribute("cref")]
public string CRef { get; set; }
[XmlText]
public string Description { get; set; }
#region IXmlSerializable Members
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
Description = reader.ReadInnerXml();
}
public void WriteXml(XmlWriter writer)
{
}
#endregion IXmlSerializable Members
}
public class ReturnsElement : IXmlSerializable
{
[XmlText]
public string Description { get; set; }
#region IXmlSerializable Members
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
Description = reader.ReadInnerXml();
}
public void WriteXml(XmlWriter writer)
{
}
#endregion IXmlSerializable Members
}
这在一定程度上可行,但我确实遇到了各种问题,因为文本内容中嵌套了其他元素。例如,<see>
元素。我尝试通过在某些元素上实现 IXmlSerializable
来处理这个问题,如上面的代码所示。但是,感觉有点“hacky”,并且我担心类似的场景。因此,在尝试处理所有这些问题之前,我决定寻找现有的解决方案。
我偶然发现了一个名为 NuDoq 的项目,并尝试使用它,但它不太适合我 - 我只想在内存中表示 XML 文件。NuDoq 使用访问者模式,将来可能对我很有用,但它只是不适合我这个项目的需求。
于是我又找了找,这时我发现了 AutoHelp 项目,它使用了名为 Jolt.NET 的旧项目。那里的解决方案是使用 XSD 文件。显然,XML 注释文件没有官方的 XSD 模式,第三方也很少见。Jolt.NET 的实现不如我见过的一些全面,但它不会抛出任何错误,因为它不那么严格。实现如下
<?xml version="1.0" encoding="utf-8"?>
<!-- *
* This schema contains an unofficial representation of the Visual Studio XML Doc Comment file.
* The schema is a contains a subset of the elements that are allowed in a doc comment file,
* and is restricted to representing the structure
* of the <doc>, <assembly>, <members> and <member>
* elements. The child elements of <member> are not represented in this schema.
*
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="unqualified">
<xs:simpleType name="NameType">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="AssemblyType">
<xs:sequence>
<xs:element name="name" type="NameType"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="MemberType" mixed="true">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
</xs:sequence>
<xs:attribute name="name" type="NameType"/>
</xs:complexType>
<xs:complexType name="MemberCollectionType">
<xs:sequence>
<xs:element name="member" type="MemberType" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="DocumentType">
<xs:sequence>
<xs:element name="assembly" type="AssemblyType"/>
<xs:element name="members" type="MemberCollectionType"/>
</xs:sequence>
</xs:complexType>
<xs:element name="doc" type="DocumentType"/>
</xs:schema>
然后,这被用于 Jolt 的 XmlDocCommentReadPolicy
和 XmlDocCommentReader
来解析 XML。最后一步是有一个类来使用前面提到的 XmlDocCommentReader
将 XML 注释读取到对象中。我在这里不赘述所有细节,因为您可以自己获取代码并查看 Doxie.Core
项目中的 DocParser.cs。但本质上,它读取 XML 注释文件,并将其与从程序集及其类型反射获得的数据合并。
将数据序列化为 JSON 格式文件
此过程的最后一步是使用 DocParser
获取所有选定程序集的数据,并将结果序列化为 JSON 格式文件。我的实现如下
public static class JsonHelpFileGenerator
{
private static DocParser docParser = new DocParser();
public static void Generate(IEnumerable<string> selectedAssemblyPaths, string outputPath)
{
var assemblies = GetAssemblies(selectedAssemblyPaths);
string outputFileName = Path.Combine(outputPath, "assemblies.json");
assemblies.ToJson().ToFile(outputFileName);
}
private static IEnumerable<AssemblyModel> GetAssemblies(IEnumerable<string> selectedAssemblyPaths)
{
return selectedAssemblyPaths.Select(filePath => GetAssembly(filePath)).ToArray();
}
private static AssemblyModel GetAssembly(string filePath)
{
var assembly = docParser.Parse(filePath);
assembly.FileName = filePath;
return assembly;
}
}
注意:上面代码示例中的
ToJson()
和ToFile(string)
方法是 Extenso 项目中的扩展方法。
基本上就是这样。一旦您拥有了所有这些组件,您只需要创建一个工具来接收要传递给 JsonHelpFileGenerator.Generate(IEnumerable<string> selectedAssemblyPaths, string outputPath)
的参数。Doxie 为此提供了 GUI 版本和控制台应用程序。
捆绑
Doxie 的默认设置可能有点慢。您可以通过启用 JSPM 捆绑来改进这一点。我在 packages.json 和 gulpfile.js 中添加了必要的配置。您只需要运行 gulp bundle.
注意:Gulp 往往会抱怨 config.js 中的 baseURL 被设置为
location.pathname
。您可以暂时(或永久地,如果您愿意)将其更改为您需要的硬编码路径。在许多情况下,这可能只是 /,而对于 GitHub Pages,它通常是 /[Your Project Name]/。然后 Gulp 任务应该可以正常运行。
进一步注意:如“入门”部分所述,您也可以将 JSPM 替换为 webpack。
致谢
用于读取 XML 注释文件的代码和 XSD 模式来自一个名为 Jolt.NET 的旧项目。原始源代码可以在 这里找到,RedGate 在 GitHub 上创建了自己的分支 这里,其中包含一个重要的错误修复。
至于 UI 和整体思路,我受到了 AutoHelp 的启发,但完全重写了它以使用 Aurelia,并且我还决定最好生成一个 JSON 文件来读取,而不是依赖 MVC 控制器操作来获取数据。这样,它就可以轻松地在 GitHub Pages 中使用。
限制
- XML 文件必须与 DLL 文件一起存在。这就是重点,但在某些情况下,可能有一两个程序集您尚未为其生成 XML 文档。在这种情况下,如果 Doxie 仍然可以读取类型并生成部分信息而无需 XML 注释,那就很好了。这将在未来完成。
- 生成器不知道如何处理某些 XML 元素(例如:
<see>
),因此它会忽略它们。这可能会在未来得到修复。