代码改变世界

注释文档在线编辑及生成

2014-07-21 17:29  stoneniqiu  阅读(1877)  评论(2编辑  收藏

     产品上市之前需要详细的帮助文档,每个程序员写各自负责的部分,为了统一格式和减轻工作量,决定用程序实现。文档生成方便一直很出名的就是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文档可能打不开,需要在属性里面解除锁定。因为一些原因,暂时不方便公开源码,只能分享下实现思路。