Click here to Skip to main content
15,860,972 members
Articles / Desktop Programming / WPF

200% Reflective Class Diagram Creation Tool

Rate me:
Please Sign up or sign in to vote.
4.92/5 (200 votes)
20 Feb 2014CPOL22 min read 572.9K   11.5K   437   262
WPF: Version II of my 100% Reflective class diagram creation tool.

Table of Contents 

Introduction

Some of you may have been www.codeproject.com members for quite some time, and may recall about 4 years ago, I published an article which drew a class diagram from a DLL/Exe called "AutoDiagrammer". I was quite lucky with that article, as it turned out to be very popular, and got loads of votes and a ton of views. Basically people seemed to love it, which is ace... I was very happy with that. Every author here wants people to like the stuff they publish (myself included, it's the whole vanity thing I suppose).

Thing is, I wrote that original article a long time ago, when I was just getting into WPF, and although I was pretty happy with it, I always thought it could be so much better. It was also WinForms, so fast forward a couple of years, I now know enough WPF to really do justice to the original article and get it to how I always envisaged it could be.

The things that I felt were wrong with the first "AutoDiagrammer" article were as follows:

  1. The drawing of class associations was based on a grid layout.
  2. The user could not move the classes around on the design surface; once they were laid out, that was it.
  3. The Association lines were not that clear to see.
  4. The user could not scale the produced diagram that well (it was possible, but was not that great).
  5. The loading of the DLL/Exe to be drawn as a class diagram was done in the same AppDomain as the AutoDiagrammer.exe app, so when reflecting, it would be forced into loading all the additionally reflected types from the DLL/Exe into the AutoDiagrammer.exe app's AppDomain. Ouch...not cool.
  6. People found it slightly cumbersome figuring out how to get a diagram actually produced.

That said, there were things I feel I definitely got right such as:

  1. The overall idea (people seemed to generally like it, and find it a very useful tool).
  2. The reflecting of information was correct.
  3. The ability to fine tune what was show on the diagram.

With all these good and bad points in mind. coupled with the fact that I now know enough WPF to do the original code justice and do it how I always wanted to do it, I thought.. yes, the time is right, do a complete re-write of the original article.

So that is what this article is, it is a complete re-write of the original "AutoDiagrammer" article; the feature list of this new articles code is:

  1. Detection of valid .NET assembly (yes, same as the first article, this tool only works with .NET Assemblies).
  2. The ability to move the classes in the design surface within the diagram.
  3. The ability to not show classes that did not have any associations on the diagram, but still allow the user the ability to view these from a drop down list. This aids in keeping the diagram clutter-free, only show what is absolutely needed.
  4. Persisted settings, so that the next time the application is run, your personal settings will be as you left them.
  5. Proper scaling of objects as vectors, so whatever scale the diagram is viewed at, the objects are as clear as they could be.
  6. Saving of the diagram to an PNG file which can easily be viewed using the standard Window Image Viewer.
  7. Printing to a printer.
  8. Loading the DLL/Exe into a separate AppDomain that does not polute the AutoDiagrammer.exe AppDomain with the loaded DLL/Exe types.
  9. Integrated help.
  10. The classes show a full Association popup with all Associations shown as a list of strings.
  11. The Association lines show in a different color when the user hovers over them with the mouse.
  12. Better detection of Associations between classes via parsing of method body IL (Intermediate Language).
  13. The ability to view method body IL (Intermediate Language).

As you can see, I have kept what was good with the old article/codebase and have added more features to it. I am really happy with how it turned out and I hope you will be too.

What Does it Look Like

I think the best way to show you how this all looks is with a couple of screenshots, so let's look at a few. Then we will look at how to use this new version of AutoDiagrammer, which I have cunningly called AutoDiagrammer II. Nice, huh?

When the app starts, it looks like this, where it is waiting for you to pick a DLL/Exe to draw the class diagram of.

Image 1

And after you click the "Open Dll/Exe" button and navigate to a valid .NET DLL/Exe, this is what it might look like (Note: I have created a dummy test DLL to test it with, so that is what is shown above):

Image 2

This is now waiting for you to pick the items that you wish to appear on the drawn diagram. This is much easier than the first AutoDiagrammer article, as all you do now is select using the check boxes beside each TreeViewItem and then click the "Draw Icon" above the TreeView.

The next step would be to click the "Draw Icon", but before we do that, let's consider my small test case DLL, which is shown in its entirety below:

C#
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);

        }
    }
}

