|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis 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. What This Article IsAs I have stated this article is all about creating a class diagram using reflection. That means the attached application (codenamed Remember, a picture tells a thousand words. AutoDiagrammer: FeaturesI have tried to make a useful product: to this end the following features are supported:
So those are the main features. These will all be explained in more detail in the following sections So How Does It All Work "In a Nutshell"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.
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. Step 1 : Opening A FileI 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
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 if (DotNetObject.isDotNetAssembly(f.FullName))
{
//proceed
}
else
{
//not a valid CLR so tell user and do nothing more
}
Step 2: Determining If A File Is A Valid CLR TypeSo how does the /// <summary>
/// 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
/// </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 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];
//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,
it is 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 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
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. //load the assembly and check it is not System namespace
//we don't want to be here all year
Assembly ass = Assembly.LoadFrom(f.FullName);
if (ass.FullName.StartsWith("System"))
{
Program.ErrorBox("System namespace assemblies not allowed");
}
//valid assembly that not a System one, so proceed
#region valid CLR type, non-system namespace
else
{
//set UI state correctly
......
......
//create a new BackgroundWorker thread to do the scanning
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 Step 3: Create The Tree And ClassesThe new /// <summary>
/// The background worker thread start doing work, calls the
/// internal StartAnylsisProcess() method
/// </summary>
/// <param name="sender">The background worker thread</param>
/// <param name="e">the event args</param>
private void bgw_DoWork(object sender, DoWorkEventArgs e)
{
//get Assembly from the workers argument
Assembly a = (Assembly)e.Argument;
//start the analysis process, making sure to marshall
//to correct thread for GUI property change operation, which
//will require the thread to be on the same thread as the handle
//to the control
if (this.InvokeRequired)
{
this.Invoke(new EventHandler(delegate
{
StartAnylsisProcess(a);
}));
}
else
{
StartAnylsisProcess(a);
}
}
It can seen that all the /// <summary>
/// Loop through each Type in the current Assembly
/// adding each to the forms treeview by calling the
/// internal addTypesToTree() method
/// </summary>
/// <param name="a">The current Assembly being examined</param>
private void StartAnylsisProcess(Assembly a)
{
//loop through each Type in the current Assembly
//adding each to the forms treeview
foreach (Type t in a.GetTypes())
{
addTypesToTree(_tvnRoot, t.Name, t.Namespace, t);
}
}
This method also calls another method, namely /// <summary>
/// If namespace isn't empty, add the namspace to the current node and
/// <see cref="Namespaces">Namespaces</see> object. Then check the type
/// is actualy one that can be added to the diagram, could be System
/// (which is not allowed). Then add the type to the namespace by creating
/// a new <see cref="ucDrawableClass">ucDrawableClass</see> object,
/// for each allowable type
/// </summary>
/// <param name="nd">The current treenode to add new nodes to</param>
/// <param name="typename">the current typename</param>
/// <param name="nspace">the current namspace</param>
/// <param name="t">the Type to examine</param>
private void addTypesToTree(TreeNode nd, String typename,
String nspace, Type t)
{
try
{
//if namspace isn't empty, add the namspace to the current node
//and to the treeview and the Namspaces object
if (nspace != null)
{
if (!nspace.Trim().Equals(String.Empty))
{
//1st add the new namespace
if (!TreeContainsNode(nd, nspace))
{
nd.Nodes.Add(new TreeNode(nspace, 1, 1));
_nspaces.addNameSpace(nspace, null);
}
//then check the type is actualy one that can be added
//to the diagram, could be System (which is not allowed)
if (IsWantedForDiagramType(t))
{
//now add the type to the namespace,
//by getting namespace from the list of nodes 1st
foreach (TreeNode tn in nd.Nodes)
{
if (tn.Text == nspace)
{
TreeNode currTypeNode = new TreeNode
(typename, 2, 2);
tn.Nodes.Add(currTypeNode);
//create a new ucDrawableClass
//and add it to the current namespace
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 private Dictionary<string, List<ucDrawableClass>> _assObjects =
new Dictionary<string, List<ucDrawableClass>>();
Step 3a : Should Type Be IncludedRecall earlier that I mentioned that certain if (IsWantedForDiagramType(t))
This method call is what determines if the current /// <summary>
/// Returns true if the Type specified by the t parameter is a valid
/// diagrammable type. e.i. Not System namespace, and is one that the
/// user wants on the current diagram
/// </summary>
/// <param name="t"></param>
/// <returns>true if the Type specified by the t parameter is a valid
/// diagrammable type. e.i. Not System namespace, and is one that the
/// user wants on the current diagram</returns>
private bool IsWantedForDiagramType(Type t)
{
//don't really want user to trawl the standard System namespaces
if (t.Namespace.StartsWith("System"))
return false;
//should Enums be shown, on final diagram
if (!Program._AllowEnumOnDiagram)
{
if (t.BaseType != null)
{
if (t.BaseType.FullName.Equals("System.Enum"))
{
return false;
}
}
else
{
return false;
}
}
//should MulticastDelegates be shown, on final diagram
if (!Program._AllowDelegatesOnDiagram)
{
if (t.BaseType != null)
{
if (t.BaseType.FullName.Equals("System.MulticastDelegate"))
{
return false;
}
}
else
{
return false;
}
}
//check for ApplicationSettingsBase
if (t.BaseType != null)
{
if (t.BaseType.FullName.Equals
("System.Configuration.ApplicationSettingsBase"))
{
return false;
}
}
//if we get to here its an allowable Type
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. Step 4: Individual Class Analysis (Reflection)Recall from above, that the main form (frmMain.cs) creates new
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 This control is constructed using custom painting (override I have explained that each So the /// <summary>
/// Stores the type provided as an internal field and
/// calls the AnalyseType() internal method. And also
/// creates several <see cref="ucExpander">ucExpander </see>
/// controls to display the individual Properties,Events,Fields,
/// Constructors,Methods details. Finally an overall
/// collapse / expand image is created
/// </summary>
/// <param name="t">The Type to analyze</param>
public ucDrawableClass(Type t)
{
...
AnalyseType();
...
}
The constructor calls a method called The analysis process creates the following lists:
So how are these lists created. Reflection Reflection Reflection Reflection, its all about Reflection. Lets have a look shall we. /// <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></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();
//and build the list of types for the result string
if (genTypes.Length == 1)
{
//name+="<" + genTypes[0].Name + ">";
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;
}
}
/// <summary>
/// Analyses the current Type (which was supplied on construction)
/// and creates lists for its Constructors, Fields, Properties,
/// Interfaces, Methods, Events to provide to these lists to
/// <see cref="ucExpander">ucExpander </see>controls
/// </summary>
private void AnalyseType()
{
// lists for containing get and set methods
List<MethodInfo> propGetters = new List<MethodInfo>();
List<MethodInfo> propSetters = new List<MethodInfo>();
#region Constructors
//do 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="";
//add all the constructor param types to the associations List,
//so that the association lines for this class can be
//obtained, and possibly drawn on the container
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 += ")";
//do we want long or short field constructor displayed
if (Program._FullConstructorDescribe)
{
//_Constructors.Add(ci.ToString().Replace(".ctor", ""));
_Constructors.Add(cDetail);
}
else
_Constructors.Add(_type_to_Draw.Name + "( )");
}
}
#endregion
#region Fields
//do fields
foreach (FieldInfo fi in
_type_to_Draw.GetFields(Program.RequiredBindings))
{
if (_type_to_Draw == fi.DeclaringType)
{
//add all the field types to the associations List, so that
//the association lines for this class can be obtained, and
//possibly drawn on the container
string fName=getGenericsForType(fi.FieldType);
fName = LowerAndTrim(fName);
if (!_Associations.Contains(fName))
{
_Associations.Add(fName);
}
//do we want long or short field description displayed
if (Program._IncludeFieldType)
_Fields.Add(fName + " " + fi.Name);
else
_Fields.Add(fi.Name);
}
}
#endregion
#region Properties
//do properties
foreach (PropertyInfo pi in
_type_to_Draw.GetProperties(Program.RequiredBindings))
{
if (_type_to_Draw == pi.DeclaringType)
{
// add read method if exists
if (pi.CanRead) { propGetters.Add(pi.GetGetMethod(true)); }
// add write method if exists
if (pi.CanWrite) { propSetters.Add(pi.GetSetMethod(true)); }
string pName=getGenericsForType(pi.PropertyType);
//add all the property types to the associations List, so that
//the association lines for this class can be obtained, and
//possibly drawn on the container
pName = LowerAndTrim(pName);
if (!_Associations.Contains(pName))
{
_Associations.Add(pName);
}
//do we want long or short property description displayed
if (Program._IncludePropValues)
_Properties.Add(pName + " " + pi.Name);
else
_Properties.Add(pi.Name);
}
}
#endregion
#region Interfaces
//do interfaces
if (Program._IncludeInterfaces)
{
Type[] tiArray = _type_to_Draw.GetInterfaces();
foreach (Type ii in tiArray)
{
_Interfaces.Add(ii.Name.ToString());
}
}
#endregion
#region Methods
//do methods
foreach (MethodInfo mi in
_type_to_Draw.GetMethods(Program.RequiredBindings))
{
if (_type_to_Draw == mi.DeclaringType)
{
string mDetail = mi.Name + "( ";
string pDetail="";
//do we want to display method arguments, if we do create the
//appopraiate string
if (Program._IncludeMethodArgs)
{
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);
if (!_Associations.Contains(pName))
{
_Associations.Add(pName);
}
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);
if (!_Associations.Contains(rName))
{
_Associations.Add(rName);
}
//do we want to display method return types
if (Program._IncludeMethodReturnType)
mDetail += " : " + rName;
}
else
{
//do we want to display method return types
if (Program._IncludeMethodReturnType)
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 (!Program._ShowPropGetters &&
propGetters.Contains(mi)) { /* hidden get method */ }
else if (!Program._ShowPropSetters &&
propSetters.Contains(mi)) { /* hidden set method */ }
else {
_Methods.Add(mDetail);
}
}
}
#endregion
#region Events
//do events
foreach (EventInfo ei in
_type_to_Draw.GetEvents(Program.RequiredBindings))
{
if (_type_to_Draw == ei.DeclaringType)
{
//add all the event types to the associations List, so that
//the association lines for this class can be obtained, and
//possibly drawn on the container
string eName=getGenericsForType(ei.EventHandlerType);
eName = LowerAndTrim(eName);
if (!_Associations.Contains(eName))
{
_Associations.Add(eName);
}
//do we want long or short event description displayed
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 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 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. Step 5: User Selects Which Classes Should Be Shown On DiagramSo 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 :
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) The specialized panel acts as a container to display the The An example of what is shown within the
It can be seen that there are rows/columns where the individual Basically what happens is that the So what we end up with for each association, is a source 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 #region Constructor
/// <summary>
/// Creates a new AssociationDrawer object using the parameters provided and
/// then calls the internal Draw() method
/// </summary>
/// <param name="g">The graphics object of the
/// <see cref="ClassDrawerContainerPanel">
/// ClassDrawerContainerPanel</see>, so that this class
/// can draw an association
/// line on the panel where the classes are held</param>
/// <param name="ucdSrc">The source
/// <see cref="ucDrawableClass">class</see></param>
/// <param name="ucdDest">The destination
/// <see cref="ucDrawableClass">class</see></param>
/// <param name="genericSpace">The generic space between
/// classes used by the
/// <see cref="ClassDrawerContainerPanel">ClassDrawerContainerPanel</see>
/// when laying out the controls</param>
public AssociationDrawer(Graphics g,ucDrawableClass ucdSrc,
ucDrawableClass ucdDest,int genericSpace)
{
this._ucdSrc = ucdSrc;
this._ucdDest = ucdDest;
this._GenericSpace = genericSpace;
this._g = g;
//do the draw
GetDirectionAndDraw();
}
#endregion
It can be seen that the constructor takes several parameters that allow the
The association rules -> method callsIt can be seen that the association rules make use of the previously set properties for each /// <summary>
/// Works out which direction the association line should be based
/// on the source <see cref="ucDrawableClass">class</see> ContainerRow/
/// ContainerColumn and the destination
/// <see cref="ucDrawableClass">class</see>
/// ContainerRow/ ContainerColumn. When the direction is found,
/// one of the Draw
/// direction methods is called, for example DrawNorth(), DrawSouth()
/// </summary>
private void GetDirectionAndDraw()
{
//NORTH = Row is 1 below the current row, but same column
if (_ucdDest.ContainerRow == _ucdSrc.ContainerRow - 1 &&
_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn)
{
DrawNorth();
}
//SOUTH = Row is 1 above the current row, but same column
if (_ucdDest.ContainerRow == _ucdSrc.ContainerRow + 1 &&
_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn)
{
DrawSouth();
}
//EAST = Column is 1 above the current column, but same row
if (_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn + 1 &&
_ucdDest.ContainerRow == _ucdSrc.ContainerRow)
{
DrawEast();
}
//WEST = Column is 1 below the current column, but same row
if (_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn - 1 &&
_ucdDest.ContainerRow == _ucdSrc.ContainerRow)
{
DrawWest();
}
//NORTH-EAST = Row is 1 or more below and the column is 1 or more above
if (_ucdDest.ContainerRow <= _ucdSrc.ContainerRow - 1 &&
_ucdDest.ContainerColumn >= _ucdSrc.ContainerColumn + 1)
{
DrawNorthEast_DrawSouthEast();
}
//SOUTH-EAST = Row is 1 or more above and the column is 1 or more above
if (_ucdDest.ContainerRow >= _ucdSrc.ContainerRow + 1 &&
_ucdDest.ContainerColumn >= _ucdSrc.ContainerColumn + 1)
{
DrawNorthEast_DrawSouthEast();
}
//NORTH-WEST = Row is 1 or more below and the column is 1 or more below
if (_ucdDest.ContainerRow <= _ucdSrc.ContainerRow - 1 &&
_ucdDest.ContainerColumn <= _ucdSrc.ContainerColumn - 1)
{
DrawNorthWest_DrawSouthWest();
}
//SOUTH-WEST = Row is 1 or more above and the column is 1 or more below
if (_ucdDest.ContainerRow >= _ucdSrc.ContainerRow + 1 &&
_ucdDest.ContainerColumn <= _ucdSrc.ContainerColumn - 1)
{
DrawNorthWest_DrawSouthWest();
}
//NORTH-NON-DIRECT = Row is 2 or more below the current row,
//but same column
if (_ucdDest.ContainerRow <= _ucdSrc.ContainerRow - 2 &&
_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn)
{
DrawNorthNonDirect_DrawSouthNonDirect();
}
//SOUTH-NON-DIRECT = Row is 2 or more above the current row,
//but same column
if (_ucdDest.ContainerRow >= _ucdSrc.ContainerRow + 2 &&
_ucdDest.ContainerColumn == _ucdSrc.ContainerColumn)
{
DrawNorthNonDirect_DrawSouthNonDirect();
}
//EAST-NON-DIRECT = Column is 2 or more above the current column,
//but same row
if (_ucdDest.ContainerRow == _ucdSrc.ContainerRow &&
_ucdDest.ContainerColumn >= _ucdSrc.ContainerColumn+2)
{
DrawEastNonDirect_DrawWestNonDirect();
}
//WEST-NON-DIRECT = Column is 2 or more below the current column,
//but same row
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. /// <summary>
/// Draws a North association line and arrow
/// </summary>
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;
//create a dasked line
Pen p = new Pen(new SolidBrush(Color.Black));
p.DashStyle = DashStyle.Dash;
//and drawn the association
_g.DrawLine(p,new Point(xStart, yStart), new Point(xEnd, yEnd));
//draw the end arrow
_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 (
What Else Can It Do ?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. Customizing What's Shown On The DiagramTo 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:
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. Saving A DiagramI 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 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 " /// <summary>
/// Saves the entire contents of the pnlFlowClasses to an image, specified
/// by the input parameters
/// </summary>
/// <param name="filename">the filename to save the diagram to</param>
/// <param name="imgFormat">the image format to save the diagram as</param>
/// <returns></returns>
private bool SaveTheDiagram(string filename, ImageFormat imgFormat)
{
Cursor.Current = Cursors.WaitCursor;
int bmpSrcWidth = pnlFlowClasses.MaxSize.Width;
int bmpSrcHeight = pnlFlowClasses.MaxSize.Height;
//create a new ClassDrawerContainerPanel (which will not be shown
//on the form)
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;
//save the image, however if we are saving the image
//to the save file name
//the Bitmap object maintains a lock on the physical file,
//so we need to use another dummy Bitmap
//to hold the original image, so that the lock creates
//by the original image saving process can be released,
//then we can savethe dummy Bitmap contents
//back to the original image and conduct the save.
//This is a well documented feature, see the following resources
//http://blog.vishalon.net/
//http://support.microsoft.com/?id=814675
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);
// As original SrcBmp keeps lock on file,
// we need to copy the original image
// to a second image, and then release the lock on the image file.
// Of course,image from the initial image file
// is now existing on the new Graphics object
// which points to the second image,
//ready to copy back to the original image
SrcBmp.Dispose();
SrcBmp = bmpNew;
gfx.Dispose();
//do the save (now that the original lock has been dealt with)
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 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 SupportThere is now support for printing.
Reflector AddinYes 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. | ||||||||||||||||||||||