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

200%反射式类图创建工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (197投票s)

2011年6月6日

CPOL

22分钟阅读

viewsIcon

673869

downloadIcon

11555

WPF:我的100%反射类图创建工具的第二版。

目录 

引言

你们中的一些人可能是 www.codeproject.com 的老会员了,可能还记得大约 4 年前,我发表了一篇关于从 DLL/Exe 绘制类图的文章,名为 "AutoDiagrammer"。我很幸运,那篇文章非常受欢迎,获得了大量的投票和浏览量。基本上人们似乎都很喜欢它,这很棒……我对此非常满意。这里的每个作者都希望人们喜欢他们发表的东西(包括我自己,我想这是虚荣心作祟)。

问题是,我很久以前就写了那篇原始文章,那时我刚开始接触 WPF,虽然我对它很满意,但我总觉得它可以做得更好。它也是 WinForms 的,所以快进几年,我现在对 WPF 足够了解,可以真正对原始文章进行修改,并使其达到我一直设想的样子。

我觉得第一个 "AutoDiagrammer" 文章存在的问题如下:

  1. 类关联的绘制基于网格布局。
  2. 用户无法在设计平面上移动类;一旦它们被布局好,就固定了。
  3. 关联线不够清晰。
  4. 用户无法很好地缩放生成的图表(虽然可以,但效果不佳)。
  5. 加载要绘制为类图的 DLL/Exe 是在与 *AutoDiagrammer.exe* 应用程序相同的 AppDomain 中完成的,因此在反射时,它会被强制将 所有 额外反射的类型从 DLL/Exe 加载到 *AutoDiagrammer.exe* 应用程序的 AppDomain 中。哎呀……这不好。
  6. 人们觉得弄清楚如何实际生成图表有点麻烦。

话虽如此,我确实觉得有些地方做得很好,例如:

  1. 总体思路(人们似乎普遍喜欢它,并认为它是一个非常有用的工具)。
  2. 信息反射是正确的。
  3. 能够微调图表上显示的内容。

考虑到所有这些优点和缺点,再加上我现在对 WPF 足够了解,可以对原始代码进行修改并使其达到我一直想要的样子,我想……是的,时机成熟了,对原始文章进行彻底重写。

所以这篇文章就是对原始 "AutoDiagrammer" 文章的彻底重写;这篇新文章代码的特性列表是:

  1. 检测有效的 .NET 程序集(是的,与第一篇文章一样,此工具仅适用于 .NET 程序集)。
  2. 能够在图表的设计平面中移动类。
  3. 能够不显示图表上没有任何关联的类,但仍允许用户从下拉列表中查看这些类。这有助于保持图表整洁,只显示绝对需要的内容。
  4. 持久化设置,以便下次运行应用程序时,您的个人设置将保持您离开时的样子。
  5. 对象作为矢量进行适当缩放,因此无论以何种比例查看图表,对象都尽可能清晰。
  6. 将图表保存为 PNG 文件,可以使用标准 Windows 图像查看器轻松查看。
  7. 打印到打印机。
  8. 将 DLL/Exe 加载到单独的 AppDomain 中,该 AppDomain 不会用加载的 DLL/Exe 类型污染 *AutoDiagrammer.exe* AppDomain
  9. 集成帮助。
  10. 类显示一个完整的关联弹出窗口,所有关联都显示为字符串列表。
  11. 当用户将鼠标悬停在关联线上时,关联线会以不同的颜色显示。
  12. 通过解析方法体 IL(中间语言)更好地检测类之间的关联。
  13. 能够查看方法体 IL(中间语言)。

如您所见,我保留了旧文章/代码库中的优点,并增加了更多功能。我对结果非常满意,希望您也会如此。

它长什么样

我认为展示这一切的最佳方式是使用几个屏幕截图,所以让我们看几个。然后我们将看看如何使用这个新版本的 AutoDiagrammer,我巧妙地称之为 AutoDiagrammer II。不错吧?

应用程序启动时,它看起来像这样,等待您选择一个 DLL/Exe 来绘制类图。

在您单击“打开 Dll/Exe”按钮并导航到有效的 .NET DLL/Exe 后,它可能看起来像这样(注意:我创建了一个虚拟测试 DLL 来进行测试,所以上面显示的就是它)

现在它正在等待您选择希望显示在绘制图表上的项目。这比第一个 AutoDiagrammer 文章容易得多,因为您现在只需使用每个 TreeViewItem 旁边的复选框进行选择,然后单击 TreeView 上方的“绘制图标”。

下一步是点击“绘制图标”,但在我们这样做之前,让我们考虑一下我的小测试用例 DLL,其完整内容如下所示

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ClassLibrary1
{
    public interface IDoSomething
    {
        void DoSomeStuff();
    }

    public class Doer : IDoSomething
    {
        public void DoSomeStuff()
        {
            List<String> stuff = new List<string>();
            for (int i = 0; i < 10; i++)
            {
                stuff.Add(i.ToString());
            }
        }
    }

    public class Class2 : Doer
    {
        private Renderer renderer;

        public Class2(Renderer renderer)
        {
            this.renderer = renderer;
        }
    }

    public abstract class Renderer
    {
        public abstract void Render();
    }

    public class ConcereteRenderer : Renderer
    {
         
        public override void  Render()
        {
            Console.WriteLine("This is the render() method");
        }
    }

    public class Class1
    {
        public void DoIt()
        {
            ConcereteRenderer rend = new ConcereteRenderer();
            Class2 x = new Class2(rend);

        }
    }
}

根据这段代码,将绘制出以下内容:

现在有几件事值得注意,例如:

  1. 图表上并没有显示所有的类,“`Doer`”类没有显示!为什么呢?因为它没有任何关联,所以不在图表上,而是位于右上角的“未关联项”列表中,您可以从那里查看该类。
  2. 从 `Class1` 到 `ConcreteRenderer` 的关联被找到了,这要归功于对 `Class1.DoIt()` 方法中的方法体 IL 的解析。
  3. 当您将鼠标悬停在类上时,关联线会以不同的颜色显示。
  4. 我们可以让图表占据页面的所有宽度,并完全隐藏左侧窗格。

现在让我们看看其他一些功能,例如查看关联,可以通过单击类右上角的“显示关联”图标来查看。

那么,能够查看方法的 IL 呢?这可能吗?是的,只需点击方法旁边(只要启用)的放大镜,就会显示一个 IL 窗口,其中包含方法体 IL。如果我有无限的时间,我可能会将其转换为 C#,但我现在没有,所以只能是 IL。嘿,甚至可能你们中有些人能像读 C# 一样读 IL,谁知道呢。

如何使用新的 AutoDiagrammer

以下部分将向您概述如何使用新版本的 AutoDiagrammer。请注意,我在上面展示屏幕截图时已经涵盖了一些内容,对此请原谅我。

安装 AutoDiagrammer

当您下载并构建附加的代码时,您会在 *bin/XXXX* 文件夹中看到以下内容:

您只需将整个 *bin/XXXX* 文件夹复制到您希望运行 *AutoDiagrammer.exe* 的新文件夹中即可。这就是您需要做的所有事情。然后,要运行 *AutoDiagrammer.exe*,只需双击您复制文件的位置,它应该就能正常工作。

加载 DLL/Exe

这很简单,您只需要点击“打开”按钮,然后选择是要打开 DLL 还是 Exe,然后浏览到您要打开的文件位置。

选择要绘制的类

选择要加载的 DLL/Exe 后,您将看到一个已填充的 TreeView,它按类所在的命名空间进行组织。这个过程可能需要一段时间,因为大部分工作都在这里完成,即从请求的 DLL/Exe 中反射出信息,而且这个过程也有一个超时,可以从“设置”窗口进行调整。

在生成树视图时,您将看到此加载横幅

假设您已加载了 TreeView,现在您只需选择希望出现在图表上的类。这可以通过简单地使用类名旁边的 CheckBoxes,或者整个命名空间旁边,甚至整个 TreeView 旁边的 CheckBoxes 来完成。然后点击“绘制”按钮(是的,就是那个带有铅笔图标的按钮)。

单击要绘制的类并单击“绘制”按钮后,您将看到第二个加载屏幕(形式如下所示),同时创建图表。

图表创建完成后(或超时,超时时间同样可通过“设置”窗口调整)

将显示一个新图表(如果超时,则显示上次成功加载的图表(如果有的话))

注意:并非所有类都可能出现在图表上,因为实际图表上只显示那些具有关联的类。这是为了清除图表上不那么有效的信息。未关联的类仍然可以查看,如下所述。

使用绘制的类图

每个类都有几个可能的组成部分(同样,如果没有任何内容可显示,用户请求不显示这些部分,图表将不包含这些组成部分)。给定类上可能显示的组成部分如下:

  • 接口
  • 构造函数
  • 字段
  • 事件
  • 属性
  • 方法

这是一个典型的类示例,它可能看起来像这样:

查看方法体 IL

还有一个非常实用的功能是,您可以使用显示的小放大镜图标来查看方法体 IL。

重要提示:每个这些部分都在一个可展开区域内。还有一个设置可以用来确定在这些部分展开时是否重新绘制类图。默认情况下,图表在展开/折叠时不会重新绘制;如果这不适合您,请随时使用系统设置来更改此行为。

查看关联

通过将鼠标悬停在类上,还可以查看给定类的所有关联,这将显示一个关联的弹出窗口。

未关联的类

如前所述,图表通过不将任何没有关联的类实际放置在图表上来保持整洁。这些类仍然可用,但它们不在主图表上,并且必须使用“非关联项”下拉菜单访问,该菜单仅在用户选择绘制的类中存在没有关联的类时才会显示。

选择其中一个将简单地弹出一个窗口,其中包含所选类的详细信息,使用与作为主图表一部分相同的分区。

保存

可以使用提供的“保存”按钮将图表保存为 PNG 格式。

然后可以在标准图像查看器中查看

打印

可以使用提供的打印按钮打印图表。

自定义图表上显示的内容

“设置”窗口允许根据您的具体要求定制图表。这些设置将在您关闭 *AutoDiagrammer.exe* 时保存到磁盘,并在您下次运行 *AutoDigrammer.exe* 时重新加载。

这里有许多设置,不仅可以控制图表上显示的内容,还可以控制图表应使用的图布局算法。默认的图布局算法是“高效 Sugiyama”,但您可能会发现其他图布局可能更适合您的图表需求。这主要取决于试验您的图表显示内容以及哪种方法最适合您。

图布局算法

设置窗口允许用户在创建图表时选择不同的图布局算法。如前所述,默认的图布局算法是“高效 Sugiyama”,但还有其他几种布局算法,如下所示:

  1. 有界 FR
  2. 高效 Sugiyama
  3. FR
  4. ISOM
  5. KK
  6. Tree

这些布局算法可能适合也可能不适合您的特定图表需求。这主要取决于试错。但是,所有设置都会持久化,所以当您退出 AutoDiagrammer 并重新打开它时,请放心,它将保持您离开时的样子。

所有不同布局算法的共同之处在于,它们有许多不同的参数可以供您调整。例如,以下是“高效 Sugiyama”布局算法可用的不同设置集:

以下是“树”布局算法的设置

我建议,一旦您有一个带有类和关联的活动图表,就打开“设置”窗口,尝试不同的布局算法,然后点击右上角的“重新布局图”按钮(如上图突出显示所示),以找到最适合您的方法。

选择要显示的内容

AutoDiagrammer 也有许多控制图表绘制方式的设置。这些设置如下所示:

其中很多都是一种主动设置,意味着您不需要重新布局图表。这是因为图表直接绑定到作为单例实例的设置 ViewModel。显然,超时只会在下次创建新图表时生效。

工作原理

以下小节将有望让您了解新的 AutoDiagrammer 代码是如何协同工作的。我需要提及的一点是,它基于 WPF 和 MVVM。现在,你们中有些人可能知道我编写了自己的 MVVM 框架,名为 Cinch,我在编写 WPF 应用程序时,几乎所有 MVVM 开发都使用它。本文也不例外;因此,您会发现我确实使用了 Cinch 和 MVVM。如果您不熟悉 Cinch 或 MVVM,您可能需要先阅读一下它们。如果您对这两者都满意,没问题,我们继续。

基本思路

在我们深入细节之前,让我们以非常简单的编号步骤回顾一下我们想要实现的基本思想:

  1. 允许用户打开一个 DLL/Exe,然后检查它是否是一个有效的 .NET 程序集。如果不是,在告知用户原因后退出。如果是有效的,则转到步骤 2。
  2. 在新的 AppDomain 中加载有效的 .NET 程序集,并提取树视图信息以及所有类信息,如接口/方法(包括方法体 IL)/属性/事件等。
  3. 使用步骤 2 中的数据绘制命名空间和找到的类型的树视图。
  4. 允许用户选择他们希望绘制的类型。
  5. 对于步骤 4 中创建的树视图中所有用户选择的类型,创建将表示所选类型的实际图形对象。
  6. 对于步骤 5 中所有具有关联的图形对象,将这些对象添加到 Graphsharp 图中。
  7. 对于步骤 5 中所有具有关联的图形对象,将这些对象添加到组合框中,以便用户可以查看它们,但它们不属于主图表。

简而言之,这就是图表的创建方式。显然还有其他与图表创建不直接相关的区域,例如设置/帮助等,但我们也会涵盖这些,不用担心。

但是,有一些支持类我不会深入探讨,因为我觉得没有必要,但代码显然已附加到本文中,如果您对其中一个我未涵盖的类感到好奇,请在本文论坛中添加查询,我将回答。

检测 DLL/Exe 是否为 .NET

AutoDiagrammer.exe 仅支持渲染在有效 .NET DLL/Exe 中找到的类型。使用以下辅助类可以轻松实现这一点:

/// <summary>
/// A simple helper class, that one has one method, that
/// is used to determine if an input file is an actual
/// CLR type file.
/// </summary>
public class DotNetObject
{
    #region Public Methods
    /// <summary>
    /// Return true if the file specified is a real CLR type, otherwise false is returned.
    /// False is also returned in the case of an exception being caught
    /// </summary>
    /// <param name="file">A string representing the file to check for CLR validity</param>
    /// <returns>True if the file specified is a real CLR type, otherwise false is returned.
    /// False is also returned in the case of an exception being caught</returns>
    public static bool IsValidDotNetAssembly(String file)
    {
        uint peHeader;
        uint peHeaderSignature;
        ushort machine;
        ushort sections;
        uint timestamp;
        uint pSymbolTable;
        uint noOfSymbol;
        ushort optionalHeaderSize;
        ushort characteristics;
        ushort dataDictionaryStart;
        uint[] dataDictionaryRVA = new uint[16];
        uint[] dataDictionarySize = new uint[16];

        //get the input stream
        Stream fs = new FileStream(@file, FileMode.Open, FileAccess.Read);

        try
        {
            BinaryReader reader = new BinaryReader(fs);
            //PE Header starts @ 0x3C (60). Its a 4 byte header.
            fs.Position = 0x3C;
            peHeader = reader.ReadUInt32();
            //Moving to PE Header start location...
            fs.Position = peHeader;
            peHeaderSignature = reader.ReadUInt32();
            //We can also show all these value, but we will be       
            //limiting to the CLI header test.
            machine = reader.ReadUInt16();
            sections = reader.ReadUInt16();
            timestamp = reader.ReadUInt32();
            pSymbolTable = reader.ReadUInt32();
            noOfSymbol = reader.ReadUInt32();
            optionalHeaderSize = reader.ReadUInt16();
            characteristics = reader.ReadUInt16();
            /*
                Now we are at the end of the PE Header and from here, the
                PE Optional Headers starts...
                To go directly to the datadictionary, we'll increase the      
                stream’s current position to with 96 (0x60). 96 because,
                28 for Standard fields
                68 for NT-specific fields
                From here DataDictionary starts...and its of total 128 bytes. 
                DataDictionay has 16 directories in total,
                doing simple maths 128/16 = 8.
                So each directory is of 8 bytes.
             
                In this 8 bytes, 4 bytes is of RVA and 4 bytes of Size.
                btw, the 15th directory consist of CLR header! if its 0, its not a CLR file :)

                */
            dataDictionaryStart = Convert.ToUInt16(Convert.ToUInt16(fs.Position) + 0x60);
            fs.Position = dataDictionaryStart;
            for (int i = 0; i < 15; i++)
            {
                dataDictionaryRVA[i] = reader.ReadUInt32();
                dataDictionarySize[i] = reader.ReadUInt32();
            }
            if (dataDictionaryRVA[14] == 0)
            {
                fs.Close();
                return false;
            }
            else
            {
                fs.Close();
                return true;
            }
        }
        catch (Exception)
        {
            return false;
        }
        finally
        {
            fs.Close();
        }
    }

    /// <summary>
    /// Return true if t is wanted for diagram, at present the only thing
    /// not allowed are System namespaced Dlls
    /// </summary>
    public static bool IsWantedForDiagramType(Type t)
    {
        //check to see if the class lives in a namespace
        if (!string.IsNullOrEmpty(t.Namespace))
        {
            //dont really want user to trawl the standard System namespaces
            if (t.Namespace.StartsWith("System"))
                return false;
        }

        return true;
    }
    #endregion
}

在单独的 AppDomain 中检查类型

AutoDiagrammer 将要做的一项主要工作是检查 DLL/Exe 文件并从中反射出信息。这听起来足够简单,但如果您不小心,这些反射的信息将被加载到您当前的 AppDomain 中。是的,所有反射的类型都将加载到当前 AppDomain 中。旧的 AutoDiagrammer 没有为这一明显的疏忽做任何准备。

然而,本文中介绍的新 AutoDiagrammer 通过确保加载的 DLL/Exe 在其自己的 AppDomain 中使用反射进行检查,并在反射过程完成后卸载该 AppDomain 来解决所有这些问题。这确保了反射的 DLL/Exe 中的任何类型信息都不会序列化到当前的 AppDomain 中。

这部分代码相当复杂,需要大量的代码列表才能完全解释,所以我将尽量简短。不过,在处理另一个 AppDomain 时,如果您的代码依赖于从新 AppDomain 中的代码返回的某种数据结构,那么一般经验法则是,这些数据结构本身必须是可序列化的,才能允许它们序列化回主 AppDomain。另一个技巧是,您的辅助 AppDomain 加载器应该继承自 MarshallByRefObject,这允许它在主 AppDomain 中被解封。

建议将相同的 `Evidence` 应用到新的 `AppDomain`,就像应用到主 `AppDomain` 一样。

正如我所说,要详细讲解这些代码实在太多了;相反,我将向您展示核心部分,这应该能让您了解如果对此感兴趣,可以在附加代码中查找哪些内容。

大部分工作都是使用以下代码实现的

[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(ITreeCreator))]
public class TreeCreator : ITreeCreator
{
    #region ITreeCreator Members
    public List<AssemblyTreeViewModel> ScanAssemblyAndCreateTree(String assemblyFileName)
    {
        AppDomain childDomain = BuildChildDomain(AppDomain.CurrentDomain, assemblyFileName);

        try
        {
            List<AssemblyTreeViewModel> tree = new List<AssemblyTreeViewModel>();

            Type loaderType = typeof(SeperateAppDomainAssemblyLoader);
            if (loaderType.Assembly != null)
            {
                SeperateAppDomainAssemblyLoader loader =
                    (SeperateAppDomainAssemblyLoader)childDomain.CreateInstanceFrom(
                        loaderType.Assembly.Location, loaderType.FullName).Unwrap();

                loader.Initialise(assemblyFileName);
                tree = loader.ScanAssemblyAndCreateTree();
            }

            return tree;
        }
        catch (AggregateException aggEx)
        {
            throw new InvalidOperationException(
                string.Format("Could not load namespaces for the assembly file : {0}\r\n\r\n{1}",
                assemblyFileName,
                aggEx.InnerException.Message));
        }
        finally
        {
            AppDomain.Unload(childDomain);
        }
    }
    #endregion

    #region Private Methods
    private AppDomain BuildChildDomain(AppDomain parentDomain, string fileName)
    {
        Evidence evidence = new Evidence(parentDomain.Evidence);
        AppDomainSetup setup = parentDomain.SetupInformation;
        FileInfo fi = new FileInfo(fileName);
        AppDomain newAppDomain = 
          AppDomain.CreateDomain("DiscoveryRegion", evidence, setup);

        return newAppDomain;
    }
    #endregion
}

public class SeperateAppDomainAssemblyLoader : MarshalByRefObject
{
    #region Data
    private String assemblyFileName;
    private Assembly assembly;
    #endregion

    #region Public Methods
    public void Initialise(String assemblyFileName)
    {
        this.assemblyFileName = assemblyFileName;
        assembly = Assembly.LoadFrom(assemblyFileName);
    }
    #endregion