Based on this code, this is what will get drawn:

Image 3

Now there are a couple of things of note right there, such as:

  1. Not all the classes are shown on the diagram, the "Doer" class is not shown! Why is that? Well, it has no associations so is not on the diagram, but is instead in the list of "Non Associated Items" in the top right, from where you can view the class.
  2. The association from Class1 to ConcreteRenderer was found, this is thanks to the parsing of the method body IL within the Class1.DoIt() method.
  3. Association lines are colored differently when you hover over a class.
  4. We can make the diagram take up all the width of the page, and hide the left hand pane altogether.

Now let's see some other features such as viewing the Associations, which is possible by clicking on the Show Associations icon in the top right hand side of a class.

Image 4Image 5

And how about being able to view the IL for a method? That's possible, right? Yes, just click the magnifying glass next to a method (as long as it is enabled) and you will be shown an IL window with the method body IL in it. If I had an infinite amount of time, I could probably get this into C#, but I just don't right now, so IL it is. Heck there may even be some of you that can read IL as well as read C#, who knows.

Image 6

How to Use the New AutoDiagrammer

The following sections will give you an overview of how to use the new version of AutoDiagrammer. Note, I have covered some of this ground whilst showing the screenshots above, so please forgive me for that.

Installing AutoDiagrammer

When you download and build the attached code, you will see the following in the bin/XXXX folder:

Image 7

All you need to do is copy the entire bin/XXXX folder to a new folder where you wish to run AutoDiagrammer.exe from. That is all you need to do. Then to run AutoDiagrammer.exe, just double click on it wherever you copied the files and it should work just fine.

Load a DLL/Exe

This is the easy bit, all you need to do is pick the "Open" button and then choose whether it is a DLL or Exe you wish to open, and then browse to the location of the file you wish to open.

Image 8

Picking the Classes to Draw

Once you have picked which DLL/Exe to load, you will get a populated TreeView which is organized into namespaces that the classes reside in. This process may take a while as this is where the bulk of the work is done reflecting out the information from the requested DLL/Exe, and there is also a timeout on this process, which can be adjusted from the Settings window.

Image 9

Whilst the treeview is being generated, you will see this loading banner:

Image 10

Assuming you have a loaded TreeView, all you need to do now is pick which classes you would like to appear on the diagram. This is done by simply using the CheckBoxes beside the names of the classes, or beside the entire namespace, or even the whole TreeView. Then click on the Draw button (yes, the one shown with the pencil).

Image 11

After you have clicked which classes to draw and clicked the Draw button, you will see a second loading screen (that will be of the form shown below) whilst the diagram is created.

Image 12

After the diagram has been created (or a timeout occurs, which again can be adjusted via the Settings window):

Image 13

A new diagram will be shown (or in the case of a timeout, the last diagram which was successfully loaded (if there was one)):

Image 14

Note: All the classes may not appear on the diagram, as only those classes which have associations are shown on the actual diagram. This is to de-clutter the diagram of information that is not really that valid. Not associated classes may still be viewed, which is explained below.

Working With the Drawn Class Diagram

Classes

Each class has several possible parts (again, the diagram will not include these parts if there is nothing to show or if the user has requested these parts not be shown). The possible parts that may show up on a given class are as follows:

  • Interfaces
  • Constructors
  • Fields
  • Events
  • Properties
  • Methods

Here is what a typical example of a class may look like:

Image 15

Viewing Method Body IL

One thing that is also quite useful is that you can view the method body IL using the small magnifying glass icon shown:

Image 16

Important note: Each of these sections is within an expandable region. There is also a setting that you can use to determine if the class diagram is redrawn when these sections are expanded. The default is that the diagram will not be drawn again on expand/collapse; if this does not suit you, feel free to use the system setting to alter this behaviour.

Image 17

Viewing Associations

It is also possible to view all the associations for a given class by hovering over the class, which will show a popup of the associations:

Image 18

Image 19

Not Associated Classes

