left-icon

Visual Studio Add-Ins Succinctly®
by Joseph D. Booth

Previous
Chapter

of
A
A
A

CHAPTER 13

Code Model

Code Model


The code model is a language-independent view of a source code file. You can use this view to extract code elements from the classes found within a namespace down to the variables and methods within a class.

Using the code model

In order to illustrate how the code model system works, let’s build a very simple class source file.

using System;

public class ComputePayrollAmount

{

    public string PersonName;

    public double payRate;

    private double TaxRate    = 0.28F;        // Internally used in class.

    public void GetWeeklyPay()

    {

        double TotalPay = 40.0 * payRate;

        string checkLine = WriteCheck(PersonName, TotalPay);

    }

    private string WriteCheck(string forWhom,double Amount)

    {

        string result = "Pay to the order of " + forWhom + " $" + Amount.ToString();

        return result;

    }

}

You can access the code model for the active document using the following code sample:

FileCodeModel fileCM =       dte.ActiveDocument.ProjectItem.FileCodeModel;

Once you have the code model available, one of the properties is a variable called code elements, which contains the code pieces at the current level. In our previous example, two code elements are returned:

  • Import element.
  • Class element.

The import element occurs once for each import or using statement in the file. Whether you are using VB, C++, or C#, each statement to import a module is included.

The second code element is the class statement and will contain the full name of the class, as well as an object property called Children. This object contains the code elements within the class structure. In this case, there will be five elements. The three variables and two methods are included in the Children object.

The fifth child of the class element refers to the WriteCheck() method, and it has two children elements, representing the parameters in the function.

Through recursive calls, you can easily trace a source file from its namespace, to classes within the file, to methods within the classes, to parameters of the methods.

The code element object can represent variables, methods, parameters, namespaces, etc. It has a kind property to know what you are working with, and point properties to keep track of location with the source file.

Get the code model of a source file

The code model is associated with a project item (see Chapter 9). Every document object that is part of a project has a ProjecItem property associated with it. Once you have a reference to the document, you can get the code model by using the following code. In this sample, we are getting the CodeModel property of the currently active document:

FileCodeModel fileCM =       dte.ActiveDocument.ProjectItem.FileCodeModel;

The FileCodeModel class has two properties of interest; one is the language property that contains a GUID string indicating the type of language, C#, C++, or VB. You can use the following constants to determine what language the module is written in:

  • vsCMLanguageCSharp
  • vsCMLanguageVC
  • vsCMLanguageVB

The other property is the collection of code elements. Each code element in this list can have a child collection of additional associated code elements, and can contain children elements as deep as the source code structure goes. Each item in the collection represents a single code element from the source file.

Code element properties

The key properties for working with individual code elements are listed in the following table:

Property

Data Type

Description

Children

Collection

Collection of nested code elements, if any.

FullName

String

Fully qualified (class and variable name) name of element.

Kind

Enumerated

The type of element, such as:

vsCMElementVariable, vsCMElementClass, etc.

Name

String

Name of the code element.

StartPoint

TextPoint

An object pointing to the beginning of the element.

EndPoint

TextPoint

An object referring to the end of the code element.

With these key properties, you can determine the type of code element you are working with, and you can extract it or write it back to the source code window using the point properties.

In addition to providing code elements, the file code model also allows you to add variable elements (classes, variables, namespaces, etc.), get a code element at a particular point in the code, and remove a code element from the source file.

Putting it all together

For our example project, we are going to create an add-in that will read a source code file and document the class details, as well as all the public variables and methods within the class.

To look at what the code will do, consider the following class example, before and after:

using System;

public class ComputePayrollAmount

{

    public string PersonName;

    public double payRate;

    enum Payclass

   {

    FullTime    = 1,

    PartTime    = 2,

    Consultants = 4

};

    private string _firstName;

    public string  FirstName

    {

        get { return _firstName; }

        set { _firstName = value; }

    }

    private double TaxRate    = 0.28F;        // Internally used in class.

    private string _LastName;

    public string LastName

    {

        get { return _LastName; }

        set { _LastName = value; }

    }

    enum MaritalStatus

    {

        Single = 1,

        Married = 2,

        Seperated = 4

    };

    public void GetWeeklyPay()

    {

        double TotalPay = 40.0 * payRate;

        string checkLine = WriteCheck(PersonName, TotalPay);

    }

    public ComputePayrollAmount()

    {

    }

    private string WriteCheck(string forWhom,double Amount)

    {

        string result = "Pay to the order of " + forWhom + " $" + Amount.ToString();

        return result;

    }

}

