65.9K
CodeProject 正在变化。 阅读更多。
Home

类图生成器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (19投票s)

2014年10月29日

CPOL

6分钟阅读

viewsIcon

63051

downloadIcon

3931

一个项目驱动的 .NET 类图生成器,可以从 dll 或 exe 生成。

引言

本文针对类图生成以及 Visual Studio 无法将单个或所有类图导出为单个文件的问题。我们将研究程序集(Assemblies)以及如何从中获取字段、属性、方法和事件等信息。

下载 EXE

下载源

下载 UnmanagedLibraryHelper.zip

背景

虽然所有开发人员都讨厌文档和重复性任务,但它们仍然是我们有时日常工作的一部分。

是的,市面上有一些工具可以缓解痛苦,例如 Ghost Doc 或 Sandcastle Help Builder,但它们都不能生成类图。Code Project 也包含几个类图生成器,但这个有什么不同呢?嗯……我可以说,它能够创建一个包含多个指向你想要生成类图的 dll 或 exe 的条目的项目。本文将描述我如何分析 dll 或 exe,以及如何生成类图。此代码的另一个功能是将其指向代码目录并在类头中添加注释,以帮助生成包含类可视化表示的 CHM 文件。

使用代码

分析托管的 .NET dll 或 exe。

[以下代码位于 CDGenerator.cs 中]

        /// <summary>
        /// Analyzes the DLL.
        /// </summary>
        /// <param name="dllLocation">The DLL location.</param>
        /// <param name="excludeServices">if set to <c>true</c> [exclude services].</param>
        /// <param name="excludeCLR">if set to <c>true</c> [exclude color].</param>
        public static void AnalyzeDLL(string dllLocation, bool excludeServices = false, bool excludeCLR = true)
        {
            string assemblyNameSpace = "";
            Type currentType = null;
            Type[] typesFound = null;
            ClassList = new List<ClassInfo>();
            CDAssembly = null;

            try
            {
                if (dllLocation != string.Empty && File.Exists(dllLocation))
                {
                    CDAssembly = Assembly.LoadFrom(dllLocation);
                    if (CDAssembly.FullName != "")
                    {
                        assemblyNameSpace = CDAssembly.FullName.Substring(0, CDAssembly.FullName.IndexOf(","));

                        typesFound = CDAssembly.GetTypes();

                        if (typesFound != null)
                        {
                            foreach (var type in typesFound)
                            {
                                if (type.Namespace != null)
                                {
                                    if (type.IsNotPublic)
                                    {
                                        continue;
                                    }
                                    else
                                    {
                                        //Excludes Meta data classes and only generate for the current namespace
                                        if (!type.FullName.Contains("Metadata") && type.Namespace.Contains(assemblyNameSpace))
                                        {
                                            if (excludeServices && type.FullName.Contains("Service."))
                                            {
                                                continue;
                                            }

                                            //Exclude weird naming conventions.. usually generated classes not coded by a developer
                                            if (!type.FullName.Contains("<") && !type.FullName.Contains(">"))
                                            {
                                                currentType = type;
                                                ClassList.Add(new ClassInfo(Path.GetFileName(dllLocation), type, excludeCLR));
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(string.Format("{0}: {1}", currentType.Name, ex.Message));
                throw ex;
            }
        }

看看上面的 AnalyzeDLL 方法。你会看到里面使用了一些辅助类。
ClassInfo:存储类类型、名称以及 dll 或 exe 中找到的 BasicInfo 列表等信息。
BasicInfo:存储信息类型(例如,字段、属性、构造函数、方法或事件)。

代码开始时设置了几个变量并进行了一些检查,以确保我们实际上将有效的 dll 或 exe 位置传递给了它以进行分析。

将 dll 或 exe 加载到内存的实际工作是通过以下命令完成的:

CDAssembly = Assembly.LoadFrom(dllLocation);

我使用了 Assembly.LoadFrom() 来确保我们不会因为可能位于已加载文件相同目录中的引用程序集而遇到任何错误。这通常是在使用 Assembly.Load() 加载 dll 或 exe 时存在的问题。

为了确保文件实际加载,在程序集的完全限定名之后会进行检查,然后将命名空间记录到 assemblyNameSpace 变量中。

通过使用 CDAssembly 上的 GetTypes() 方法,从程序集中提取类型数组。

进一步检查以确保找到了类型并且它们是公共可访问的。
排除元数据,并进行检查以确保我们只查看主命名空间中的方法。

如果所有这些检查都通过,则将当前类型添加到 ClassList,并创建一个新的 ClassInfo 项。

ClassList.Add(new ClassInfo(Path.GetFileName(dllLocation), currentType, excludeCLR));

仔细查看 ClassInfo 构造函数,你会发现会检查类的类型(例如,Class、Interface、AbstractClass、Enum 或 Struct)。

一旦我们知道类的类型,就会调用 GetMoreInfo() 方法。此方法查找所有属性、字段、构造函数、方法和事件。它们都以相同的方式找到。

让我们看看方法是如何找到的:

MethodInfo[] methodInfos = referenceType.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.CreateInstance);

通过检查程序集类型并调用 GetMethods() 方法来构建一个数组。对于字段 (GetFields())、属性 (GetProperties()) 等也是如此。标志设置为 Public、Static、Instance 或 CreateInstance。

if (methodInfos != null && methodInfos.Length > 0)
                {
                    foreach (MethodInfo methodInfo in methodInfos)
                    {
                        //Only add custom methods. Don't show built in DotNet methods
                        if (excludeCLR && methodInfo.Module.ScopeName != ReferenceFileName)
                        {
                            continue;
                        }
                        else
                        {
                            ClassInformation.Add(new BasicInfo(BasicInfo.BasicInfoType.Methods, methodInfo.Name, methodInfo.ReturnType));
                        }
                    }
                }

数组构建完成后,会进行检查以查看是否有 null 值,并确保数组确实包含记录。
当检查通过时,我们会遍历找到的所有方法,同时检查方法是否在传递文件的作用域内,以及是否必须排除内置的 .NET 字段。内置的 .NET 方法的示例是 ToString() 或 GetType()。

生成类图


public static void GeneratesClassDiagram(ClassInfo classInfo, string outputPath, bool includeProperties = true, bool includeMethods = true, bool includeEvents = true, bool includeFields = true, bool includeConstructor = true)
        {... }
​

 现在我们有了一个类 (ClassInfo),其中包含其所有构造函数、字段、属性、方法和事件的列表,我们就可以实际生成类图图像了。
 这一切都由 CDGenerator 类中包含的 GeneratesClassDiagram() 方法完成。

 GeneratesClassDiagram 中使用了一个辅助方法和一个辅助类。
 CalculateSize:根据存在的构造函数、字段、属性、方法和事件的数量计算新类图的图像大小。

 RoundedRectangle:此类为 GDI+ 绘图创建了一个图形路径。它有助于根据给定的宽度、高度和直径计算圆角。

图形以“图层”绘制,以表示类框及其阴影,以及一个带有类名和类型的渐变标题。根据配置设置,每个类类型可以具有不同的颜色。

应用抗锯齿以提高文本可读性。

//Enable AntiAliasing
g.TextRenderingHint = TextRenderingHint.AntiAlias;

最后,当所有“图层”都绘制完成后,会进行尺寸比较,以避免保存空的类图。

对代码进行注释

通过调用 RemarkAllImages() 方法来对代码进行注释。
它将代码路径和图像路径作为变量,然后继续“注释”代码中所有具有相应类图图像的类。

/// <summary>
        /// Remarks all images.
        /// </summary>
        /// <param name="codePath">The code path.</param>
        /// <param name="imagesPath">The images path.</param>
        public static void RemarkAllImages(string codePath, string imagesPath)
        {
            string currentClassName = "";
            string remarkTemplate = Properties.Settings.Default.RemarkTemplate;
            string searchString = "";
            string currentClassRemark = "";

            int startIndex = 0;
            StreamReader sr;
            StreamWriter sw;
            FileInfo currentCodeFile;
            string currentCodeFileText = "";
            string outCodeFileText = "";
            if (Directory.Exists(imagesPath))
            {
                DirectoryInfo codeDirInfo = new DirectoryInfo(codePath);

                FileUtils.GetAllFilesInDir(codeDirInfo, "*.cs");

                foreach (string fileName in Directory.GetFiles(imagesPath, "*.png"))
                {
                    startIndex = 0;
                    try
                    {
                        currentClassName = Path.GetFileName(fileName).Replace(".png", "");
                        currentCodeFile = FileUtils.files.Where(f => f.Name == string.Format("{0}.cs", currentClassName)).FirstOrDefault();

                        if (currentCodeFile != null)
                        {
                            using (sr = new StreamReader(currentCodeFile.FullName))
                            {
                                currentCodeFileText = sr.ReadToEnd();
                                sr.Close();

                                //Finding the class description logic goes here. In essence it results in something like this:
                                //searchString = string.Format("public {0}", currentClassName);
                                

                                startIndex = currentCodeFileText.IndexOf(searchString);

                                //Add the remark
                                currentClassRemark = string.Format("{0}{1}\t", string.Format(remarkTemplate, currentClassName), Environment.NewLine);

                                if (!currentCodeFileText.Contains(currentClassRemark))
                                {
                                    outCodeFileText = currentCodeFileText.Insert(startIndex, currentClassRemark);

                                    using (sw = new StreamWriter(currentCodeFile.FullName, false))
                                    {
                                        sw.WriteLine(outCodeFileText);
                                        sw.Close();
                                    }
                                }

                                if (RemarkDone != null)
                                {
                                    RemarkDone(currentClassName);
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                        ErrorOccurred(ex, null);
                    }
                }
            }
        }

看看上面的 RemarkAllImages 方法。

它使用 FileUtils 类查找所有 C# 代码文件。

FileUtils.GetAllFilesInDir(codeDirInfo, "*.cs");

该方法有一个循环,然后获取所有 .png 文件,并通过 LINQ 筛选出所有没有生成图像的代码文件。

FileUtils.files.Where(f => f.Name == string.Format("{0}.cs", currentClassName)).FirstOrDefault();

应用一些逻辑来构建一个基于类类型的搜索字符串。
然后搜索字符串找到类头,并在其上方添加注释,使用注释模板。

字符串追加后,会写回代码文件。

 

非托管 DLL

所以,如果涉及到 .NET dll 或托管 dll,这篇文章都很好,但非托管 dll 呢?
非托管 dll 不能像 .NET dll 那样进行反射。嗯;那就是 Windows 中有一个很酷的 dbghelp.dll,我们可以利用它来获取这些信息。

看看 UnmanagedLibraryHelper。它尚未在解决方案中实现,但值得一看。

public static List<string> GetUnmanagedDllFunctions(string filePath)
        {
            methodNames = new List<string>();

            hCurrentProcess = Process.GetCurrentProcess().Handle;

            ulong baseOfDll;
            bool status;

            // Initialize sym.
            status = SymInitialize(hCurrentProcess, null, false);

            if (status == false)
            {
                Console.Out.WriteLine("Failed to initialize sym.");
            }

            // Load dll.
            baseOfDll = SymLoadModuleEx(hCurrentProcess, IntPtr.Zero, filePath, null, 0, 0, IntPtr.Zero, 0);

            if (baseOfDll == 0)
            {
                Console.Out.WriteLine("Failed to load module.");
                SymCleanup(hCurrentProcess);
            }

            // Enumerate symbols. For every symbol the callback method EnumSyms is called.
            if (SymEnumerateSymbols64(hCurrentProcess, baseOfDll, EnumSyms, IntPtr.Zero))
            {
                Console.Out.WriteLine("Failed to enum symbols.");
            }

            // Cleanup.
            SymCleanup(hCurrentProcess);

            return methodNames;
        }

首先,我们通过调用 SymInitialize 来确保我们初始化符号处理器,并为其提供一个指向调用代码的句柄。

[DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SymInitialize(IntPtr hProcess, string UserSearchPath, [MarshalAs(UnmanagedType.Bool)]bool fInvadeProcess);

然后,我们通过调用 SymLoadModuleEx 来加载非托管 dll;在这里,我们将我们的句柄和 dll 的文件路径传递给它。

[DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern ulong SymLoadModuleEx(IntPtr hProcess, IntPtr hFile, string ImageName, string ModuleName, long BaseOfDll, int DllSize, IntPtr Data, int Flags);

dll 加载完成后,我们必须枚举其所有函数。这是通过调用 SymEnumerateSymbols64 来实现的。EnumSyms 方法允许我们读取每次枚举并获取函数名。

[DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SymEnumerateSymbols64(IntPtr hProcess, ulong BaseOfDll, SymEnumerateSymbolsProc64 EnumSymbolsCallback, IntPtr UserContext);

private static bool EnumSyms(string name, ulong address, uint size, IntPtr context)
        {
            if (!name.Contains("Ordinal"))
                methodNames.Add(string.Format("{0}", name));

            return true;
        }

加载 dll 并获取其函数后,需要进行一些清理工作。这是通过调用 SymCleanup 并将我们的句柄传递给它来实现的。

// Cleanup.
SymCleanup(hCurrentProcess);

就这样。它不像托管 dll 那样能完全了解 dll 内部发生的情况,但至少为我们提供了一个起点。从这里开始,您可以开始识别方法和属性,并结合 ClassInfo 类,就可以用来绘制类图。

关注点

我们公司目前已广泛使用此工具,并且正在将其扩展以包含 EF 辅助类生成、代码注释中的 SQL 数据库注释以及数据库字典生成。希望有一天我能将其作为工具上传到这里,以帮助更广泛的受众。

历史

版本 1:带有注释生成的类图生成器。

© . All rights reserved.