As stated earlier, the diagram is kept free of clutter by not placing any classes that do not have Associations actually on the diagram. These classes are still available, but they are just not on the main diagram, and must be accessed using the Not Associated Items dropdown, which will only be shown if there are classes with no Associations that the user selected to draw.

Image 20

Picking one of these will simply show a popup window with the details about the selected class using the same sections as if it was part of the main diagram.

Image 21

Saving

The diagram can be saved to an PNG, using the Save button provided.

Image 22

Which can then be viewed in the standard Image Viewer

Printing

The diagram may be printed using the Print button provided.

Image 23

Customising What is Shown on a Diagram

The Settings window allows the diagram to be tailored to your specific requirements. These settings will be saved to disk whenever you close AutoDiagrammer.exe, and are reloaded when you next run AutoDigrammer.exe.

Image 24

There are many settings here to control not only what is shown on the diagram but also what graph layout algorithm the diagram should use. The default graph layout algorithm is "Efficient Sugiyama", but you may find that another graph layout may be better for your diagram needs. This will largely be down to experimenting with what your diagram shows and what works best for you.

Graph Layout Algorithm

The Settings windows allow the user to pick between different graph layout algorithms that may be used when creating the diagram. As previously mentioned, the default graph layout algorithm is "Efficient Sugiyama", but there are several other layout algorithms, as shown in the list below:

  1. Bounded FR
  2. Efficient Sugiyama
  3. FR
  4. ISOM
  5. KK
  6. Tree

These layout algorithms may or may not suit your specific diagramming needs. This will largely be down to trial and error. However, all settings are persisted, so when you exit AutoDiagrammer and come back to it, rest assured it will be how you left it.

What is common for all the different layout algorithms is that they have many different parameters that you can play with. For example, here is a different set of settings that are available for the "Efficient Sugiyama" layout algorithm:

Image 25

And here are the settings for the "Tree" layout algorithm:

Image 26

I would suggest that once you have an active diagram with classes and Associations, you open the Settings window and experiment with the different layout algorithms and then hit the "Re-Layout Graph" button at the top right hand corner (shown highlighted in figures above) to find what works best for you.

Picking What to Show

AutoDiagrammer also has many, many settings which control how the diagram will be drawn. These settings are shown below:

Image 27

A lot of these are kind of pro-active settings in the sense that you will not need to Re-Layout the diagram again. This is down to the fact that the diagram binds directly to the settings ViewModel which is a singleton instance. Obviously, the timeouts will only take effect the next time a new diagram is created.

How it Works

The following subsections will hopefully give you an understanding of how the new AutoDiagrammer code hangs together. One thing I should just mention now is that it is based on WPF and MVVM. Now, some of you may know that I authored my own MVVM framework called Cinch which I pretty much use for any MVVM development I do when I write something in WPF. And this article is no different; as such, you will find that I do indeed use Cinch and MVVM. If you are not familiar with Cinch or MVVM, you may want to read about those first. If you are happy with these two things, no problem, let's continue.

The Basic Idea

Before we get into the nitty gritty, let's just go through the basic idea in dead simple numbered steps of what we are trying to achieve:

  1. Allow the user to open a DLL/Exe, which is then examined to see if it is a valid .NET assembly. If it not, quit after telling the user why. If it is valid, go to step 2.
  2. Load the valid .NET assembly in a new AppDomain and extract the treeview information and all the class information like interfaces/methods (including the method body IL)/properties/events etc.
  3. Use the data from step 2 to draw a treeview of the namespaces and types found.
  4. Allow the user to select what types they wish to draw.
  5. For all the user selected types within the treeview which was created in step 4, create the actual graph objects which will represent the selected type.
  6. For all those graph objects from step 5 that have Associations, add these objects to a Graphsharp graph.
  7. For all those graph objects from step 5 that do not have Associations, add these objects to a combobox such that the user can view these but they are not part of the main diagram.

In a nutshell, this is how the diagram is created. There are obviously other areas that are not directly related to the creation of the diagram such as settings/help etc., but we will cover those too, do not worry.

There are however a few supporting classes that I will not be going into, as I just don't think it is all that necessary, but obviously, the code is attached to this article, and if you are curious about one of the classes I do not cover, add a query to this article forum and I will answer it.