    #region Private/Internal Methods
    [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
    internal List<AssemblyTreeViewModel> ScanAssemblyAndCreateTree()
    {
        AppDomain curDomain = AppDomain.CurrentDomain;

        try
        {
            AppDomain.CurrentDomain.AssemblyResolve += ReflectionOnlyResolveEventHandler;
            List<AssemblyTreeViewModel> tree = GroupAndCreateTree(assemblyFileName);
            return tree;
        }
        finally
        {
            AppDomain.CurrentDomain.AssemblyResolve -= ReflectionOnlyResolveEventHandler;
        }
    }

    private Assembly ReflectionOnlyResolveEventHandler(object sender, ResolveEventArgs args)
    {
        DirectoryInfo directory = new DirectoryInfo(assemblyFileName);

        Assembly loadedAssembly =
            AppDomain.CurrentDomain.GetAssemblies()
                .FirstOrDefault(asm => string.Equals(asm.FullName, args.Name, 
                    StringComparison.OrdinalIgnoreCase));

        if (loadedAssembly != null)
        {
            return loadedAssembly;
        }

        AssemblyName assemblyName = new AssemblyName(args.Name);
        string dependentAssemblyFilename = Path.Combine(
            directory.FullName, assemblyName.Name + ".dll");

        if (File.Exists(dependentAssemblyFilename))
        {
            return Assembly.LoadFrom(dependentAssemblyFilename);
        }
        return Assembly.Load(args.Name);
    }

    private List<AssemblyTreeViewModel> GroupAndCreateTree(String assemblyFileName)
    {
        AssemblyTreeViewModel root = null;
        List<AssemblyTreeViewModel> tree = new List<AssemblyTreeViewModel>();

        var groupedTypes = from t in assembly.GetTypes()
                            where DotNetObject.IsWantedForDiagramType(t)
                            group t by t.Namespace into g
                            select new { NameSpace = g.Key, Types = g };

        foreach (var g in groupedTypes)
        {
            if (g.NameSpace != null)
            {

                AssemblyTreeViewModel sub = null;
                AssemblyTreeViewModel parentToAddTo = null;

                if (tree.Count == 0)
                {
                    root = new AssemblyTreeViewModel(RepresentationType.AssemblyOrExe,
                           String.Format("Assembly : {0}", 
                           assembly.GetName().Name), null, null);
                    tree.Add(root);
                    //Add the types
                    AddTypes(g.Types, root);
                }
                else
                {
                    string trimmedNamespace = g.NameSpace;
                    if (g.NameSpace.Contains("."))
                        trimmedNamespace = 
                          g.NameSpace.Substring(0, g.NameSpace.LastIndexOf("."));

                    if (g.NameSpace.Equals(String.Empty))
                        parentToAddTo = root;
                    else
                        parentToAddTo = FindCorrectTreeNodeToAddTo(root, trimmedNamespace);

                    if (parentToAddTo == null)
                        parentToAddTo = root;

                    sub = new AssemblyTreeViewModel(
                        RepresentationType.Namespace, g.NameSpace, null, parentToAddTo);

                    parentToAddTo.Children.Add(sub);
                    //add the types
                    AddTypes(g.Types, sub);
                }
            }
        }

        return tree;
    }

    private AssemblyTreeViewModel FindCorrectTreeNodeToAddTo(
        AssemblyTreeViewModel node, String @namespace)
    {
        var results = node.Children.Where(x => x.Name == @namespace);

        if (results.Count() > 0)
            return results.First();


        foreach (AssemblyTreeViewModel child in node.Children)
        {
            AssemblyTreeViewModel assemblyTreeViewModel = 
                FindCorrectTreeNodeToAddTo(child, @namespace);

            if (assemblyTreeViewModel != null)
                return assemblyTreeViewModel;
        }

        return null;
    }

    private void AddTypes(IGrouping<String, Type> types, AssemblyTreeViewModel parent)
    {

        TypeReflector.RequiredBindings = SettingsViewModel.Instance.RequiredBindings;
        TypeReflector.ShowConstructorParameters = 
            SettingsViewModel.Instance.ShowConstructorParameters;
        TypeReflector.ShowFieldTypes = SettingsViewModel.Instance.ShowFieldTypes;
        TypeReflector.ShowFieldTypes = SettingsViewModel.Instance.ShowPropertyTypes;
        TypeReflector.ShowInterfaces = SettingsViewModel.Instance.ShowInterfaces;
        TypeReflector.ShowMethodArguments = SettingsViewModel.Instance.ShowMethodArguments;
        TypeReflector.ShowMethodReturnValues = SettingsViewModel.Instance.ShowMethodReturnValues;
        TypeReflector.ShowGetMethodForProperty = SettingsViewModel.Instance.ShowGetMethodForProperty;
        TypeReflector.ShowSetMethodForProperty = 
            SettingsViewModel.Instance.ShowSetMethodForProperty;
        TypeReflector.ShowEvents = SettingsViewModel.Instance.ShowEvents;

        //Load ILReaader Globals
        MethodBodyReader.LoadOpCodes();

        foreach (var t in types)
        {
            TypeReflector typeReflector = new TypeReflector(t);
            typeReflector.ReflectOnType();

            SerializableVertex vertex = new SerializableVertex(
                typeReflector.Name,
                typeReflector.ShortName,
                typeReflector.Constructors,
                typeReflector.Fields,
                typeReflector.Properties,
                typeReflector.Interfaces,
                typeReflector.Methods,
                typeReflector.Events,
                typeReflector.Associations,
                typeReflector.HasConstructors,
                typeReflector.HasFields,
                typeReflector.HasProperties,
                typeReflector.HasInterfaces,
                typeReflector.HasMethods,
                typeReflector.HasEvents);

            AssemblyTreeViewModel newNode = 
                new AssemblyTreeViewModel(RepresentationType.Class, t.Name, vertex, parent);
            parent.Children.Add(newNode);
        }
    }

    #endregion
}

这个类负责创建新的 AppDomain 和您在运行 AutoDiagrammer 应用程序时看到的 TreeView。您可以看到方法

List<AssemblyTreeViewModel> ScanAssemblyAndCreateTree(String assemblyFileName);

它是 TreeCreator 服务上唯一公开的方法,返回一个 List<AssemblyTreeViewModel>,其中 AssemblyTreeViewModel 是可序列化的(因为它从新的 AppDomain 返回到主 AppDomain)。

public enum RepresentationType { AssemblyOrExe = 1, Namespace, Class };

[Serializable]
[DebuggerDisplay("{ToString()}")]
public class AssemblyTreeViewModel : INPCBase
{
    public AssemblyTreeViewModel(RepresentationType nodeType, string name, 
    SerializableVertex vertex, AssemblyTreeViewModel parent)
    {
        this.NodeType = nodeType;
        this.Name = name;
        this.Vertex = vertex;
        this.Parent = parent;
        Children = new List<AssemblyTreeViewModel>();
    ...
    ...
    ...

    }

