产品上市之前需要详细的帮助文档,每个程序员写各自负责的部分,为了统一格式和减轻工作量,决定用程序实现。文档生成方便一直很出名的就是sandcastle,但他的格式不是想要的。于是就在sandcastle的基础上进行改造。
需求的最终结果是这个样子:
一、基本原理
主要针对二次开发的用户使用,简单明了。右边最多分五个块:概要,定义,注释,参数和示例。
DocumentGenerator原理图如下:
如上图所示,我们在VS上编译生成之后,一个dll就会对应一个xml文件(当然前提是你最好自己写了一些注释,用GhostDoc很方便),我们将一个dll和对应的xml都加载进DocumentGenerator,DocumentGenerator有用Razor定义好的模板也就是所谓的参考主题,而根据dll和xml会解析出来summary,remarks,returns,define,example这五个部分。在编辑的地方,支持在线编辑,不然直接编辑xml文档,很麻烦很累,在web中根据树节点展开这样每个写文档的人只用“填空”就行了,不用去关心哪些标签和格式。最后一键生成,速度很快。那具体xml文档是怎么对应的,如下图:
然后再将这些html,目录文件通过hhc编译器生成了chm文档。
二、一些细节
我们独立出来一个MemberDocumentApplet对象,提供,onload,onsave,OnGenerateChm 等方法,而且包含了一个树对象和MemberDocument集合,前者用来导航,后者用来呈现一个方法或者一个属性它的注释及示例。无论是web还是winform,通过这个applet对象都可以进行加载、修改报错及生成的动作。
1.OnLoad()
加载xml文档后,先会备份一个XXname_DBfile,
_bakFileName = Path.GetDirectoryName(xmlFile) + Path.GetFileNameWithoutExtension(xmlFile) + "_DBFile.xml";
修改的时候是修改这个文件,这样做的目的是为了保留用户修改的内容,如果一个用户已经在DocumentGenerator中编辑了一部分突然发现又要加一些方法,于是用vs修改源文件后生成了新的xml和dll,这样子再加载到DocumentGenerator中的时候上次填写的内容都还在,他只需要完善那些新的方法就可以生成文档,而不用全部再写一遍。
public bool OnLoad(string xmlFile, string dllFile) { if (!File.Exists(xmlFile)) return false; _xmlComment = new XmlCommentsFile(xmlFile); _rootNode = new AssemblyNode("N:" + _xmlComment.AssemblyName); _bakFileName = Path.GetDirectoryName(xmlFile) + Path.GetFileNameWithoutExtension(xmlFile) + "_DBFile.xml"; //加载dll文件 var assemble = Assembly.LoadFrom(dllFile); foreach (var type in assemble.GetTypes()) { var methodRootNode = new MemberTypeNode("方法", MemberTypes.Method);//, new MemberDocumentCollection() var propertyRootNode = new MemberTypeNode("属性",MemberTypes.Property); var eventRootNode = new MemberTypeNode("事件",MemberTypes.Event); var properties = type.GetProperties(); foreach (var memberInfo in type.GetMembers(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) { #region 过滤 //过滤get set var p1 = properties.Count(t => t.Name == AdjustMethodName(memberInfo.Name)); if (p1 > 0) continue; //过滤不包含ScriptVisible的项 var attribute = memberInfo.GetCustomAttributes(typeof(ScriptVisibleAttribute), false); if (attribute.Length <= 0) continue; #endregion string mKey = ConsummateKeyValue(memberInfo); var paramTypeList = GetMethodParamList(memberInfo as MethodInfo); var paramOptionalList = GetMethodParamOptionalStatusList(memberInfo as MethodInfo); var defineStr = GetMethodDefineStr(memberInfo as MethodInfo); var fullname = memberInfo.ReflectedType.FullName.Substring(memberInfo.ReflectedType.FullName.LastIndexOf(".", StringComparison.Ordinal) + 1) + "." + memberInfo.Name; var memberDoc = new MemberDocument(mKey, _xmlComment[mKey], defineStr, paramOptionalList, paramTypeList, fullname); _memberDocumentCollection.Add(memberDoc);//_memberDocumentCollection 初始化 switch (memberInfo.MemberType) { case MemberTypes.Method: methodRootNode.MemberDocumentGroup.Add(memberDoc); break; case MemberTypes.Property: propertyRootNode.MemberDocumentGroup.Add(memberDoc); break; case MemberTypes.Event: eventRootNode.MemberDocumentGroup.Add(memberDoc); break; } } //_rootMemberDic初始化 string classKey = "T:" + type.FullName; var classNode = new ClassNode(classKey, _xmlComment[classKey], "", null,null, ""); if (methodRootNode.MemberDocumentGroup.Count > 0) classNode.MemberTypeNodeList.Add(methodRootNode); if(propertyRootNode.MemberDocumentGroup.Count > 0) classNode.MemberTypeNodeList.Add(propertyRootNode); if(eventRootNode.MemberDocumentGroup.Count>0) classNode.MemberTypeNodeList.Add(eventRootNode); _rootNode.ClassNodeGroup.Add(classNode); _memberDocumentCollection.Add(new MemberDocument { Define = classNode.Define, Example = classNode.Example!=null?new ExampleSection(classNode.Example.Name,classNode.Example.Code):null, FullName = classNode.FullName, Name = classNode.Name, ParamList = new List<ParamSection>(classNode.ParamList), Remarks = classNode.Remarks, Returns = classNode.Returns, Summary = classNode.Summary }); } //var assemble = Assembly.LoadFrom(dllFile); //合并 if (File.Exists(_bakFileName)) { //合并 _memberDocumentCollection 和 bakFileName文件 var bakList = DeserializeMemberDocument(_bakFileName); foreach (var document in bakList) { if (_memberDocumentCollection.ContainsKey(document.Name)) { if (String.CompareOrdinal(_memberDocumentCollection[document.Name].Summary, document.Summary) != 0) _memberDocumentCollection[document.Name].Summary = document.Summary; if (String.CompareOrdinal(_memberDocumentCollection[document.Name].Remarks, document.Remarks) != 0) _memberDocumentCollection[document.Name].Remarks = document.Remarks; if (String.CompareOrdinal(_memberDocumentCollection[document.Name].Returns, document.Returns) != 0) _memberDocumentCollection[document.Name].Returns = document.Returns; if (document.Example != null && _memberDocumentCollection[document.Name].Example != null) { if (String.CompareOrdinal(_memberDocumentCollection[document.Name].Example.Code, document.Example.Code) != 0) _memberDocumentCollection[document.Name].Example.Code = document.Example.Code; if (String.CompareOrdinal(_memberDocumentCollection[document.Name].Example.Name, document.Example.Name) != 0) _memberDocumentCollection[document.Name].Example.Name = document.Example.Name; } else { if (_memberDocumentCollection[document.Name].Example == null && document.Example != null) { _memberDocumentCollection[document.Name].Example = new ExampleSection(document.Example.Name, document.Example.Code); } } if (document.ParamList != null) { for (int i = 0; i < document.ParamList.Count; i++) { if (String.CompareOrdinal(_memberDocumentCollection[document.Name].ParamList[i].Content,document.ParamList[i].Content) != 0) _memberDocumentCollection[document.Name].ParamList[i].Content = document.ParamList[i].Content; } } } else { _memberDocumentCollection.Add(new MemberDocument() { Define = document.Define, Example = document.Example, FullName = document.FullName, Name = document.Name, ParamList = new List<ParamSection>(document.ParamList), Remarks = document.Remarks, Returns = document.Remarks, Summary = document.Summary }); } } } return true; }
另外一点,需要生成文档的对象不会所有的类和方法,我们只需要其中的一部分,于是就加了一个ScriptVisibleAttribute标签过滤。意即带有这个标签的方法才会生成在文档中。
var attribute = memberInfo.GetCustomAttributes(typeof(ScriptVisibleAttribute), false);
最小可编辑的对象就是MemberDocument,包含了sumary,remarks returns,param,example等部分。
using System; using System.Collections.Generic; using System.Xml; namespace HelpFileExtract { [Serializable] public class MemberDocument { public MemberDocument() { } public MemberDocument(string name, XmlNode member, string definedName, List<bool> optionalList, List<string> paramTypeList, string fullName) { this.Name = name; this.Summary = string.Empty; this.Remarks = string.Empty; this.Example = null; this.Define = definedName; this.Returns = string.Empty; this.FullName = fullName; if (member == null) return; //从xml文件中获得参数个数和参数名 var paramNodes = member.SelectNodes("param"); //合并 if (paramNodes != null && (paramNodes.Count > 0 && paramTypeList != null && paramNodes.Count == paramTypeList.Count)) { int i = 0; foreach (XmlNode param in paramNodes) { if (param.Attributes != null) this.ParamList.Add(new ParamSection(param.Attributes["name"].Value, optionalList[i], paramTypeList[i], param.InnerText)); i++; } } if (member["summary"] != null) this.Summary = member["summary"].InnerText; if (member["remarks"] != null) this.Remarks = member["remarks"].InnerText; if (member["returns"] != null) this.Returns = member["returns"].InnerText; if (member["example"] == null) return; if (member["example"]["code"] != null) { var node = member["example"]["code"]; var value = node.InnerText; member["example"].RemoveChild(node); Example = new ExampleSection( member["example"].InnerText, value); } } public string Name { get; set; } public string Summary { get; set; } public string Define { get; set; } public List<ParamSection> ParamList = new List<ParamSection>(); public string Remarks { get; set; } public ExampleSection Example { get; set; } public string Returns { get; set; } public string FullName { get; set; } } }
所有这些对象都在集合_memberDocumentCollection中。而MemberTypes有 构造函数、事件、字段、属性等8个部分
// 摘要: // 标记每个已定义为 MemberInfo 的派生类的成员类型。 [Serializable] [ComVisible(true)] [Flags] public enum MemberTypes { // 摘要: // 指定该成员是一个构造函数,表示 System.Reflection.ConstructorInfo 成员。 0x01 的十六进制值。 Constructor = 1, // // 摘要: // 指定该成员是一个事件,表示 System.Reflection.EventInfo 成员。 0x02 的十六进制值。 Event = 2, // // 摘要: // 指定该成员是一个字段,表示 System.Reflection.FieldInfo 成员。 0x04 的十六进制值。 Field = 4, // // 摘要: // 指定该成员是一个方法,表示 System.Reflection.MethodInfo 成员。 0x08 的十六进制值。 Method = 8, // // 摘要: // 指定该成员是一个属性,表示 System.Reflection.PropertyInfo 成员。 0x10 的十六进制值。 Property = 16, // // 摘要: // 指定该成员是一种类型,表示 System.Reflection.MemberTypes.TypeInfo 成员。 0x20 的十六进制值。 TypeInfo = 32, // // 摘要: // 指定该成员是一个自定义成员类型。 0x40 的十六进制值。 Custom = 64, // // 摘要: // 指定该成员是一个嵌套类型,可扩展 System.Reflection.MemberInfo。 NestedType = 128, // // 摘要: // 指定所有成员类型。 All = 191, }
2.Onsave
public void OnSave() { //保存 _memberDocumentCollection 为 bakFileName文件 SerializeMemberDocument(MemberDocumentCollection.CollectionList, _bakFileName); } private void SerializeMemberDocument( List<MemberDocument> memberList,string xmlFileName ) { var xd = new XmlDocument(); using (var sw = new StringWriter()) { var xz = new XmlSerializer(typeof(List<MemberDocument>));//memberList.GetType() xz.Serialize(sw, memberList); //Console.WriteLine(sw.ToString()); xd.LoadXml(sw.ToString()); xd.Save(xmlFileName); } }
Save的时候将集合的中的list写入到xml中。
3.OnGenerateChm
public string OnGenerateChm(string title,string fileName,string path="") { return DoucmentGenerator.ExtractHelpFile(this, title, fileName,path); }
ExtractHelpFile 需要生成的对象越多,时间越久。这里就是参考了Sandcastle的源码,重新组织了模板。
部分核心代码:
public static string ExtractHelpFile(MemberDocumentApplet memberDocumentApplet, string title, string fileName,string path = "")//string dllFullPath, string xmlFullPath { string myDocumentPath = String.IsNullOrEmpty(path)? Environment.GetFolderPath(Environment.SpecialFolder.Desktop):path; string outputFileName = myDocumentPath + "\\Help\\" + fileName + ".chm";//此处顺序不能调整 if (File.Exists(outputFileName)) File.Delete(outputFileName); htmlFolder = myDocumentPath + "\\Help\\Working\\Output\\HtmlHelp1\\html\\"; workingFolder = myDocumentPath + "\\Help\\Working\\"; outputFolder = myDocumentPath + "\\Help\\"; help1Folder = myDocumentPath + "\\Help\\Working\\Output\\HtmlHelp1\\"; helpName = fileName; helpTitle = title; _memberDocumentApplet = memberDocumentApplet; if (fieldMatchEval == null)fieldMatchEval = new MatchEvaluator(OnFieldMatch); //创建临时工作区 if (Directory.Exists(workingFolder)) Directory.Delete(workingFolder, true); CopyDirectory(workingSourcePath, outputFolder); //通过反射得到toc.xml文件 tocFile = GenerateIntermediateToc(); if (!File.Exists(tocFile)) return string.Empty; //根据toc.xml生成htm文件 GenerateHtmFiles(); //根据toc.xml生成hhc WriteHelp1XTableOfContents(); //GenerateHHC(); GenerateHHK(); //根据toc.xml生成hhk WriteHelp1XKeywordIndex(); //生成编译配置文件hhp TransformTemplate("Help1x.hhp", templatePath, workingFolder); //生成编译工程文件*.proj TransformTemplate("Build1xHelpFile.proj", templatePath, workingFolder); //用MSBuild编译生成chm RunProcess(msBuildPath, "Build1xHelpFile.proj"); //删除临时工作区 if (Directory.Exists(workingFolder)) Directory.Delete(workingFolder, true); if (File.Exists(outputFileName)) return outputFileName; return string.Empty; }
三、Web部分
这个时候web就只是一种表现形式了,因为web不同于winform,不能直接获取用户电脑上的文件,也不能直接将文件生成到用户电脑上。所以就要想上传,编辑生成之后下载下来。
上传之后,点击编辑文档
这样每个人都可以在线编辑自己负责的部分,然后生成文档下载下来给用户使用。
PS:下载下来的chm文档可能打不开,需要在属性里面解除锁定。因为一些原因,暂时不方便公开源码,只能分享下实现思路。