Detecting if a DLL/Exe is .NET

AutoDiagrammer.exe only supports the rendering of types that are found inside a valid .NET DLL/Exe. This is easily achieved using the following helper class:

C#
/// <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
}

Examining the Types in a Separate AppDomain

One of the main things that AutoDiagrammer will be doing is examining DLL/Exe files and reflecting out information from those loaded files. Which sounds easy enough, but if you are not careful, this reflected information will be loaded into your current AppDomain. Yes, all the reflected types will be loaded into the current AppDomain. The old AutoDiagrammer did not make any provision for this obvious oversight.

However, the new AutoDiagrammer presented in this article does fix all this by making sure that the loaded DLL/Exe is examined using Reflection in its own AppDomain which is unloaded when the Reflection process is finished. This ensures that none of the type information in the reflected DLL/Exe is serialized into the current AppDomain.

This is a fairly complex bit of code, and would take many, many code listings to fully explain, so I will keep things brief. The general rule of thumb though when working with another AppDomain where your code relies on data structures of some sort being returned from the code in the new AppDomain, is the data structures themselves must be Serializable to allow them to be serialized back into the primary AppDomain. The other trick is that your secondary AppDomain loader should inherit from MarshallByRefObject, which allows it to be unwrapped in the primary AppDomain.

It it also advisable to apply the same Evidence to the new AppDomain as the primary AppDomain.

As I say, there is way too much code to go through this in fine detail; instead, I will show you the core pieces, and that should allow you to understand where to look in the attached code, if that is of interest to you.

Most of the work is achieved using the following code:

C#
[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
}

This class is responsible for creating the new AppDomain and creating the TreeView that you see when you run the AutoDiagrammer app. You can see that the method:

C#
List<AssemblyTreeViewModel> ScanAssemblyAndCreateTree(String assemblyFileName);

which is the only method that is exposed on the TreeCreator service, returns a List<AssemblyTreeViewModel> where AssemblyTreeViewModel is serializable (as it is returned from the new AppDomain to the primary AppDomain).

C#
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; }
    ....
    ....
    ....
    ....
    ....
}

These represent the TreeView items, and also hold an internal reference to a SerializableVertex which represents the class information found. So how is it that one of these AssemblyTreeViewModel objects is constructed with a fully populated SerializableVertex?

Well, if you look closely at the end of the TreeCreator code above (look at the

AddTypes(IGrouping<String, Type> types, 
AssemblyTreeViewModel parent)
method), you will see lines like these:

C#
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);

It can be seen that we make use of a little helper class called TypeReflector which does all the work for us. We will look at that next. From this code, you can see, by the time we return a  List<AssemblyTreeViewModel> from the TreeCreator service, we have already reflected out all the information we need from the loaded DLL/Exe.

Reflecting Out Class Data

As stated above, the  TreeCreator service is the code that is responsible for loading and reflecting the DLL/Exe data in a separate AppDomain, where the result is a List<AssemblyTreeViewModel> where each AssemblyTreeViewModel is constructed with a fully populated SerializableVertex which is later used to draw the Graphsharp graph (the diagram essentially).

So let's see how one of these SerializableVertex objects is created.

Recall, I stated that we use a helper class called TypeReflector which looks like this:

C#
[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
}

Note: I have only shown the code above for reflecting out methods, but the other reflective methods are pretty similar to the one for the methods, I think you get the idea though.

How Associations are Found

One of the things that I am most happy with in this new version of AutoDiagrammer is how the Associations between classes are found. Here are the general rules of how Associations from one type to another type are found:

  1. If there is a property to a different type
  2. For each backing field to a different type
  3. For each constructor parameter to a different type
  4. For each method argument to a different type
  5. For each NEWOBJ IL instruction found when parsing a method body to a different type

Most of this is dead simple/standard Reflection code, apart from point 5, which is what I want to spend a little bit of time on.

So what is done to achieve that? Well, what we do is that for each method we see, we load up the IL instructions for that method, and look for any new objects being instantiated, and we look at the type of the new object and work out whether to add the new object instance as an Association to the type currently being reflected.

Here is the relevant code:

C#
/// <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 "";
    }
}