    public RepresentationType NodeType { get; private set; }
    public List<AssemblyTreeViewModel> Children { get; private set; }
    public bool IsInitiallySelected { get; private set; }
    public string Name { get; private set; }
    public AssemblyTreeViewModel Parent { get; private set; }
    public SerializableVertex Vertex { get; private set; }
    ....
    ....
    ....
    ....
    ....
}

这些代表 TreeView 项,并且还持有一个 SerializableVertex 的内部引用,该引用表示找到的类信息。那么,这些 AssemblyTreeViewModel 对象之一是如何用一个完全填充的 SerializableVertex 构造的呢?

嗯,如果你仔细看上面 TreeCreator 代码的末尾(查看 AddTypes(IGrouping<String, Type> types, AssemblyTreeViewModel parent) 方法),你会看到这样的代码行:

foreach (var t in types)
{
    TypeReflector typeReflector = new TypeReflector(t);
    typeReflector.ReflectOnType();

    SerializableVertex vertex = new SerializableVertex(
        typeReflector.Name,
        typeReflector.ShortName,
        typeReflector.Constructors,
        typeReflector.Fields,
        typeReflector.Properties,
        typeReflector.Interfaces,
        typeReflector.Methods,
        typeReflector.Events,
        typeReflector.Associations,
        typeReflector.HasConstructors,
        typeReflector.HasFields,
        typeReflector.HasProperties,
        typeReflector.HasInterfaces,
        typeReflector.HasMethods,
        typeReflector.HasEvents);

    AssemblyTreeViewModel newNode = 
        new AssemblyTreeViewModel(RepresentationType.Class, t.Name, vertex, parent);
    parent.Children.Add(newNode);

可以看出,我们利用了一个名为 TypeReflector 的小辅助类,它为我们完成了所有工作。我们接下来将讨论它。从这段代码中,您可以看到,当我们将 List<AssemblyTreeViewModel>TreeCreator 服务返回时,我们已经从加载的 DLL/Exe 中反射出了所有需要的信息。

反射出类数据

如上所述,TreeCreator 服务是负责在单独的 AppDomain 中加载和反射 DLL/Exe 数据的代码,其结果是一个 List<AssemblyTreeViewModel>,其中每个 AssemblyTreeViewModel 都由一个完全填充的 SerializableVertex 构成,该 SerializableVertex 稍后用于绘制 Graphsharp 图(本质上就是图表)。

那么,让我们看看这些 SerializableVertex 对象是如何创建的。

回想一下,我曾说过我们使用了一个名为 TypeReflector 的辅助类,它看起来是这样的:

[Serializable]
public class TypeReflector
{
    private List<MethodInfo> propGetters = new List<MethodInfo>();
    private List<MethodInfo> propSetters = new List<MethodInfo>();
    private List<Type> extraAssociations = new List<Type>();

    public TypeReflector(Type type)
    {
        this.TypeInAssembly = type;
        this.Name = type.FullName;
        this.ShortName = type.Name;

        Constructors = new List<string>();
        Fields = new List<string>();
        Properties = new List<string>();
        Interfaces = new List<string>();
        Methods = new List<SerializableMethodData>();
        Events = new List<string>();
        Associations = new List<string>();
    }

    public void ReflectOnType()
    {
        ReflectOutConstructors();
        ReflectOutFields();
        ReflectOutProperties();
        ReflectOutInterfaces();
        ReflectOutMethods();
        ReflectOutEvents();
    }

    public Type TypeInAssembly { get; private set; }
    public String Name { get; private set; }
    public String ShortName { get; private set; }
    public List<String> Constructors { get; private set; }
    public List<String> Fields { get; private set; }
    public List<String> Properties { get; private set; }
    public List<String> Interfaces { get; private set; }
    public List<SerializableMethodData> Methods { get; private set; }
    public List<String> Events { get; private set; }
    public List<String> Associations { get; private set; }
    public bool HasConstructors { get; private set; }
    public bool HasFields { get; private set; }
    public bool HasProperties { get; private set; }
    public bool HasInterfaces { get; private set; }
    public bool HasMethods { get; private set; }
    public bool HasEvents { get; private set; }
        
    public static BindingFlags RequiredBindings { get; set; }
    public static bool ShowConstructorParameters { get; set; }
    public static bool ShowFieldTypes { get; set; }
    public static bool ShowPropertyTypes { get; set; }
    public static bool ShowInterfaces { get; set; }
    public static bool ShowMethodArguments { get; set; }
    public static bool ShowMethodReturnValues { get; set; }
    public static bool ShowGetMethodForProperty { get; set; }
    public static bool ShowSetMethodForProperty { get; set; }
    public static bool ShowEvents { get; set; }

    private void ReflectOutMethods()
    {
        //do methods
        foreach (MethodInfo mi in TypeInAssembly.GetMethods(RequiredBindings))
        {
            if (TypeInAssembly == mi.DeclaringType)
            {
                string mDetail = mi.Name + "( ";
                string pDetail = "";
                //do we want to display method arguments, if we do create the 
                //appopraiate string
                if (ShowMethodArguments)
                {
                    ParameterInfo[] pif = mi.GetParameters();
                    foreach (ParameterInfo p in pif)
                    {
                        //add all the parameter types to the associations List, so that 
                        //the association lines for this class can be obtained, and 
                        //possibly drawn on the container
                        string pName = GetGenericsForType(p.ParameterType);
                        pName = LowerAndTrim(pName);
                        string association = p.ParameterType.IsGenericType ? 
                               pName : p.ParameterType.FullName;
                        if (!Associations.Contains(association))
                        {
                            Associations.Add(association);
                        }
                        pDetail = pName + " " + p.Name + ", ";
                        mDetail += pDetail;
                    }
                    if (mDetail.LastIndexOf(",") > 0)
                    {
                        mDetail = mDetail.Substring(0, mDetail.LastIndexOf(","));
                    }
                }
                mDetail += " )";
                //add the return type to the associations List, so that 
                //the association lines for this class can be obtained, and 
                //possibly drawn on the container
                string rName = GetGenericsForType(mi.ReturnType);
                //dont want to include void as an association type
                if (!string.IsNullOrEmpty(rName))
                {
                    rName = GetGenericsForType(mi.ReturnType);
                    rName = LowerAndTrim(rName);
                    string association = mi.ReturnType.IsGenericType ? 
                rName : mi.ReturnType.FullName;
                    if (!Associations.Contains(association))
                    {
                        Associations.Add(association);
                    }
                    //do we want to display method return types
                    if (ShowMethodReturnValues)
                        mDetail += " : " + rName;
                }
                else
                {
                    //do we want to display method return types
                    if (ShowMethodReturnValues)
                        mDetail += " : void";
                }

                //work out whether this is a normal method, in which case add it
                //or if its a property get/set method, should it be added
                if (!ShowGetMethodForProperty && propGetters.Contains(mi))
                {
                    /* hidden get method */
                }
                else if (!ShowSetMethodForProperty && propSetters.Contains(mi))
                {
                    /* hidden set method */
                }
                else
                {
                    Methods.Add(new SerializableMethodData(mDetail, 
            ReadMethodBodyAndAddAssociations(mi)));
                }
            }
        }
        HasMethods = Methods.Any();
    }

    ......
    ......
    ......
    ......
    ......
    ......
    ......
    ......

    /// <summary>
    /// Returs a string which is the name of the type in its full
    /// format. If its not a generic type, then the name of the
    /// t input parameter is simply returned, if however it is
    /// a generic method say a List of ints then the appropraite string
    /// will be retrurned
    /// </summary>
    /// <param name="t">The Type to check for generics</param>
    /// <returns>a string representing the type</returns>
    private string GetGenericsForType(Type t)
    {
        string name = "";
        if (!t.GetType().IsGenericType)
        {
            //see if there is a ' char, which there is for
            //generic types
            int idx = t.Name.IndexOfAny(new char[] { '`', '\'' });
            if (idx >= 0)
            {
                name = t.Name.Substring(0, idx);
                //get the generic arguments
                Type[] genTypes = t.GetGenericArguments();
                Associations.AddRange(genTypes.Select(x => x.FullName));

                //and build the list of types for the result string
                if (genTypes.Length == 1)
                {
                    name += "<" + GetGenericsForType(genTypes[0]) + ">";
                }
                else
                {
                    name += "<";
                    foreach (Type gt in genTypes)
                    {
                        name += GetGenericsForType(gt) + ", ";
                    }
                    if (name.LastIndexOf(",") > 0)
                    {
                        name = name.Substring(0, name.LastIndexOf(","));
                    }
                    name += ">";
                }
            }
            else
            {
                name = t.Name;
            }
            return name;
        }
        else
        {
            return t.Name;
        }
    }
    #endregion
}

注意:我上面只展示了反射方法出的代码,但其他反射方法与方法的方法非常相似,我想你懂这个意思。

如何找到关联

在新版 AutoDiagrammer 中,我最满意的一点就是它如何找到类之间的关联。以下是查找从一种类型到另一种类型的关联的一般规则:

  1. 如果存在到不同类型的属性
  2. 对于每个指向不同类型的支持字段
  3. 对于每个指向不同类型的构造函数参数
  4. 对于每个指向不同类型的方法参数
  5. 在解析方法体时发现的每个指向不同类型的 NEWOBJ IL 指令

除了第 5 点之外,大部分都是非常简单/标准的反射代码,我想花一点时间讲解第 5 点。

那么,为了实现这一点,做了什么呢?嗯,我们所做的就是,对于我们看到的每个方法,我们加载该方法的 IL 指令,并查找任何正在实例化新对象的指令,然后我们查看新对象的类型,并确定是否将新对象实例作为关联添加到当前正在反射的类型中。

以下是相关代码

/// <summary>
/// Code in this method does the following
/// 1. Read the methodbodyIL string
/// 2. Look at all ILInstructions and look for new objects being 
///    created inside the method, and add as Association
/// 3. Finally return the method body IL for the diagram to use
/// </summary>
private String ReadMethodBodyAndAddAssociations(MethodInfo mi)
{
    String ilBody = "";

    try
    {
        if (mi == null)
            return "";

        if (mi.GetMethodBody() == null)
            return "";

        MethodBodyReader mr = new MethodBodyReader(mi);

        foreach (ILInstruction instruction in mr.Instructions)
        {
            if (instruction.Code.Name.ToLower().Equals("newobj"))
            {
                dynamic operandType = instruction.Operand;
                String association = operandType.DeclaringType.FullName;
                if (!Associations.Contains(association))
                {
                    Associations.Add(association);
                }
            }
        }
        ilBody = mr.GetBodyCode();
        return ilBody;
    }
    catch (Exception ex)
    {
        return "";
    }
}

此代码使用了 MethodBodyReader,它可以在 Sorin Serban 在 www.codeproject.com 上发表的文章“解析方法体的 IL”中找到。Sorin,干得漂亮,谢谢!

创建图表

图表显然基于某种图形。我很幸运在之前的文章中尝试过一个相当酷的 WPF 图形库,所以选择非常容易,只需使用我以前用过的 Graphsharp,它是一个非常易于使用的 WPF 图形库。

一旦所有类(在 Graphsharp 语言中是 Vertex)都被反射出来,图表的创建实际上非常简单;所有需要做的事情如下:

MainWindowViewModel CommenceDrawingCommand 大致执行以下操作:

private void ExecuteCommenceDrawingCommand(Object parameter)
{
    try
    {
        ......
        ......
        //Get a task that returns the Graph Vertex/Edges
        Task<GraphResults> task =
            assemblyManipulationService.CreateGraph();

        int timeout = SettingsViewModel.Instance.GraphDrawingTimeOutInSeconds * 1000;

        bool finishedOk = task.Wait(timeout); // wait 20 seconds before timing out

        if (finishedOk)
        {
            AddItemsToGraph(task.Result);
            graphPrintableWindow.ZoomToFit();

            //TODO Need to also show the non connected ones in a ComboBox
            //which will launch the popup
            hasActiveGraph = true;
        }
        else
        {
            messageBoxService.ShowError(String.Format(
                "The generating of the class diagram took longer than {0} seconds, " + 
        "maybe try increase this setting and try again",
                SettingsViewModel.Instance.GraphDrawingTimeOutInSeconds));
        }
    }
    catch (AggregateException AggEx)
    {
        ......
        ......
    }
    finally
    {
        ......
        ......
    }
}

上述方法所依赖的艰苦工作已由整体过程的先前解释的反射阶段完成;实际上所发生的一切是,用户选择的任何类都被转换为 UI 就绪的基于 Graphsharp 的 Vertex/Edge 对象。这是通过 UI 服务 AssemblyManipulationService 实现的,该服务处理所有 Assembly 反射/AssemblyTreeViewModel 对象,并且有一个方法,其唯一工作是获取当前选定的 AssemblyTreeViewModel 并返回一个表示 UI 就绪的基于 Graphsharp 的 Vertex/Edge 对象的 GraphResults 对象。我们稍后会详细讨论 GraphResults 对象。

public Task<GraphResults> CreateGraph()
{
    Task<GraphResults> task = Task.Factory.StartNew<GraphResults>(() =>
        {
            //for each item in selectedTreeItems
            //1. Create all PocVertex, and are them to Reflect() which will store an internal
            //   List<Name> which are the Associations needed by that Vertex
            //2. Go through each PocVertex Associations and see if we have that Association
            //   Vertex and if so create a new PocEdge


            List<PocVertex> vertices = new List<PocVertex>();
            Parallel.For(0, selectedTreeValues.Count, (i) =>
            {
                SerializableVertex serializableVertex = selectedTreeValues[i].Vertex;
                PocVertex vertex = new PocVertex(
                    serializableVertex.Name,
                    serializableVertex.ShortName,
                    serializableVertex.Constructors,
                    serializableVertex.Fields,
                    serializableVertex.Properties,
                    serializableVertex.Interfaces,
                    TranslateMethods(serializableVertex.Methods),
                    serializableVertex.Events,
                    serializableVertex.Associations,
                    serializableVertex.HasConstructors,
                    serializableVertex.HasFields,
                    serializableVertex.HasProperties,
                    serializableVertex.HasInterfaces,
                    serializableVertex.HasMethods,
                    serializableVertex.HasEvents);


                vertices.Add(vertex);
            });

            List<PocEdge> edges = new List<PocEdge>();
            Parallel.ForEach(vertices, (x) =>
                {
                    PocVertex vertex1 = x;

                    foreach (String associationName in vertex1.Associations)
                    {
                        PocVertex vertex2 = (from vert in vertices
                                                where vert.Name == associationName
                                                select vert).SingleOrDefault();

                        if (vertex2 != null)
                        {
                            if (vertex1.Name != vertex2.Name)
                            {
                                //TODO : Need to make sure both of these are in the
                                //list of selected items in the tree before they are added
                                edges.Add(AddNewGraphEdge(vertex1, vertex2));
                                vertex1.NumberOfEdgesFromThisVertex += 1;
                                vertex2.NumberOfEdgesToThisVertex += 1;
                            }
                        }

                    }
                });
                    

            return new GraphResults(vertices, edges);

        });
    return task;
}

其中此方法返回的 Task.ResultMainWindowViewModel AddItemsToGraph() 方法使用,如下所示:

private void AddItemsToGraph(GraphResults graphResults)
{
    NotAssociatedVertices = graphResults.Vertices
                            .Where(v => v.NumberOfEdgesFromThisVertex == 0 &&
                                        v.NumberOfEdgesToThisVertex == 0)
                            .OrderBy(x => x.Name).ToList();
    HasNotAssociatedVertices = NotAssociatedVertices.Any();


    graph = new PocGraph(true);
    graphLayout.Graph = graph;

    graph.Clear();

    List<PocVertex> vertices = graphResults.Vertices
        .Where(v => v.NumberOfEdgesFromThisVertex > 0 ||
                    v.NumberOfEdgesToThisVertex > 0).ToList();

    foreach (PocVertex vertex in vertices)
    {
        if (vertex != null)
            graph.AddVertex(vertex);
    }

    foreach (PocEdge edge in graphResults.Edges)
    {
        if(edge != null)
            graph.AddEdge(edge);
    }

    NotifyPropertyChanged(graphLayoutArgs);
}

GraphResults 看起来像这样

public class GraphResults
{
    public List<PocVertex> Vertices { get; private set; }
    public List<PocEdge> Edges { get; private set; }

    public GraphResults(List<PocVertex> vertices, List<PocEdge> edges)
    {
        this.Vertices = vertices;
        this.Edges = edges;
    }
}

设置

正如文章中已经多次提到的那样,有一个单例 SettingsViewModel 用于控制与图表相关的设置。设置很多,总的来说,所有发生的只是属性值的更改。然而,有一点值得注意,那就是 SettingsViewModel 在 AutoDiagrammer 关闭/打开时将其设置持久化/水化到磁盘。

这可能很有趣。这实际上是通过使用 XLINQ 实现的;以下是 SettingsViewModel 最相关的代码:

namespace AutoDiagrammer
{
    public class SettingsViewModel : ValidatingViewModelBase
    {
        private IOverlapRemovalParameters 
                overlapRemovalParameters = new OverlapRemovalParametersEx();
        private Dictionary<String, ILayoutParameters> availableLayoutParameters = 
        new Dictionary<String, ILayoutParameters>();
        private List<String> layoutAlgorithmTypes = new List<string>();
        private ILayoutParameters layoutParameters = null;
        private string layoutAlgorithmType;
        
        private const string xmlFileName = "Settings.xml";
        private string xmlFileLocation;

        private bool showInterfaces = true;

        private static readonly Lazy<SettingsViewModel> instance = 
        new Lazy<SettingsViewModel>(() => new SettingsViewModel());

        /// <summary>
        /// Singleton instance
        /// </summary>
        public static SettingsViewModel Instance
        {
            get
            {
                return instance.Value;
            }
        }

        private void ExecuteSaveSettingsAsXmlCommand(Object parameter)
        {
            XElement settingsXml = new XElement("settings");
            foreach (KeyValuePair<String, ILayoutParameters> 
                     layoutKVPair in availableLayoutParameters)
            {
                if (layoutKVPair.Value is ISetting)
                {
                    settingsXml.Add((layoutKVPair.Value as ISetting).GetXmlFragement());
                }
            }
            settingsXml.Add((overlapRemovalParameters as ISetting).GetXmlFragement());

            //Add misc settings
            settingsXml.Add(new XElement("setting", 
               new XAttribute("type", "LayoutAlgorithmType"),
               new XElement("SelectedType", LayoutAlgorithmType)));
            settingsXml.Add(new XElement("setting", 
               new XAttribute("type", "GeneralSettings"),
               new XElement("ShowInterfaces", ShowInterfaces),
                .....
                .....
                .....
                                ));
            settingsXml.Save(xmlFileLocation);
        }

        private void ExecuteRehydrateSettingsFromXmlCommand(Object parameter)
        {
            if (!File.Exists(xmlFileLocation))
                return;

            XElement settingsXml = XElement.Load(xmlFileLocation);

            foreach (XElement el in settingsXml.Elements("setting"))
            {
                string typeOfSetting = el.Attribute("type").Value;
                switch (typeOfSetting)
                {
                    case "Overlap":
                        (overlapRemovalParameters as ISetting).SetFromXmlFragment(el);
                        break;
                    case "LayoutAlgorithmType":
                        LayoutAlgorithmType = el.Descendants()
                .Where(x => x.Name.LocalName == "SelectedType").Single().Value;
                        break;
                    case "GeneralSettings":
                        ShowInterfaces = Boolean.Parse(el.Descendants()
                .Where(x => x.Name.LocalName == "ShowInterfaces").Single().Value);
                        ....
                        ....
                        ....
                        break;
                    default:
                        ISetting setting = (ISetting)availableLayoutParameters[typeOfSetting];
                        setting.SetFromXmlFragment(el);
                        break;
                }
            }
        }
    }
}

对于简单的单值属性,我们只使用新的 XLINQ XElement。然而,一些 Graphsharp 设置是具有许多属性的复杂类型。为了处理这些,我们只是扩展了原始的 Graphsharp 设置类,并允许创建/检索 XML 片段,如下所示。

public interface ISetting
{
    void SetFromXmlFragment(XElement fragment);
    XElement GetXmlFragement();
}

public class BoundedFRLayoutParametersEx : BoundedFRLayoutParameters, ISetting
{
    public void SetFromXmlFragment(XElement fragment)
    {
        Width = Double.Parse(fragment.Descendants()
        .Where(x => x.Name.LocalName == "Width").Single().Value);
        Height = Double.Parse(fragment.Descendants()
        .Where(x => x.Name.LocalName == "Height").Single().Value);
        AttractionMultiplier = Double.Parse(fragment.Descendants()
        .Where(x => x.Name.LocalName == "AttractionMultiplier").Single().Value);
        RepulsiveMultiplier = Double.Parse(fragment.Descendants()
        .Where(x => x.Name.LocalName == "RepulsiveMultiplier").Single().Value);
        IterationLimit = Int32.Parse(fragment.Descendants()
        .Where(x => x.Name.LocalName == "IterationLimit").Single().Value);
    }

    public XElement GetXmlFragement()
    {
        return
            new XElement("setting", new XAttribute("type", "BoundedFR"),
                    new XElement("Width", Width),
                    new XElement("Height", Height),
                    new XElement("AttractionMultiplier", AttractionMultiplier),
                    new XElement("RepulsiveMultiplier", RepulsiveMultiplier),
                    new XElement("IterationLimit", IterationLimit));
    }
} 

AutoDiagrammer 使用的所有其他 Graphsharp 设置都以相同的方式工作。

保存为 PNG

我希望能够保存为 Windows 本身支持且有本地查看器的格式。为此,我选择了 PNG(便携式网络图形)作为格式。以下是我将图表保存为 PNG 文件的方法。

这是允许保存为 PNG 的服务

[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(ISavePNGFileService))]
public class SavePNGFileService : ISavePNGFileService
{
    public bool Save(string filePath, FrameworkElement visual)
    {
        try
        {
            RenderTargetBitmap bmp = new RenderTargetBitmap(
		(int)visual.ActualWidth, (int)visual.ActualHeight, 96, 96, PixelFormats.Pbgra32);
            bmp.Render(visual);
            PngBitmapEncoder png = new PngBitmapEncoder();
            png.Frames.Add(BitmapFrame.Create(bmp));

            using (Stream stm = File.Create(filePath))
            {
                png.Save(stm);
            }
            return true;
        }
        catch
        {
            return false;
        }
    }
}

以及它在代码其余部分中的使用方式,可以看到我们只需传递要保存的文件名和要打印到 PNG 的 UIElement,在本例中是实际的图表 UIElement

private void ExecuteSaveCommand(Object parameter)
{
    isGenerallyBusy = true;
    try
    {
        saveFileService.InitialDirectory = @"c:\temp";
        saveFileService.OverwritePrompt = true;
        saveFileService.Filter = "*.PNG | PNG Files";

        bool? result = saveFileService.ShowDialog(null);
        String filePath = saveFileService.FileName;

        if (!filePath.ToLower().EndsWith(".png"))
            filePath += ".png";

        if (result.HasValue && result.Value)
        {
            FrameworkElement visual = graphPrintableWindow.GetGraphToPrint;
            Double currentZoom = graphPrintableWindow.Zoom;
            graphPrintableWindow.Zoom = 1.0;

            if (savePNGService.Save(filePath, visual))
            {
                messageBoxService.ShowInformation(string.Format("Sucessfully saved file to {0}", filePath));
            }
            else
            {
                messageBoxService.ShowError(string.Format("Error saving file {0}", filePath));
            }

            graphPrintableWindow.Zoom = currentZoom;
        }
    }
    finally
    {
        isGenerallyBusy = false;
    }
            


}

上面还有一些额外的复杂性,即实际的图表在一个 ZoomControl 中(它是 WPFExtensions CodePlex 项目的一部分),因此您必须考虑当前的缩放,然后将图表设置为 Zoom = 1.0,然后保存 PNG,然后再将缩放重置为之前的值。

打印

AutoDiagrammer.exe 允许打印。这是通过 AutoDiagrammer.exe 中的打印按钮实现的,点击该按钮将显示打印对话框。

这是实现 PNG 文件打印的 UI 服务代码:

/// <summary>
/// This class implements the IPrintPNGFileService
/// </summary>
[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(IPrintPNGFileService))]
public class PrintPNGFileService : IPrintPNGFileService
{
    #region Data