This is often how classes built over time and by multiple developers end up: public variables, methods, properties, etc., all intermixed in the source code file. After running our add-in, the following comment code is generated and added to the top of the class definition:

// [====================================================================================

//             Class: ComputePayrollAmount

//            Author: jbooth

//              Date: 10/4/2012

// 

// Class Information:

//          Inherits: Object

// 

//  Public Interface:

// 

//         Variables: PersonName (string)

//                    payRate (double) [40.0F]

//        Properties: FirstName (string)

//                    LastName (string)

//           Methods: GetWeeklyPay

//                    WriteCheck(forWhom:string,Amount:double) ==> string

// =====================================================================================]

You can see in this example that the code model distinguished between public and private variables and methods, and also discerned enough to not include the constructor in the list of public methods.

For simplicity's sake, our code assumes a single class in a source file, and will only process the first class it finds. However, you can use this concept as a starting point for enforcing coding standards, making code more readable, etc.

Note: The code will overwrite the existing comment if you run it multiple times

Class documenter

We are still going to use the wizard to create our basic add-in, and then attach our module to the context menu of the code window. So let’s start up the wizard with the following:

  • Visual C# (or your preferred language).
  • Application Host: Only Visual Studio.
  • Name/Description: DocumentClass and Document a class file
  • Create UI Menu and do not load at start-up.

Attaching to the code editor window

The first change we want to make is to move the menu option to appear on the Code window context menu rather than the Tools menu. Our connect code should be changed to the following:

if(connectMode == ext_ConnectMode.ext_cm_UISetup)

  {

   // Create the command object.

   object[] contextGUIDS = new object[] { };

   Commands2 commands = (Commands2)_applicationObject.Commands;

   try

       {

        Command cmd = commands.AddNamedCommand2(_addInInstance, "DocumentClass",

          "Class Documentator","Document your class module ", true, 59, ref contextGUIDS,

          (int)vsCommandStatus.vsCommandStatusSupported +

          (int)vsCommandStatus.vsCommandStatusEnabled,

          (int)vsCommandStyle.vsCommandStylePictAndText,

          vsCommandControlType.vsCommandControlTypeButton);

        // Create a command bar on the code window.

        CommandBar CmdBar = ((CommandBars)_applicationObject.CommandBars)["Code Window"];

        // Add a command to the Code Window's shortcut menu.

        CommandBarControl cmdBarCtl = (CommandBarControl)cmd.AddControl(CmdBar,

                                                       CmdBar.Controls.Count + 1);

        cmdBarCtl.Caption = "Class Doc";

        }

        catch (System.ArgumentException)

        {

        }

}

Getting the code model

We now need to add our code to the Exec() function to get the code model and use its parsing ability to create a documentation header.