This code makes use of MethodBodyReader which can be found in the www.codeproject.com article by Sorin Serban: "Parsing the IL of a Method Body". Great work Sorin, thanks for that. 

Creating the Diagram

The diagram is obviously based on a graph of some sort. I am lucky enough to have messed around with a rather cool graph for WPF in a previous article, so the choice was very easy, just use what I had used before, which is Graphsharp, which is a very easy to use WPF graphing library.

Once all the classes (Vertex in Graphsharp language) have been reflected, the creating of the diagram is actually pretty simple; all that has to be done is as follows:

The MainWindowViewModel CommenceDrawingCommand does roughly this:

C#
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
    {
        ......
        ......
    }
}

The hard work that aids the method above has already been done by the previously explained reflection stages of the overall process; all that is really happening is that whatever classes were selected by the users are then turned into UI ready Graphsharp based Vertex/Edge objects. This is achieved by the use of the UI service AssemblyManipulationService which deals with all the Assembly reflection/AssemblyTreeViewModel objects, and has this method whose sole job it is to take the currently selected AssemblyTreeViewModel and return a GraphResults object which represents UI ready Graphsharp based Vertex/Edge objects. We will talk more about the GraphResults object in a minute.

C#
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;
}

Where the returning Task.Result from this method is used by the MainWindowViewModel AddItemsToGraph() method, which is as follows:

C#
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);
}

And where the GraphResults look like this:

C#
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;
    }
}

Settings

As already mentioned throughout the article, there is a singleton SettingsViewModel which is used to control the settings associated with the diagram. There are numerous settings, and by and large, all that happens is that a property value is changed. However, there is one point of interest in that the SettingsViewModel persists/hydrates its settings to disk whenever AutoDiagrammer closes/opens.

That may be of interest. This is actually achieved through the use of XLINQ; here is the most relevant code for the SettingsViewModel:

C#
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;
                }
            }
        }
    }
}

Where for simple single valued properties, we just use a new XLINQ XElement. However, some of the Graphsharp settings are complex types with many properties. To handle these, we just extend the original Graphsharp settings classes and allow the creation/retrieval of an XML fragment, as shown below.

C#
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));
    }
} 

All the other Graphsharp settings that AutoDiagrammer uses work in the same manner.

Saving to PNG

I wanted to be able to save to a format that I know has native support on Windows, and that has a native viewer available. To this end, I chose to use PNG (Potrable Network Graphic) as the format. Here is how I save the diagram to an PNG file.

Here is the service that allows saving to PNG:

C#
[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;
        }
    }
}

And here is how it is used within the rest of the code, where it can be seen that we simply pass along the name of the file to save and a UIElement to print to PNG, which in this case is the actual diagram UIElement.

C#
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;
    }
            


}

There is some added complexity above in that the actual diagram is inside a ZoomControl (which is part of the WPFExtensions CodePlex project), so you have to take into account the current zoom, then set the diagram to Zoom = 1.0, and then save the PNG, and then reset the zoom to its previous value.

Print

AutoDiagrammer.exe allows it to be printed. This is achieved using the print button within the AutoDiagrammer.exe, which when clicked will show a print dialog.

Image 28

Here is the UI Service code that achieves the printing of an PNG file:

C#
/// <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
}

Integrated Help

AutoDiagrammer actually includes an embedded help system which is available by using the help button within AutoDiagrammer.exe.

Image 29

When this button is clicked, it simply shows an embedded HTML file in a WebBrowser control which is hosted inside a WPF Window.

Here is a screenshot of it in action:

Image 30

Here is the code that populates the WebBrowser with the correct HTML file, in case you are interested:

C#
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));
    }
}

Special Thanks

Special thanks go out to:

That's It

That brings us to the end of this new version of AutoDiagrammer. I hope you can see that this new article and its associated code is actually a lot better than the old AutoDiagrammer code. If you think this new code may be of use to you, could you spare the time to add a vote/comment? It would be most appreciated, thanks.