    /// <summary>
    /// Embedded PrintDialog to pass back correctly selected
    /// values to ViewModel
    /// </summary>
    private PrintDialog pd = new PrintDialog();
    private String filename = "";
    #endregion

    #region Ctor
    public PrintPNGFileService()
    {
        pd.PageRangeSelection = PageRangeSelection.AllPages;
        pd.UserPageRangeEnabled = true;
    }
    #endregion

    #region IPrintPNGFileService Members
    /// <summary>
    /// Prints the file
    /// </summary>
    /// <returns>Exception if the printing failed, otherwise null</returns>
    public Exception Print(FrameworkElement visual)
    {
        try
        {
            // Display the dialog. This returns true if the user presses the Print button.
            Nullable<Boolean> print = pd.ShowDialog();
            if (print.HasValue && print.Value)
            {
                pd.PrintVisual(visual, string.Format("AutoDiagrammerPNGExport_{0}", DateTime.Now));
                return null;
            }
            else
            {
                return null;
            }
        }
        catch (Exception ex)
        {
            return ex;
        }
    }

    /// <summary>
    /// PageRangeSelection : Simply use embedded PrintDialog.PageRangeSelection
    /// </summary>
    public PageRangeSelection PageRangeSelection
    {
        get { return pd.PageRangeSelection; }
        set { pd.PageRangeSelection = value; }
    }


