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:
- The drawing of class associations was based on a grid layout.
- The user could not move the classes around on the design surface; once they were laid out, that was it.
- The Association lines were not that clear to see.
- The user could not scale the produced diagram that well (it was possible, but was not that great).
- 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. - 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:
- The overall idea (people seemed to generally like it, and find it a very useful tool).
- The reflecting of information was correct.
- 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:
- Detection of valid .NET assembly (yes, same as the first article, this tool only works with .NET Assemblies).
- The ability to move the classes in the design surface within the diagram.
- 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.
- Persisted settings, so that the next time the application is run, your personal settings will be as you left them.
- Proper scaling of objects as vectors, so whatever scale the diagram is viewed at, the objects are as clear as they could be.
- Saving of the diagram to an PNG file which can easily be viewed using the standard Window Image Viewer.
- Printing to a printer.
- Loading the DLL/Exe into a separate
AppDomain
that does not polute the AutoDiagrammer.exe AppDomain
with the loaded DLL/Exe types. - Integrated help.
- The classes show a full Association popup with all Associations shown as a list of strings.
- The Association lines show in a different color when the user hovers over them with the mouse.
- Better detection of Associations between classes via parsing of method body IL (Intermediate Language).
- 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.
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):
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:
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:
Now there are a couple of things of note right there, such as:
- 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. - The association from
Class1
to ConcreteRenderer
was found, this is thanks to the parsing of the method body IL within the Class1.DoIt()
method. - Association lines are colored differently when you hover over a class.
- 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.
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.
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:
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.
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.
Whilst the treeview is being generated, you will see this loading banner:
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 CheckBox
es 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).
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.
After the diagram has been created (or a timeout occurs, which again can be adjusted via the Settings window):
A new diagram will be shown (or in the case of a timeout, the last diagram which was successfully loaded (if there was one)):
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:
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:
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.
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:
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.
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.
Saving
The diagram can be saved to an PNG, using the Save button provided.
Which can then be viewed in the standard Image Viewer
Printing
The diagram may be printed using the Print button provided.
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.
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:
- Bounded FR
- Efficient Sugiyama
- FR
- ISOM
- KK
- 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:
And here are the settings for the "Tree" layout algorithm:
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:
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:
- 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.
- 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.
- Use the data from step 2 to draw a treeview of the namespaces and types found.
- Allow the user to select what types they wish to draw.
- 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.
- For all those graph objects from step 5 that have Associations, add these objects to a Graphsharp graph.
- 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:
public class DotNetObject
{
#region Public Methods
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];
Stream fs = new FileStream(@file, FileMode.Open, FileAccess.Read);
try
{
BinaryReader reader = new BinaryReader(fs);
fs.Position = 0x3C;
peHeader = reader.ReadUInt32();
fs.Position = peHeader;
peHeaderSignature = reader.ReadUInt32();
machine = reader.ReadUInt16();
sections = reader.ReadUInt16();
timestamp = reader.ReadUInt32();
pSymbolTable = reader.ReadUInt32();
noOfSymbol = reader.ReadUInt32();
optionalHeaderSize = reader.ReadUInt16();
characteristics = reader.ReadUInt16();
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();
}
}
public static bool IsWantedForDiagramType(Type t)
{
if (!string.IsNullOrEmpty(t.Namespace))
{
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:
[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);
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);
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;
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:
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
).
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:
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:
[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()
{
foreach (MethodInfo mi in TypeInAssembly.GetMethods(RequiredBindings))
{
if (TypeInAssembly == mi.DeclaringType)
{
string mDetail = mi.Name + "( ";
string pDetail = "";
if (ShowMethodArguments)
{
ParameterInfo[] pif = mi.GetParameters();
foreach (ParameterInfo p in pif)
{
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 += " )";
string rName = GetGenericsForType(mi.ReturnType);
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);
}
if (ShowMethodReturnValues)
mDetail += " : " + rName;
}
else
{
if (ShowMethodReturnValues)
mDetail += " : void";
}
if (!ShowGetMethodForProperty && propGetters.Contains(mi))
{
}
else if (!ShowSetMethodForProperty && propSetters.Contains(mi))
{
}
else
{
Methods.Add(new SerializableMethodData(mDetail,
ReadMethodBodyAndAddAssociations(mi)));
}
}
}
HasMethods = Methods.Any();
}
......
......
......
......
......
......
......
......
private string GetGenericsForType(Type t)
{
string name = "";
if (!t.GetType().IsGenericType)
{
int idx = t.Name.IndexOfAny(new char[] { '`', '\'' });
if (idx >= 0)
{
name = t.Name.Substring(0, idx);
Type[] genTypes = t.GetGenericArguments();
Associations.AddRange(genTypes.Select(x => x.FullName));
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:
- If there is a property to a different type
- For each backing field to a different type
- For each constructor parameter to a different type
- For each method argument to a different type
- 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:
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:
private void ExecuteCommenceDrawingCommand(Object parameter)
{
try
{
......
......
Task<GraphResults> task =
assemblyManipulationService.CreateGraph();
int timeout = SettingsViewModel.Instance.GraphDrawingTimeOutInSeconds * 1000;
bool finishedOk = task.Wait(timeout);
if (finishedOk)
{
AddItemsToGraph(task.Result);
graphPrintableWindow.ZoomToFit();
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.
public Task<GraphResults> CreateGraph()
{
Task<GraphResults> task = Task.Factory.StartNew<GraphResults>(() =>
{
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)
{
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:
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:
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
:
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());
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());
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.
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:
[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
.
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.
Here is the UI Service code that achieves the printing of an PNG file:
[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(IPrintPNGFileService))]
public class PrintPNGFileService : IPrintPNGFileService
{
#region Data
private PrintDialog pd = new PrintDialog();
private String filename = "";
#endregion
#region Ctor
public PrintPNGFileService()
{
pd.PageRangeSelection = PageRangeSelection.AllPages;
pd.UserPageRangeEnabled = true;
}
#endregion
#region IPrintPNGFileService Members
public Exception Print(FrameworkElement visual)
{
try
{
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;
}
}
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.
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:
Here is the code that populates the WebBrowser
with the correct HTML file, in case you are interested:
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