History

  • 06/06/2011: Initial issue.
  • 08/06/2011:
    • Fixed the Culture parsing of the settings. 
    • Added new settings to control the number of items on the diagram. 
    • Also added the ability to opt in/out of parsing method body IL. 
    • Also added right click context menu to allow all parts of classes on diagram to be toggled as one.
  • 13/06/2011:
    • Added MouseWheel zooming. 
    • Added ability to drag on single DLL/Exe as well as traditional OpenFileDialog support.
  • 27/09/2011
    • Added extra settings to control how constructors/fields/properties/methods add to drawn association lines.
    • Fixed small typo in TreeCreator.cs 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralRe: My vote of 5 Pin
Sacha Barber19-Sep-11 4:56
Sacha Barber19-Sep-11 4:56 
GeneralRe: My vote of 5 Pin
taony19-Sep-11 5:11
taony19-Sep-11 5:11 
GeneralRe: My vote of 5 Pin
Sacha Barber19-Sep-11 5:30
Sacha Barber19-Sep-11 5:30 
QuestionGreat article Pin
Quinton Viljoen21-Aug-11 18:48
Quinton Viljoen21-Aug-11 18:48 
AnswerRe: Great article Pin
Sacha Barber22-Aug-11 7:43
Sacha Barber22-Aug-11 7:43 
Questionmscorlib.dll Pin
samster125618-Aug-11 23:46
samster125618-Aug-11 23:46 
AnswerRe: mscorlib.dll Pin
Sacha Barber18-Aug-11 23:53
Sacha Barber18-Aug-11 23:53 
GeneralMy vote of 5 Pin
gokulnathm10-Aug-11 11:09
gokulnathm10-Aug-11 11:09 
GeneralRe: My vote of 5 Pin
Sacha Barber12-Aug-11 21:30
Sacha Barber12-Aug-11 21:30 
Questionme vote #5 Pin
BillWoodruff9-Aug-11 15:33
professionalBillWoodruff9-Aug-11 15:33 
AnswerRe: me vote #5 Pin
Sacha Barber9-Aug-11 19:34
Sacha Barber9-Aug-11 19:34 
QuestionSystem.StackOverflowException when loading an assembly Pin
Espen Harlinn9-Aug-11 12:19
professionalEspen Harlinn9-Aug-11 12:19 
AnswerRe: System.StackOverflowException when loading an assembly Pin
Sacha Barber9-Aug-11 19:35
Sacha Barber9-Aug-11 19:35 
GeneralRe: System.StackOverflowException when loading an assembly Pin
Espen Harlinn10-Aug-11 0:47
professionalEspen Harlinn10-Aug-11 0:47 
GeneralRe: System.StackOverflowException when loading an assembly Pin
Espen Harlinn29-Aug-11 1:17
professionalEspen Harlinn29-Aug-11 1:17 
GeneralRe: System.StackOverflowException when loading an assembly [modified] Pin
Sacha Barber5-Sep-11 22:18
Sacha Barber5-Sep-11 22:18 
GeneralRe: System.StackOverflowException when loading an assembly Pin
Espen Harlinn6-Sep-11 0:08
professionalEspen Harlinn6-Sep-11 0:08 
GeneralRe: System.StackOverflowException when loading an assembly Pin
Sacha Barber6-Sep-11 5:34
Sacha Barber6-Sep-11 5:34 
GeneralRe: System.StackOverflowException when loading an assembly Pin
Sacha Barber9-Oct-11 23:56
Sacha Barber9-Oct-11 23:56 
GeneralRe: System.StackOverflowException when loading an assembly Pin
Espen Harlinn10-Oct-11 9:21
professionalEspen Harlinn10-Oct-11 9:21 
GeneralRe: System.StackOverflowException when loading an assembly Pin
Sacha Barber10-Oct-11 9:33
Sacha Barber10-Oct-11 9:33 
GeneralRe: System.StackOverflowException when loading an assembly Pin
Espen Harlinn10-Oct-11 11:51
professionalEspen Harlinn10-Oct-11 11:51 
GeneralRe: System.StackOverflowException when loading an assembly Pin
Sacha Barber10-Oct-11 22:25
Sacha Barber10-Oct-11 22:25 
GeneralRe: System.StackOverflowException when loading an assembly Pin
Espen Harlinn12-Oct-11 8:58
professionalEspen Harlinn12-Oct-11 8:58 
QuestionMy vote of 5 Pin
Topchris9-Aug-11 2:33
Topchris9-Aug-11 2:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.