Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

100% Reflective Class Diagram Creation Tool

0.00/5 (No votes)
14 Jun 2011 12  
100% Reflective Class Diagram Creation Tool

Version II

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

Introduction

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.

What This Article Is

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.

AutoDiagrammer: Features

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 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.

  1. Ask the user to select a file to reflectively draw the classes for.
  2. 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.
  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.
  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.
  5. 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.

Step 1 : Opening A File

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))

{



//proceed

}

else

{



//not a valid CLR so tell user and do nothing more

}

Step 2: Determining If A File Is A Valid CLR Type

So how does the DotNetObject.isDotNetAssembly() method actually work? Well, it works as follows:

/// <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 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.

//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 BackGroundWorker is created, to deal with the further analysis of the current assembly.

Step 3: Create The Tree And Classes

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.

/// <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 BackGroundWorker really does is to call the StartAnylsisProcess(a); method passing it the current assembly. So lets have a look at that.

/// <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 addTypesToTree(_tvnRoot, t.Name, t.Namespace, t); so I'll also show this

/// <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 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>>();

Step 3a : Should Type Be Included

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.

/// <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 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 :

/// <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 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.

/// <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 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.

Step 5: User Selects Which Classes Should Be Shown On Diagram

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
/// <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 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.

/// <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 (ucDrawableClass controls) look like in the various states:fully expanded / fully collapsed, partially collapsed (individual ucExpander controls collapsed).

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 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.

Saving A Diagram

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:

/// <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 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.

Reflector Addin

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.

So What Do 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.

Conclusion

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.

History

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here