• 注释文档在线编辑及生成


         产品上市之前需要详细的帮助文档,每个程序员写各自负责的部分,为了统一格式和减轻工作量,决定用程序实现。文档生成方便一直很出名的就是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;
            }
    View Code

     另外一点,需要生成文档的对象不会所有的类和方法,我们只需要其中的一部分,于是就加了一个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; }
        }
    }
    View Code

    所有这些对象都在集合_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,
        }
    View Code

    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);
                }
            }
    View Code

    Save的时候将集合的中的list写入到xml中。

    3.OnGenerateChm

      public string OnGenerateChm(string title,string fileName,string path="")
            {
                return DoucmentGenerator.ExtractHelpFile(this, title, fileName,path);
            }
    View Code

    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;
            }
    View Code

    三、Web部分

    这个时候web就只是一种表现形式了,因为web不同于winform,不能直接获取用户电脑上的文件,也不能直接将文件生成到用户电脑上。所以就要想上传,编辑生成之后下载下来。

     上传之后,点击编辑文档

      

       这样每个人都可以在线编辑自己负责的部分,然后生成文档下载下来给用户使用。

      PS:下载下来的chm文档可能打不开,需要在属性里面解除锁定。因为一些原因,暂时不方便公开源码,只能分享下实现思路。

  • 相关阅读:
    日志框架之Slf4j整合Logback
    使用SLF4J和Logback
    Java日志框架SLF4J和log4j以及logback的联系和区别
    docker部署apollo
    mysql8.0设置忽略大小写后无法启动
    将项目迁移到kubernetes平台是怎样实现的
    kubectl port-forward
    linux服务器安全配置最详解
    CentOS7.3下部署Rsyslog+LogAnalyzer+MySQL中央日志服务器
    统计linux 下当前socket 的fd数量
  • 原文地址:https://www.cnblogs.com/stoneniqiu/p/3858627.html
Copyright © 2020-2023  润新知