if(commandName == "DocumentClass.Connect.DocumentClass)

{

    FileCodeModel2 fileCM = null;

    // Make sure there is an open source-code file.

    try

       {

        fileCM =  (FileCodeModel2)_applicationObject.ActiveDocument.

                                   ProjectItem.FileCodeModel;

       }

    catch

      {

         MessageBox.Show("No active source file is open...");

         handled = true;

         return;

      }

    // Some files (such as XML) will not have an associated code model.

    if (fileCM == null)

       {

          MessageBox.Show("Not a valid programming language source file...");

          handled = true;

          return;

       }

Assuming we’ve gotten a code mode (valid source file), we need to make sure it is a language we can work with, in this case, either VB or C#.

string CommentChar = "";

switch (fileCM.Language)

      {

         case CodeModelLanguageConstants.vsCMLanguageCSharp:

         {

             CommentChar = "//";

             break;

         }

         case CodeModelLanguageConstants.vsCMLanguageVB:

         {

              CommentChar = "'";

              break;

          }

     }

if (CommentChar == "")

  {

      MessageBox.Show("Only works with VB or C# class modules");

      handled = true;

      return;

  }

Finding the class elements

Once we know we’ve got a valid code module, we need to find the code element to locate the first class. Typically, a class declaration is either in the first level, or one level down (child level of the Namespace level). The following code will search two levels deep for a class code element:

// Scan the file, looking for a class construct may require two passes.

CodeElements elts = fileCM.CodeElements;

CodeElement elt = null;

int xClassElt = 0;

int xNameSpace = 0;

int nLevels = 0;

while (xClassElt == 0)

{

      nLevels++;

      for (int i = 1; i <= elts.Count; i++)

          {

             elt = elts.Item(i);

             if (elt.Kind == vsCMElement.vsCMElementClass)

                {

                  xClassElt = i;

                  break;

                }

             if (elt.Kind == vsCMElement.vsCMElementNamespace)

               {

                  xNameSpace = i;

                  break;

                }

          }

 // Found namespace and no class, let's work through the namespace looking for a class.

      if (xNameSpace != 0 && xClassElt == 0)

         {

              elts = elts.Item(xNameSpace).Children;

         }

  // Don't search deeper than three levels.

      if (nLevels > 2)  { break;  }

}

// If no class found, exit.

if (xClassElt == 0)

   {

      MessageBox.Show("No class module found in source file...");

      handled = true;

      return;

   }

Once we’ve found a class element, we grab the child elements (i.e. the variables, methods, etc. that we want to document.)

// Now we are ready to document our class.

  CodeClass theclass = (CodeClass)elts.Item(xClassElt);

  object[] interfaces = {};

  object[] bases = {};

Notice that we’ve cast the generic CodeElement to the more specific CodeClass object, which gives us access to the particular class details. This type casting is necessary to pull additional information from the various code elements, rather than just relying on the properties in the generic CodeElement class.

Building our header

We now create a string builder object and extract some information from our class variable to display in the documentation header text.

// Some initial header info.

StringBuilder sb = new StringBuilder();

sb.AppendLine(CommentChar+"[=========================================================");

if (theclass.Namespace != null)

   {

      sb.AppendLine(CommentChar + "         Namespace: " +

                           theclass.Namespace.Name.ToString());

   }

   if (theclass.IsAbstract)

   {

       sb.Append( CommentChar+"    Abstract ");

   }

   else

   {

       sb.Append( CommentChar+"             ");

   }

   sb.AppendLine("Class: "+theclass.Name);

   sb.AppendLine( CommentChar+"            Author: "+Environment.UserName);

   sb.AppendLine( CommentChar+"              Date: "+DateTime.Now.ToShortDateString());

   sb.AppendLine( CommentChar+"  ");

   sb.AppendLine( CommentChar+" Class Information:");

   // Information about the class.

   string docCategory = "          Inherits:";

   foreach (CodeElement theBase in theclass.Bases)

      {

          sb.AppendLine(CommentChar + docCategory + " " + theBase.Name);

          docCategory = "                   ";

      }

   docCategory = "        Implements:";

   foreach (CodeElement theImpl in theclass.ImplementedInterfaces)

      {

         sb.AppendLine(CommentChar + docCategory + " " + theImpl.Name);

         docCategory = "                   ";

      }

   sb.AppendLine( CommentChar+"  ");

   sb.AppendLine( CommentChar+"  Public Interface:");

   sb.AppendLine( CommentChar+"  ");

You can look to the CodeClass object for more information you might want to display. Our next step is to collect the code elements making up the class and store them in a collection so that we can display them grouped by element type later in the module.

Organizing the code elements

This next section of the code loops through the class elements and organizes them by type into queue structures. Note that we are testing the generic code element’s type and then storing the appropriate typed element type (i.e. CodeVariable, CodeEnum, etc.) into the queue structure for the particular element kind.

elts = theclass.Children;

// Build queues to hold various elements.

Queue<CodeEnum> EnumStack = new Queue<CodeEnum>();

Queue<CodeVariable> VarStack = new Queue<CodeVariable>();

Queue<CodeProperty> PropStack = new Queue<CodeProperty>();

Queue<CodeFunction> FuncStack = new Queue<CodeFunction>();

foreach (CodeElement oneElt in elts)

   {

      // Get the code element, and determine its type.

      switch (oneElt.Kind)

      {

          case vsCMElement.vsCMElementEnum :

          {

              EnumStack.Enqueue((CodeEnum)oneElt);

              break;

           }

           case vsCMElement.vsCMElementVariable:

           {

              VarStack.Enqueue((CodeVariable)oneElt);

              break;

           }

           case vsCMElement.vsCMElementProperty:

           {

               PropStack.Enqueue((CodeProperty)oneElt);

               break;

           }

           case vsCMElement.vsCMElementFunction:

           {

               FuncStack.Enqueue((CodeFunction)oneElt);

               break;

           }

           default :   { break; };

      }

 }

Once we’ve collected and built our queue collections, we can now write them back to the documentation comment text organized by code element type.

Variables

For each public variable, we want to report the variable name and data type, as well as any initial value it might be set to. Note that the variables (and all code elements) are documented in the order they are encountered; they are not sorted in the queue.

// Iterate through the variables looking for public variables.

foreach (CodeVariable theVar in VarStack) 

    {

       if (theVar.Access == vsCMAccess.vsCMAccessPublic)

          {

            sb.Append(CommentChar + docCategory + " " + theVar.Name +

                 " (" + theVar.Type.AsString + ")");

           docCategory = "                   ";

           if (!(theVar.InitExpression == null))

             {

                sb.Append(" ["+theVar.InitExpression.ToString()+"]");

             }

           sb.AppendLine("");

         }

   }

Enums

For the enumerated types, we want to report the name of the enumeration and all of the elements (stored as children variables to the enum itself). The following code illustrates how to walk through the enum and its children elements.

docCategory = "             Enums:";

foreach (CodeEnum theEnum in EnumStack)

   {

      if (theEnum.Access == vsCMAccess.vsCMAccessPublic)

         {

            sb.Append(CommentChar + docCategory + " " + theEnum.Name + " ");

            docCategory = "                   ";

            if (theEnum.Children.Count > 0)

               {

                  sb.Append("(");

                  for (int xx = 1; xx <= theEnum.Children.Count; xx++)

                   {

                     int yy = theEnum.Children.Count - xx + 1;

                     CodeVariable theVar = (CodeVariable)theEnum.Children.Item(yy);

                     sb.Append(theVar.Name);

                     if (yy > 1) { sb.Append(","); }

                   }

                   sb.Append(")");

               }

             sb.AppendLine("");

         }

    }

Properties

For each public property, we want to display the property name and the property’s data type. The following code loops through the collected CodeProperty elements and does just that.

docCategory = "        Properties:";

foreach (CodeProperty theProp in PropStack)

    {

      if (theProp.Access == vsCMAccess.vsCMAccessPublic)

         {

          sb.Append(CommentChar + docCategory + " " + theProp.Name +

                             " (" + theProp.Type.AsString + ")");

          sb.AppendLine("");

          docCategory = "                   ";

          }

   }

Methods

As we process the class methods, we want to only report on public methods and exclude the constructor from the documentation. We need to handle parameters and the return type (if not VOID).

docCategory = "           Methods:";

foreach (CodeFunction theFunc in FuncStack)

{

     if (theFunc.FunctionKind != vsCMFunction.vsCMFunctionConstructor &&

                               theFunc.Access==vsCMAccess.vsCMAccessPublic)

    {

         sb.Append(CommentChar + docCategory + " " + theFunc.Name);

         docCategory = "                   ";

         if (theFunc.Parameters.Count > 0)

         {

            int yy = theFunc.Parameters.Count;

            sb.Append("(");

            foreach (CodeParameter theParam in theFunc.Parameters)

               {

                  sb.Append(theParam.Name+":"+theParam.Type.AsString);

                  yy--;

                  if (yy > 0) { sb.Append(","); }

               }

           sb.Append(")");

         }

     if (theFunc.Type.AsString.ToUpper().EndsWith("VOID")==false)

       {

           sb.Append(" ==> " + theFunc.Type.AsString);

       }

     sb.AppendLine("");

   }

}

Writing the header back to the source window

By this point, we have a nicely formatted documentation block of text, showing all the public elements of the class. We are going to use our text document and edit point objects to either write the text to the top of the file, or update the prior version of the documentation. This allows you to run the add-in as often as you want after you’ve added new public code elements to the class.

TextDocument theText = (TextDocument)_applicationObject.ActiveDocument.Object();

EditPoint thePoint = theText.CreateEditPoint();

// Check and see if a comment already exists.

string theLine = thePoint.GetText(thePoint.LineLength);

bool FoundOldComment = false;

string OldComment = theLine+Environment.NewLine;

if (theLine.StartsWith(CommentChar + " [==="))           // Start of delimiter.

    {

       while (thePoint.AtEndOfDocument == false && FoundOldComment==false)

          {

              thePoint.LineDown(1);

              theLine = thePoint.GetText(thePoint.LineLength);

              OldComment+= theLine+Environment.NewLine;

              FoundOldComment = theLine.StartsWith(CommentChar) &&

                                theLine.EndsWith("==]");

           }

    }

    if (FoundOldComment)

       {

           thePoint = theText.CreateEditPoint(theText.StartPoint);

           thePoint.ReplacePattern(theText.EndPoint, OldComment, sb.ToString());

       }

    else

       {

           thePoint.Insert(sb.ToString());

        }

Summary

The code model features of Visual Studio allow you to determine code elements without the need to write your own parsing routines. Although not all elements are returned in the code collection (such as compiler directives), the model gives you a great starting point for writing add-in modules to work with the code in a source file.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.