產品上市以前須要詳細的幫助文檔,每一個程序員寫各自負責的部分,爲了統一格式和減輕工做量,決定用程序實現。文檔生成方便一直很出名的就是sandcastle,但他的格式不是想要的。因而就在sandcastle的基礎上進行改造。html
需求的最終結果是這個樣子:node
1、基本原理程序員
主要針對二次開發的用戶使用,簡單明瞭。右邊最多分五個塊:概要,定義,註釋,參數和示例。web
DocumentGenerator原理圖以下:app
如上圖所示,咱們在VS上編譯生成以後,一個dll就會對應一個xml文件(固然前提是你最好本身寫了一些註釋,用GhostDoc很方便),咱們將一個dll和對應的xml都加載進DocumentGenerator,DocumentGenerator有用Razor定義好的模板也就是所謂的參考主題,而根據dll和xml會解析出來summary,remarks,returns,define,example這五個部分。在編輯的地方,支持在線編輯,否則直接編輯xml文檔,很麻煩很累,在web中根據樹節點展開這樣每一個寫文檔的人只用「填空」就好了,不用去關心哪些標籤和格式。最後一鍵生成,速度很快。那具體xml文檔是怎麼對應的,以下圖:ide
而後再將這些html,目錄文件經過hhc編譯器生成了chm文檔。函數
2、一些細節ui
咱們獨立出來一個MemberDocumentApplet對象,提供,onload,onsave,OnGenerateChm 等方法,並且包含了一個樹對象和MemberDocument集合,前者用來導航,後者用來呈現一個方法或者一個屬性它的註釋及示例。不管是web仍是winform,經過這個applet對象均可以進行加載、修改報錯及生成的動做。this
1.OnLoad()lua
加載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; }
3、Web部分
這個時候web就只是一種表現形式了,由於web不一樣於winform,不能直接獲取用戶電腦上的文件,也不能直接將文件生成到用戶電腦上。因此就要想上傳,編輯生成以後下載下來。
上傳以後,點擊編輯文檔
這樣每一個人均可以在線編輯本身負責的部分,而後生成文檔下載下來給用戶使用。
PS:下載下來的chm文檔可能打不開,須要在屬性裏面解除鎖定。由於一些緣由,暫時不方便公開源碼,只能分享下實現思路。