I have now created an updated version of this code, which adds many many more features such as
- The user can 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 much better
- The loading of the Dll/Exe to be drawn as a class diagram is done in the new AppDomain
- Examining IL for better Associations
- Can view method body IL
- Proper diagram
Here is a screen shot of the new version
And here is where the new article is http://www.codeproject.com/KB/WPF/AutoDiagrammerII.aspx
This article is about using reflection. For those of you that don't know what reflection is, it is the ability to obtain information about a Type of object without really knowing anything about the object type that is being dealt with. For example one could simply ask the current Type of object if it supports a certain method name, if it does, the method can be called. This may sound strange but it is a very powerful technique. Lets suppose that I simply want to look at what methods a class supports, well that is also easily achieved using reflection. .NET allows developers to leverage reflection in many ways.
I am a big fan of reflection and have used it in many different ways. One of the most impressive uses I have ever seen reflection put to, is by Lutz Roeders Reflector tool (available for free download here). Lutz Roeders Reflector basically allows a user to point his Reflector tool at any Common Langauge Runtime (CLR) assembly, and it will then de-compile this creating an entirely reflected treeview with all the objects from the source assembly shown, with code. Yes, with code. Great stuff. Basically you can use this tool to see how any valid CLR (assuming it has not been obfuscated) assembly works.
However, the other day I used reflector to de-compile a fairly complex dll. Reflector did this with no problem (in about 2 seconds), but I found that trawling the generated tree (although entirely correct) was quite confusing, as there were a lot of nodes in the tree. And I thought to myself, I could do with a class diagram of all this lot. Or at least the parts that sound like they are the parts I am interested in. This is NOT something Lutz Roeders Reflector provides. I guess one could argue that if there is good documentation provided, one would not need a class diagram. Well, what if there is no documentation, what if you just have the Dll/Exe and nothing more. Thats kind of where this article is coming from.
Based on all this I thought why don't I try and create a tool that is capable of drawing a class diagram of any CLR assembly using reflection. So I did. This article demonstrates the results of my work in this area.
I also did some Googling to see what was already available. There was one similar project available (at a reasonable cost), namely Sun NetBeans SDE. Reasonable cost is still not free though is it? So that makes this article a much nicer one, in theory, as it is free code, which may be used and distributed freely.
As I have stated this article is all about creating a class diagram using reflection. That means the attached application (codenamed AutoDiagrammer
) does not even need source files, it doesn't care about the source files, as it just doesn't need them. It just wants to be pointed at a CLR assembly (dll) or a CLR application (exe). That's it. No source files required at all. So it can be seen as a companion to Lutz Roeders Reflector. It simply provides the class diagramming capabilities that are not present within Lutz Roeders Reflector.
Remember, a picture tells a thousand words.
I have tried to make a useful product: to this end the following features are supported:
- Detection of NON-CLR type being requested by user
- Customization of what is shown on the class diagram
- Show interfaces [Yes / No]
- Show constructor parameters [Yes / No]
- Show field types [Yes / No]
- Show method arguments [Yes / No]
- Show method return values [Yes / No]
- Show property types [Yes / No]
- Show events [Yes / No]
- Show enumerations
- Show delegates [Yes / No]
- Number of columns to use for the generated diagram (Number between 1-5)
- Class background start color
- Class background end color
- Class border color
- Accessability modifier selection (Public only / Public and Static / All)
- Automatically drawn class association lines
- Automatically drawn generalization (inheritence) arrows
- Expand individual sections of a class (Constructors / Fields / Properties / Methods / Events may all be collapsed / expanded individually)
- Expand entire class
- Class representation as similar to Visual Studio 2005 look and feel as possible
- Allow saving of diagram to the following image formats (Bmp, Emf, Exif, Gif, Jpeg, Png)
So those are the main features. These will all be explained in more detail in the following sections
So I guess by now you are wondering how all this actually happens, aren't you? Well, let me try and explain how I did all this. Let me start with a simple step-by-step account of what goes on. This will probably help, before I delve into the nitty gritty workings.
- Ask the user to select a file to reflectively draw the classes for.
- Determine if the selected file is a valid CLR type. (The class DotNetObject.cs within the attached application does this). If the input file is not a valid CLR
Type
tell the user, and do nothing more. Determine if the file is a "System" namespace assembly, if it is, alert the user and do nothing more. If we get past both these checks proceed to step 3. - Add each allowable type (obtained in sub-step 3 below) to a treeview on the main form (frmMain.cs), and also create a new class object (ucDrawableClass.cs) for each allowable
Type
seen.
- Use reflection to examine all the
Types
contained within the input file. Only include the Types
from the current input file that are not part of the "System" namespace. Including "System" types, would take forever, and is not what we are trying to do. There is lots of good documentation available for the Microsoft "System" namespace objects. That's my opinion anyway. Anyway we digress, so for each allowable Type create a new class object (ucDrawableClass.cs) passing it the Type
to represent. See step 4.
- When a new class object (ucDrawableClass.cs) is created, it will analyze the
Type
that it is supposed to represent and extract all Constructors / Fields / Properties / Methods / Events information using reflection and store these details. - The user selects what classes they would like to view from the treeview (right click) and then a new class drawing panel (ClassDrawerContainerPanel.cs) is created which lays out the classes (
ucDrawableClass
controls) that the user requested and draws the association lines (ucAssociationDrawer.cs) for any available association lines.
That's the basic operation of what is going on. But there is much more that may be of interest to the average codeproject user, so lets look at each of these steps in some more detail and also look as some of the more advanced features mentioned earlier.
I am not going to explain every line of the code for the main form (frmMain.cs), as it is fairly standard win forms stuff. I will however explain the interesting parts.
So the user tries to open a file, the following openFileDialog
will be shown
Assuming the user selects a assembly (Dll) or application (Exe), the file the user selects is then checked to see if is actually a valid CLR type.
The main form (frmMain.cs) establishes this by the following call to the DotNetObject
class.
if (DotNetObject.isDotNetAssembly(f.FullName))
{
}
else
{
}
So how does the DotNetObject.isDotNetAssembly()
method actually work? Well, it works as follows:
public static bool isDotNetAssembly(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 ex)
{
return false;
}
finally
{
fs.Close();
}
}
I have to say, this code was sourced somewhere a long time ago, and I can't quite remember just where came from. But thanks to whoever it was anyway. If you read this and you think it was you, let me know and I'll credit it in this article.
Anyway the upshot of it is that we get a boolean
to say that the current file is valid or not, that is all we care about at the moment. So if the file requested is not a valid CLR type an error message is shown, and nothing else is done.
However, if the input file is a valid CLR file, it is then checked to see if the file is a "System" assembly. Which I have not allowed, for time / space / sanity reasons.
The following code does this.
Assembly ass = Assembly.LoadFrom(f.FullName);
if (ass.FullName.StartsWith("System"))
{
Program.ErrorBox("System namespace assemblies not allowed");
}
#region valid CLR type, non-system namespace
else
{
......
......
BackgroundWorker bgw = new BackgroundWorker();
bgw.DoWork += new DoWorkEventHandler(bgw_DoWork);
bgw.RunWorkerCompleted += new RunWorkerCompletedEventHandler
(bgw_RunWorkerCompleted);
bgw.RunWorkerAsync(ass);
}
#endregion
It can be seen that if the currently selected input file is a "System" namespace assembly, an error message is shown to the user. If however, the currently selected assembly is not a "System" namespace assembly, it is a candidate for further exploration. As such, a new BackGroundWorker
is created, to deal with the further analysis of the current assembly.
The new BackGroundWorker
is created and the bgw_DoWork
method is the method that starts the analysis process. So lets have a look at that method.
private void bgw_DoWork(object sender, DoWorkEventArgs e)
{
Assembly a = (Assembly)e.Argument;
if (this.InvokeRequired)
{
this.Invoke(new EventHandler(delegate
{
StartAnylsisProcess(a);
}));
}
else
{
StartAnylsisProcess(a);
}
}
It can seen that all the BackGroundWorker
really does is to call the StartAnylsisProcess(a);
method passing it the current assembly. So lets have a look at that.
foreach (Type t in a.GetTypes())
{
addTypesToTree(_tvnRoot, t.Name, t.Namespace, t);
}
}
This method also calls another method, namely addTypesToTree(_tvnRoot, t.Name, t.Namespace, t);
so I'll also show this
private void addTypesToTree(TreeNode nd, String typename,
String nspace, Type t)
{
try
{
if (nspace != null)
{
if (!nspace.Trim().Equals(String.Empty))
{
if (!TreeContainsNode(nd, nspace))
{
nd.Nodes.Add(new TreeNode(nspace, 1, 1));
_nspaces.addNameSpace(nspace, null);
}
if (IsWantedForDiagramType(t))
{
foreach (TreeNode tn in nd.Nodes)
{
if (tn.Text == nspace)
{
TreeNode currTypeNode = new TreeNode
(typename, 2, 2);
tn.Nodes.Add(currTypeNode);
ucDrawableClass dc = new ucDrawableClass(t);
_nspaces.addNameSpace(nspace, dc);
lblStatus.Text = "Analysing type
[" + t.Name + "]";
pbStatus.Visible = true;
Refresh();
Application.DoEvents();
}
}
}
}
}
}
catch (Exception ex)
{
Program.ErrorBox(ex.Message);
}
}
#endregion
The most important part of the above method to note is that it not only populates the main form (frmMain.cs) treeview, but it also creates new ucDrawableClass
objects (to represent individual classes) and adds them to the NameSpaces
object. These will be explained in more detail later. But for now it is useful to note that the NameSpaces
object holds a list of ucDrawableClass
objects (classes). It's just like a normal hierarchy in Visual Studio, a namespace contains classes. I've tried to keep it so it has a close match to what one would normally see in an object heirachy. In fact if we look at one single line from the NameSpaces
object (NameSpaces.cs) we can see that it simply contains a Dictionary
object which stores a list of ucDrawableClass
objects (classes) against a string, where the string is a unique namespace. Or if you prefer code:
private Dictionary<string, List<ucDrawableClass>> _assObjects =
new Dictionary<string, List<ucDrawableClass>>();
Recall earlier that I mentioned that certain Types
would not be allowed, so how is this done? Notice in the code above, there is a call to a
if (IsWantedForDiagramType(t))
This method call is what determines if the current Type
being examined should be included both as a class, and to the treeview. Lets just have a quick look at this method.
private bool IsWantedForDiagramType(Type t)
{
if (t.Namespace.StartsWith("System"))
return false;
if (!Program._AllowEnumOnDiagram)
{
if (t.BaseType != null)
{
if (t.BaseType.FullName.Equals("System.Enum"))
{
return false;
}
}
else
{
return false;
}
}
if (!Program._AllowDelegatesOnDiagram)
{
if (t.BaseType != null)
{
if (t.BaseType.FullName.Equals("System.MulticastDelegate"))
{
return false;
}
}
else
{
return false;
}
}
if (t.BaseType != null)
{
if (t.BaseType.FullName.Equals
("System.Configuration.ApplicationSettingsBase"))
{
return false;
}
}
return true;
}
So that's pretty much all (for now) on the main form (frmMain.cs). Though we'll have to revisit it when we look at creating a diagram, and saving a diagram, and also to choose what is shown on a diagram. For now though, let us just concentrate on Step 4.
Recall from above, that the main form (frmMain.cs) creates new ucDrawableClass
objects. So what is one of those ucDrawableClass
objects. Well in answer to that, it's a fairly in-depth user control, that employs both custom painting and also contains child controls. To get our head around what a ucDrawableClass
control looks like, consider the following figure.
It can be seen that this control mimics the look and feel of the native Visual Studio 2005 class diagram class. It allows the user to collapse individual sections (each section is a ucExpander
control). It also allows for the entire class to be collapsed/expanded.
This control is constructed using custom painting (override OnPaint(..)
) and also by the use of child controls, namely child ucExpander
controls. Each of the child ucExpander
controls is simply passed a list of strings and a examining type. Each ucExpander
then looks after its own rendering. Each ucExpander
will show a different icon depending on what it is being asked to represent. For example if a ucExpander
control is created and asked to show Methods, it will list the method strings from its source list of display strings, and will display a Method image for each entry in its source list of display strings. Quite nice huh. Reuse is always useful if done well.
I have explained that each ucExpander
renders its own contents based on some list of display strings. So where do the individual lists of strings come from to pass to the ucExpander
controls, in the first place. Well they come from a reflective analysis process that is done by the ucDrawableClass
object. Recall that when the main form created a new ucDrawableClass
object, it did so passing in the current Type
that was being examined as part of the input scan.
So the ucDrawableClass
object gets passed a Type
on construction, like :
public ucDrawableClass(Type t)
{
...
AnalyseType();
...
}
The constructor calls a method called AnalyseType()
. It is this AnalyseType()
method that carries out the reflective gathering of all the Type
information, and creates individual lists to pass to the child ucExpander
controls.
The analysis process creates the following lists:
- Constructors
- Fields
- Properties
- Interfaces
- Methods
- Events
So how are these lists created. Reflection Reflection Reflection Reflection, its all about Reflection. Lets have a look shall we.
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();
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;
}
}
private void AnalyseType()
{
List<MethodInfo> propGetters = new List<MethodInfo>();
List<MethodInfo> propSetters = new List<MethodInfo>();
#region Constructors
foreach (ConstructorInfo ci in
_type_to_Draw.GetConstructors(Program.RequiredBindings))
{
if (_type_to_Draw == ci.DeclaringType)
{
string cDetail = _type_to_Draw.Name + "( ";
string pDetail="";
ParameterInfo[] pif = ci.GetParameters();
foreach (ParameterInfo p in pif)
{
string pName=getGenericsForType(p.ParameterType);
pName = LowerAndTrim(pName);
if (!_Associations.Contains(pName))
{
_Associations.Add(pName);
}
pDetail = pName + " " + p.Name + ", ";
cDetail += pDetail;
}
if (cDetail.LastIndexOf(",") > 0)
{
cDetail = cDetail.Substring(0,
cDetail.LastIndexOf(","));
}
cDetail += ")";
if (Program._FullConstructorDescribe)
{
_Constructors.Add(cDetail);
}
else
_Constructors.Add(_type_to_Draw.Name + "( )");
}
}
#endregion
#region Fields
foreach (FieldInfo fi in
_type_to_Draw.GetFields(Program.RequiredBindings))
{
if (_type_to_Draw == fi.DeclaringType)
{
string fName=getGenericsForType(fi.FieldType);
fName = LowerAndTrim(fName);
if (!_Associations.Contains(fName))
{
_Associations.Add(fName);
}
if (Program._IncludeFieldType)
_Fields.Add(fName + " " + fi.Name);
else
_Fields.Add(fi.Name);
}
}
#endregion
#region Properties
foreach (PropertyInfo pi in
_type_to_Draw.GetProperties(Program.RequiredBindings))
{
if (_type_to_Draw == pi.DeclaringType)
{
if (pi.CanRead) { propGetters.Add(pi.GetGetMethod(true)); }
if (pi.CanWrite) { propSetters.Add(pi.GetSetMethod(true)); }
string pName=getGenericsForType(pi.PropertyType);
pName = LowerAndTrim(pName);
if (!_Associations.Contains(pName))
{
_Associations.Add(pName);
}
if (Program._IncludePropValues)
_Properties.Add(pName + " " + pi.Name);
else
_Properties.Add(pi.Name);
}
}
#endregion
#region Interfaces
if (Program._IncludeInterfaces)
{
Type[] tiArray = _type_to_Draw.GetInterfaces();
foreach (Type ii in tiArray)
{
_Interfaces.Add(ii.Name.ToString());
}
}
#endregion
#region Methods
foreach (MethodInfo mi in
_type_to_Draw.GetMethods(Program.RequiredBindings))
{
if (_type_to_Draw == mi.DeclaringType)
{
string mDetail = mi.Name + "( ";
string pDetail="";
if (Program._IncludeMethodArgs)
{
ParameterInfo[] pif = mi.GetParameters();
foreach (ParameterInfo p in pif)
{
string pName=getGenericsForType(p.ParameterType);
pName = LowerAndTrim(pName);
if (!_Associations.Contains(pName))
{
_Associations.Add(pName);
}
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);
if (!_Associations.Contains(rName))
{
_Associations.Add(rName);
}
if (Program._IncludeMethodReturnType)
mDetail += " : " + rName;
}
else
{
if (Program._IncludeMethodReturnType)
mDetail += " : void";
}
if (!Program._ShowPropGetters &&
propGetters.Contains(mi)) { }
else if (!Program._ShowPropSetters &&
propSetters.Contains(mi)) { }
else {
_Methods.Add(mDetail);
}
}
}
#endregion
#region Events
foreach (EventInfo ei in
_type_to_Draw.GetEvents(Program.RequiredBindings))
{
if (_type_to_Draw == ei.DeclaringType)
{
string eName=getGenericsForType(ei.EventHandlerType);
eName = LowerAndTrim(eName);
if (!_Associations.Contains(eName))
{
_Associations.Add(eName);
}
if (Program._IncludeEventType)
_Events.Add(eName + " " + ei.Name);
else
_Events.Add(ei.Name);
}
}
#endregion
}
#endregion
}
Hopefully you can see that these 6 lists are then simply used to create 6 new ucExpander
controls, which are then positioned at the correct X/Y positions within the current ucDrawableClass
object.
WHAT'S THE STORY SO FAR?
We now have a treeview with ONLY valid namespaces and ONLY valid classes created. We also have a nice NameSpaces
object which contains a Dictionary of strings (for namespaces) and for each string a list of ucDrawableClass
objects (for the classes). The list of ucDrawableClass
objects, are created and are ready and waiting to be placed on a suitable drawing canvas.
But as yet we don't know what classes the user wants to draw, it could be all of them, or it could be 1 of them or even none of them. It depends on what the user selects from the treeview on the mainform (frmMain.cs). That's step 5, so let's continue our merry journey.
So the story so far is as described above. But still no diagram.
So what does the user need to do to get a diagram. Well, they need to do the following :
- Select at least one class
- Right click on the namespace in the tree (with at least one class selected) OR use the button on or the menu item on the main form (frmMain.cs)
This is shown below (just for fun, this tree shows all the classes for the AutoDiagrammer.exe (that's this article code) which were reflectively obtained)
Use the treeview right click to view diagram
Use the menu to view diagram
Use the toolbar to view diagram
When the user clicks the "View class diagram for namespace", right click menu or the button or menu, the application will then draw (providing the current node is a Namespace node, otherwise it does know what to draw) the previously generated (step 4 above) ucDrawableClass
objects, on a specialized panel object.
The specialized panel acts as a container to display the ucDrawableClass
objects in a grid formation. The specialized panel object is a subclass of the standard .NET 2.0 TableLayoutPanel
object, and is called ClassDrawerContainerPanel
. It acts quite similar to the standard TableLayoutPanel
object, in that it will organize its contents into rows/columns, but there is also custom logic in the ClassDrawerContainerPanel
to resize the rows/columns based on whether the contained ucDrawableClass
controls are in the collapsed or expanded states.
The ClassDrawerContainerPanel
container will automatically set the row/col positions of all ucDrawableClass
objects that it lays out, by setting 2 public properties of a ucDrawableClass
object, namely ContainerRow
and ContainerColumn
. These properties may then be used later, when trying to establish how to draw association lines.
An example of what is shown within the ClassDrawerContainerPanel
object is as shown below.
It can be seen that there are rows/columns where the individual ucDrawableClass
objects are placed (programmatically). What is also fairly important are the vertical and horizontal spacers. These are (intentional) blank areas that will be used to draw any association arrows that are required.
Basically what happens is that the ClassDrawerContainerPanel
object conducts a custom paint (overrides onpaint(..)
) such that for each contained ucDrawableClass
object, a list of associations is retrieved (the list of associations for each was ucDrawableClass
was created at step 4).
So what we end up with for each association, is a source ucDrawableClass
object and a destination ucDrawableClass
object, for which we must draw an association. So how can this possibly be done without drawing on top of the contained controls. Well that's exactly what the vertical and horizontal spacers are for. They allow the painting of the association lines to be done without fear of drawing over any ucDrawableClass
controls.
So how is this done. Well there are some basic rules which allow the associations to be drawn correctly. These rules are shown in the following figure
So that's how the association rules work. But how does all this translate into code. Well, for each association found, the ClassDrawerContainerPanel
object creates a new AssociationDrawer
object to draw the association line. The constructor of the AssociationDrawer
class is as shown below:
#region Constructor
public AssociationDrawer(Graphics g,ucDrawableClass ucdSrc,
ucDrawableClass ucdDest,int genericSpace)
{
this._ucdSrc = ucdSrc;
this._ucdDest = ucdDest;
this._GenericSpace = genericSpace;
this._g = g;
GetDirectionAndDraw();
}
#endregion
It can be seen that the constructor takes several parameters that allow the AssociationDrawer
object to draw the correct association line. So let's have a look at two things relating to the AssociationDrawer
class
- The
GetDirectionAndDraw()
method - And an example of how one of the associations is drawn, say North.
The association rules -> method calls
It can be seen that the association rules make use of the previously set properties for each ContainerRow
and ContainerColumn
of the source and destination ucDrawableClass
objects. The properties ContainerRow
and ContainerColumn
, were set when the ClassDrawerContainerPanel
first layed out all the required ucDrawableClass
objects. So now it's just a case of creating the rules, almost verbatim.
private void GetDirectionAndDraw()
{
if (_ucdDest.ContainerRow == _ucdSrc.ContainerRow - 1 &&
_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn)
{
DrawNorth();
}
if (_ucdDest.ContainerRow == _ucdSrc.ContainerRow + 1 &&
_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn)
{
DrawSouth();
}
if (_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn + 1 &&
_ucdDest.ContainerRow == _ucdSrc.ContainerRow)
{
DrawEast();
}
if (_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn - 1 &&
_ucdDest.ContainerRow == _ucdSrc.ContainerRow)
{
DrawWest();
}
if (_ucdDest.ContainerRow <= _ucdSrc.ContainerRow - 1 &&
_ucdDest.ContainerColumn >= _ucdSrc.ContainerColumn + 1)
{
DrawNorthEast_DrawSouthEast();
}
if (_ucdDest.ContainerRow >= _ucdSrc.ContainerRow + 1 &&
_ucdDest.ContainerColumn >= _ucdSrc.ContainerColumn + 1)
{
DrawNorthEast_DrawSouthEast();
}
if (_ucdDest.ContainerRow <= _ucdSrc.ContainerRow - 1 &&
_ucdDest.ContainerColumn <= _ucdSrc.ContainerColumn - 1)
{
DrawNorthWest_DrawSouthWest();
}
if (_ucdDest.ContainerRow >= _ucdSrc.ContainerRow + 1 &&
_ucdDest.ContainerColumn <= _ucdSrc.ContainerColumn - 1)
{
DrawNorthWest_DrawSouthWest();
}
if (_ucdDest.ContainerRow <= _ucdSrc.ContainerRow - 2 &&
_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn)
{
DrawNorthNonDirect_DrawSouthNonDirect();
}
if (_ucdDest.ContainerRow >= _ucdSrc.ContainerRow + 2 &&
_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn)
{
DrawNorthNonDirect_DrawSouthNonDirect();
}
if (_ucdDest.ContainerRow == _ucdSrc.ContainerRow &&
_ucdDest.ContainerColumn >= _ucdSrc.ContainerColumn+2)
{
DrawEastNonDirect_DrawWestNonDirect();
}
if (_ucdDest.ContainerRow == _ucdSrc.ContainerRow &&
_ucdDest.ContainerColumn <= _ucdSrc.ContainerColumn - 2)
{
DrawEastNonDirect_DrawWestNonDirect();
}
}
So lets have a look at one of the more simple association rules / resultant line. Lets say North. All association lines follow similar principles to this, but some may require more lines.
private void DrawNorth()
{
int xStart = 0;
int xEnd = 0;
if (_ucdDest.Right <= _ucdSrc.Right)
{
xStart = _ucdDest.Right - 20;
xEnd = _ucdDest.Right - 20;
}
else
{
xStart = _ucdSrc.Right - 20;
xEnd = _ucdSrc.Right - 20;
}
int yStart = _ucdSrc.Top;
int yEnd = _ucdDest.Bottom;
Pen p = new Pen(new SolidBrush(Color.Black));
p.DashStyle = DashStyle.Dash;
_g.DrawLine(p,new Point(xStart, yStart), new Point(xEnd, yEnd));
_g.DrawLine(p, new Point(xEnd, yEnd), new Point(xEnd - 5, yEnd + 10));
_g.DrawLine(p, new Point(xEnd, yEnd), new Point(xEnd + 5, yEnd + 10));
}
The following diagram illustrates what the class diagram associations look like for an example application (actually it is reflecting itself, AutoDiagrammer.exe) with only 5 of the actual namespace classes selected for viewing. It also demonstrates what the classes (ucDrawableClass
controls) look like in the various states:fully expanded / fully collapsed, partially collapsed (individual ucExpander
controls collapsed).
So that's the basics covered. But I have not yet shown you how to modify what is shown on the diagram, or how to save a diagram. So if you still want more, let us continue.
Let's start with how to modify what's shown on the diagram.
To customize what is actually going to be included on the diagram, there is an extra settings form (frmSettings.cs) which is accessible using the toolbar on the main form (frmMain.cs) or the menu of the main form (frmMain.cs).
This settings form (frmSettings.cs) looks like the following diagram. From here, diagram features may be turned on/off
There are settings for the following:
- Show interfaces [Yes / No]
- Show constructor paremeters [Yes / No]
- Show field types [Yes / No]
- Show method arguments [Yes / No]
- Show method return values [Yes / No]
- Show get method for existing property as method [Yes / No]
- Show set method for existing property as method [Yes / No]
- Show property types [Yes / No]
- Show events [Yes / No]
- Show enumerations
- Show delegates [Yes / No]
- Class background start color [ Color ]
- Class background end color [ Color ]
- Class border color [ Color ]
- Accessability modifier selection (Public only / Public and Static / All)
Let's have a look at an example class or two, with some of these extra items turned on.
It can be seen that now, method arguments and property types shown. It is really up to you how much detail you would like to see. Obviously the more detail shown, the larger the diagram will be.
I decided that although handy, this tool would be pretty useless, unless people could actually save diagrams.To this end, the application supports saving to the following image formats : Bmp, Emf, Exif, Gif, Jpeg, Png. In order to save a diagram there is an additional saving form (frmSave.cs) which is available from either the toolbar or the menu, from the main form (frmMain.cs).
This saving form (frmSave.cs) is as shown below.
I have initially tried to save the specialized panel ClassDrawerContainerPanel
(ScrollableControl
essentially) contents by programmatically scrolling around grabbing separate image segments that were then pasted into an overall image. This was quite messy and a nightmare.
Then a fellow codeprojector "James Curran" came to the rescue (I had stated in the original article contents, that someone should assist in this area). So, a big thank you to James.
James simply stated that a control could exist whilst not living on a form (Duh, I seemed to totally miss this, school boy error).
So with this one little bit of advice, the save code was dramatically reduced. I did however have to make a change (v 1.2) to cater to the fact that when the application saves an image to a filename, a lock is maintained, such that when trying to save to the same file name a "General GDI+ error
" exception was being raised. This is now fixed, and the full SaveTheDiagram()
method is as follows:
private bool SaveTheDiagram(string filename, ImageFormat imgFormat)
{
Cursor.Current = Cursors.WaitCursor;
int bmpSrcWidth = pnlFlowClasses.MaxSize.Width;
int bmpSrcHeight = pnlFlowClasses.MaxSize.Height;
ClassDrawerContainerPanel pnl = new ClassDrawerContainerPanel();
pnlFlowClasses.SuspendLayout();
Rectangle newBounds = new Rectangle(0, 0, bmpSrcWidth, bmpSrcHeight);
pnl.Height = bmpSrcHeight;
pnl.Width = bmpSrcWidth;
pnl.Bounds = newBounds;
pnl.BackColor = Color.White;
pnl.SetBounds(0, 0, bmpSrcWidth, bmpSrcHeight);
pnl.ClassesToDraw = pnlFlowClasses.ClassesToDraw;
pnl.LayoutControls();
Bitmap SrcBmp=null;
Bitmap bmpNew = null;
Graphics gfx = null;
try
{
SrcBmp = new Bitmap(bmpSrcWidth, bmpSrcHeight);
pnl.DrawToBitmap(SrcBmp, newBounds);
bmpNew = new Bitmap(SrcBmp.Width, SrcBmp.Height);
gfx = Graphics.FromImage(bmpNew);
gfx.DrawImage(SrcBmp, new Rectangle
(0, 0, bmpNew.Width, bmpNew.Height),
0, 0, SrcBmp.Width, SrcBmp.Height, GraphicsUnit.Pixel);
SrcBmp.Dispose();
SrcBmp = bmpNew;
gfx.Dispose();
SrcBmp.Save(filename, imgFormat);
SrcBmp.Dispose();
pnlFlowClasses.ResumeLayout();
return true;
}
catch (Exception ex)
{
if (SrcBmp != null) { SrcBmp.Dispose(); }
if (bmpNew != null) { bmpNew.Dispose(); }
if (gfx != null) { gfx.Dispose(); }
GC.Collect();
return false;
}
}
This is much nicer than what I originally had to save the image. As my original code relied on the GDI32.Dll BitBlt
, but it was a good exercise.
But this new way is much better, and it is all native C# code, so that is also good.
There is also a new form which allows the initial diagram to be viewed on a form where the user may zoom in or out. This new form is accessible from a button or a menu item on the main form (frmMain.cs). The zoom form (frmZoom.cs) is shown below.
There is one final form, which is the About window (frmAbout.cs), where people can contact me, should they wish to. Probably not, I'm betting.
Printing Support
There is now support for printing.
Yes there is now support for reflector addin functionality, all largely due to a fellow named "Andre Seibel", who actually got the dll information from Reflector into my code. I managed to create a Reflector plugin OK, I just couldn't get the Assembly data out of Reflector. But "Andre Seibel" did, so thanks Andre.
Here is what to do: unzip and copy the AutoDiagrammer.dll at the top of this article to your Reflector installation directory. Open Reflector, and then the add-in menu, then search for the AutoDiagrammer.dll, and add this as a valid Reflector AddIn. Then under the tools menu, there will be a new AutoDiagrammer menu. Click that and away you go.
At Lutz Roeders lead, I have now included my email with the attached Reflector Addin Assembly information, so Lutz Roeders Reflector will autmatically email me any bugs. But please bear in mind I am still a student, so do have other commitments / priorities. I do intend on fixing bugs, but I can not garuentee that they will get done at break neck speed. So what im really say is, by all means use the Reflector addin, but if it throws a wobbly now and then, please tell me about it, and ill have a look, but you may have to wait a little bit for a fix.
As you may notice this addin for Reflector does not look quite the same as the stand alone app. This is due to the fact that in the stand alone app, I am doing the reflecting. Where as in the addin, the reflecting is done by Lutz Roeders code. So I can not show the treeview up front, as the reflecting code is already done at that point. So when the user clicks the addin button the reflecting has already been done, which is fairly different from the stand alone app. There seems little point in reflecting again, just to allow users to pick what classes to show from a treeview. Its a nice to have I agree, but this one feature would require a little more work. See what you lot think; let me know. If enough folk want that back, in the Reflector addin version, I'll look into it. You could and always will be able to use the stand alone app. I will be maintaining both.
I also have to say I prefer the stand alone App, as I know whats what in the standalone app, I can't say the same about the reflector addin.
Another new feature is that, I have now added tooltips to the classes, such that when the user hovers a mouse over the class, its associations are shown as a tooltip, so its easier to follow the associations that are drawn.
Thats it
Well that's actually about it. I hope that by me just detailing the interesting parts of the application, that I have not bored you all to death. This application has taken me about two part-time weeks to complete, and I have done quite a bit of thinking about how to do things, so I hope this is reflected in the way that I have tried to discuss the design with you good folk. There will always be better ways, however, this is the journey that I took. So I thought why not share that with you guys. But finally it's up to you guys to let me know what you think.
I would just like to ask, if you liked the article please vote for it, as it allows me to know if the article was at the right level or not.
I have quite enjoyed constructing this article. I hope you lot liked it. I think its fairly useful. Even if it is to just help you to understand something about reflection. I like reflection and have more articles to write about it, but I'm going to be hitting the XAML and LINQ/DLINQ/XLINQ stuff 1st. So expect more articles in those areas before I write some more AI and reflection. Well that really is the end now, so thanks for listening to my ranting.
v1.10 08/04/07: Attempted to fix NullPointerException that some user were having with Reflector AddIn Dll.
- Though it has always worked for me as seen by the screen shots in the article. But I have had bug reports, so I've tried to see where these were occurring and tried to do something about it
v1.9 01/04/07: Fixed one small issue with line drawing maths, that I noticed when running code as Reflector AddIn
- Fixed line drawing maths, to stop it drawing lines on top of class objects.
- Another new fearture is that, I have now added tooltips to the classes, such that when the user hovers a mouse over the class, its associations are shown as a tooltip, so its easier to follow the associations that are drawn
- Also fixed the problem with v1.8 where the desktop version would not draw the classes any more. There is now one app, zip at top. And depending on if you want the standalone or reflector version, you simply change the build options in visual studio. WindowsApplication with Program as start class for stand alone app. Or as a ClassLibrary for Reflector addin. Though the stanalone app can be installed using the auto update link at the top of this article and the Reflector addin, is also available as a seperate download at the top of this article.
v1.8 31/03/07: Its now available as as Add-In for reflector
- Copy the AutoDiagrammer.dll at the top of this article to your reflector installation directory. Open reflector, and then the add-in menu, then search for the AutoDiagrammer.dll, and add this as a valid Reflector AddIn. Then under the tools menu, there will be a new AutoDiagrammer menu. Click that and away you go.
v1.7 22/03/07 : Added for control of what is shown for method on diagram:
- Allow users to pick whether for a given Property, whether the associated get/set reflected methods should also be shown on the class diagram. As the displaying of the associated get/set reflected methods is really redundant, as the Property is already shown. This new solution uses a bit of code as supplied by codeprojecter AlwiNus. So thanks AlwiNus. Well done. I was actually trying to solve a completely different issue, or thought people were asking for something else. Doh.
v1.6 15/03/07: Added support for generics:
- Allowed generic datatypes to be properly explored and diplayed so instead of "list'1" you will now get "list<string>" for example. This is largely down to something I found out the other day, namely the following method call
Type.IsGenericType()<code> and <code>Type.GetGenericArguments()<code>. So I am most happy with it now. As that one thing always bugged me. Grrr. Fixed now though.
v1.5 11/03/07: Further CodeProject user comments include. Namely the following:
- Printing is now supported
- Auto updating is now supported
- Allow customisation of class drawing colors (maybe for better printing)
- Allow customisation of class data shown based on accessability modifiers (Public / Private / Static)
v1.4 06/03/07 : Further codeproject user comments include. Namely the following :
- Save form now has Cancel or Ok
- Zoom and settings form now allow scroll with mouse, Focus() was called in wrong form event. Now all ok
- Exit shortcut key changed to CTRL + X on main form
- Tools menu changed to say "Settings"
- "User settings" menu changed to say "Configure settings"
- Additonal setting added to settings page, to allow user to adjust the number of columns that will be shown on the Diagram
- When the user selects Ok on the settings page, an automatic re-scan is done of the current file, such that the new settings are used
- When the diagram is initially shown all the classes are shown in the collapsed state
- If the user doesnt click and Namespace child nodes, application assumes that all children of the current node are to be drawn
- There is now the ability to hide the treeview from a menu, or by the veretical pin button on the new vertical panel (grey one youll see it)
v1.3 04/03/07: Fixed a bug namely the following :
- Noticed that when saving image with all classes collapsed, that right most section of image was not being saved. Traced this to a problem in the ClassDrawerContainerPanel class. This is now corrected.
v1.2 03/03/07: Fixed a bug namely the following :
- Issue with saving the image throwing a "General GDI+ error" that was mention by codeproject user "RealStranger". This was traced to the fact that when saving an image to the same file name, the original Bitmap (.NET object) was keeping a lock on the file. To this end the save method on the main form had to be changed to include a second dummy Bitmap object to hold the original image, and then the lock was realeased. Then the original image is re-assigned the stored image data, and the image can be saved.
- There is now a new Zoom form. But dont be expecting the image to work as the initial diagram goes. Its only an image respresentation of the original diagram, so you can zoom. But you cant collapse / expand the controls on the Zoom form.
v1.1 02/03/07: Codeproject initial release comments incorporated. Namely the following:
- About form now has Ok button
- Settings form now has Ok and Cancel buttons
- The main form now allows saving from both a new menu item, and a new toolbar button (Though you must still have a Namespace treeview node selected, as this is used to tell the application what to actually draw)
- The main form can now be re-sized to any size the user wishes, and may be minimized or maximized
- The saving of the diagram has been totally changed, now all native C# code. Thanks James Curran
- Any classes that do not have a Namespace, are now added to a new treeview node called "No-Namespace Classes"
v1.0 01/03/07: Initial issue