    #endregion
}

集成帮助

AutoDiagrammer 实际上包含一个嵌入式帮助系统,可以通过 *AutoDiagrammer.exe* 中的帮助按钮访问。

当点击此按钮时,它只是在一个 WPF Window 中托管的 WebBrowser 控件中显示一个嵌入式 HTML 文件。

以下是其运行时的屏幕截图

如果您感兴趣,以下是使用正确的 HTML 文件填充 WebBrowser 的代码

public HelpPopup()
{
    InitializeComponent();
    FileInfo assLocation = new FileInfo(Assembly.GetExecutingAssembly().Location);
    String helpFileLocation = Path.Combine(assLocation.Directory.FullName, 
        @"HtmlHelp/AutoDiagrammerHelp.htm");
    if (File.Exists(helpFileLocation))
    {
        wb.Navigate(new Uri(helpFileLocation, UriKind.RelativeOrAbsolute));
    }
    else
    {
        throw new ApplicationException(String.Format(
          "Can not find the file {0}\r\n\r\nThe " + 
          "AutoDiagrammer.exe help file 'AutoDiagrammerHelp.htm' " +
          "and all related help file images are expected to be " + 
          "located in a subdirectory under {1} called 'HtmlHelp'",
          helpFileLocation, assLocation));
    }
}

特别鸣谢

特别感谢

就这样

至此,新版 AutoDiagrammer 的介绍就结束了。我希望您能看到,这篇新文章及其相关代码实际上比旧版 AutoDiagrammer 代码好得多。如果您认为这些新代码可能对您有用,您能否抽空投一票/评论一下?万分感谢。

历史

  • 2011年6月6日:首次发布。
  • 08/06/2011:
    • 修复了设置的文化解析问题。
    • 添加了新设置来控制图表上的项目数量。
    • 还添加了选择是否解析方法体 IL 的功能。
    • 还添加了右键上下文菜单,允许在图表上一次性切换类的所有部分。
  • 13/06/2011:
    • 添加了鼠标滚轮缩放功能。
    • 添加了拖放单个 DLL/Exe 的功能,以及传统的 OpenFileDialog 支持。
  • 27/09/2011
    • 添加了额外的设置,用于控制构造函数/字段/属性/方法如何添加到绘制的关联线中。
    • 修复了 TreeCreator.cs 中的一个小错字。
© . All rights reserved.