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

Line Counter - Writing a Visual Studio 2005 & 2008 Add-In

0.00/5 (No votes)
13 May 2009 11  
Provides a complete Solution and Project line counting and summary tool, written as a Visual Studio 2005/2008 Managed Add-in.
Sample Image - LineCounterAddin.gif

Background

I have long been a fan of PLC (Project Line Counter) from WndTabs.com. This little utility has helped me keep track of and even gauge the progress of development projects for a few years now. I have been patiently waiting for Oz Solomon, the author of PLC, to release an update for Visual Studio 2005. I finally found some free time today and decided to see if I could update it myself. It didn't take long for me to realize I could probably write my own line counter add-in in less time than it would take me to figure out Oz's code and migrate his existing code to a VS 2005 version. So, here I am, writing to all you fine coders about my first VS add-in. I hope you find both this article and the product behind it useful. I welcome comments, improvements and suggestions, as I will be continuing to improve this little utility over time.

Visual Studio Automation and Extension

One of the greatest things about Visual Studio is its extensibility. Many of you will already be somewhat familiar with some of the features I'll be covering in this article. If you have previously written add-ins for any version of Visual Studio, or even if you have written any macros to help streamline your workflow, you've used the automation and extensibility objects that Visual Studio provides. These features are most commonly referred to as DTE, or the design time environment. This object exposes all of the different parts and pieces of Visual Studio's UI and tools to the smart programmer.

Using the DTE object, you can programmatically control just about everything in Visual Studio, from toolbars, docking tool windows, and even edit files or initiate compilation. One of the simplest uses of the DTE object is through macros. Using macros, you can do quite a lot, from simple tasks such as find and replace to complex tasks such as creating commented properties for all your variables except specific kinds. The same DTE object that is exposed through macros is also exposed through the add-in extensibility projects. Creating a Visual Studio Add-in with the Add-in Wizard, you can create the basic shell of what you could call a very advanced macro.

Screenshot - AddInWizard.gif

Visual Studio Add-ins can be written in any language, which you can choose while running the Add-in Wizard. The wizard will present you with several other options, too. This version of this article will not cover the details of what these other options do, not yet. Suffice to say, you have the option of causing your add-in to run when Visual Studio starts up. You can also add a tool bar button for your add-in that will appear when VS starts up, whether that be manually or automatically.

Screenshot - AddInLanguagePicker.gif

Creating an Add-in

After you finish the Add-in Wizard, you will have a new project with a single file of interest: Connect.cs. This little file is the starting point of any Visual Studio add-in. It implements a few key interfaces and provides some starting code in a few key methods. The most important method for now is:

OnConnection(object application, ext_ConnectMode connectMode, 
				object addInInst, ref Array custom)

When Visual Studio starts your add-in, this method is the first thing it calls. It is here that any initialization code needs to go. You could technically do anything you needed to here, as long as it worked within the bounds imposed by Visual Studio's Automation model. This is something which I myself haven't fully mapped out yet, but sometimes things need to be done a certain way. Currently, this method should be pre-populated with code created by the Add-in Wizard, which begins the implementation of whatever options you chose (such as adding a Tools menu item, for example). Most of the code in OnConnection is well documented, so we won't go into detailed explanations about all of it. One important thing of note, however, is the first three lines:

_applicationObject = (DTE2)application;
_addInInstance = (AddIn)addInInst;
if(connectMode == ext_ConnectMode.ext_cm_UISetup)
{
    // ...
}

The first line caches the DTE object, which is provided by Visual Studio when it starts the add-in. The second line caches the instance of the add-in itself, which is often required for many of the calls you may make from your add-in's code. The third line, the if statement, allows for conditional processing when the add-in is started. Visual Studio will often start an add-in a couple times. The first time allows it to set up its own UI with menu items, tool bar buttons, etc. Additional start ups are caused when the add-in is actually being run, which can happen in two different ways: automatically when VS starts or through some other process after VS has started.

The rest of the code that already exists in the OnConnection method is commented and will differ depending on what options you chose in the wizard. For the Line Counter add-in, we will actually be removing all of the generated code and replacing it with our own. If you wish to follow along with this article as I explain how to create a tool window add-in, create a new add-in project now with the following settings:

Project Name: LineCounterAddin
Language: C#
Name: Line Counter
Description: Line Counter 2005 - Source Code Line Counter
Other Options: Leave at defaults

Once the project has been created, add the following references:

System.Drawing
System.Windows.Forms

Finally, add a new User Control named LineCounterBrowser. This user control will be the primary interface of our add-in, and it works just like any normal Windows Form. You can design, add event handlers, etc. with the visual designer. We won't go into the details of building the user control in this article, as you can download the complete source code at the top of this page. For now, just open the source code of your new user control and add this code:

#region Variables
private DTE2 m_dte;
    // Reference to the Visual Studio DTE object
#endregion

/// <summary>
/// Receives the VS DTE object
/// </summary>
public DTE2 DTE
{
    set 
    {
        m_dte = value;
    }
}
#endregion

We won't need anything else in the User Control source code for now. This property and the corresponding variable provide a way for us to pass in the DTE object reference from the Connect class to our UI class. We will actually set the property in the OnConnection method of the Connect class. The full code of OnConnection should be as follows. It is well-commented, so further explanation should not be necessary.

public void OnConnection(object application, 
    ext_ConnectMode connectMode, 
    object addInInst, ref Array custom)
{
    // Cache the DTE and add-in instance objects
    _applicationObject = (DTE2)application;
    _addInInstance = (AddIn)addInInst;

    // Only execute the startup code if the connection mode is a startup mode
    if (connectMode == ext_ConnectMode.ext_cm_AfterStartup 
        || connectMode == ext_ConnectMode.ext_cm_Startup)
    {
        try
        {
            // Declare variables
            string ctrlProgID, guidStr;
            EnvDTE80.Windows2 toolWins;
            object objTemp = null;

            // The Control ProgID for the user control
            ctrlProgID = "LineCounterAddin.LineCounterBrowser";

            // This guid must be unique for each different tool window,
            // but you may use the same guid for the same tool window.
            // This guid can be used for indexing the windows collection,
            // for example: applicationObject.Windows.Item(guidstr)
            guidStr = "{2C73C576-6153-4a2d-82FE-9D54F4B6AD09}";

            // Get the executing assembly...
            System.Reflection.Assembly asm = 
                System.Reflection.Assembly.GetExecutingAssembly();

            // Get Visual Studio's global collection of tool windows...
            toolWins = (Windows2)_applicationObject.Windows;

            // Create a new tool window, embedding the 
            // LineCounterBrowser control inside it...
            m_toolWin = toolWins.CreateToolWindow2(
                 _addInInstance, 
                 asm.Location, 
                 ctrlProgID, 
                 "Line Counter", 
                 guidStr, ref objTemp);

            // Pass the DTE object to the user control...
            LineCounterBrowser browser = (LineCounterBrowser)objTemp;
            browser.DTE = _applicationObject;

            // and set the tool windows default size...
            m_toolWin.Visible = true;        // MUST make tool window
                                             // visible before using any 
                                             // methods or properties,
                                             // otherwise exceptions will 
                                             // occur.
                                            
            // You can set the initial size of the tool window
            //m_toolWin.Height = 400;
            //m_toolWin.Width = 600;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            Console.WriteLine(ex.StackTrace);
        }
        
        // Create the menu item and toolbar for starting the line counter
            if (connectMode == ext_ConnectMode.ext_cm_UISetup)
        {
        // Get the command bars collection, and find the 
        // MenuBar command bar
        CommandBars cmdBars = 
            ((Microsoft.VisualStudio.CommandBars.CommandBars)
            _applicationObject.CommandBars);
            CommandBar menuBar = cmdBars["MenuBar"];

        // Add command to 'Tools' menu
        CommandBarPopup toolsPopup = 
            (CommandBarPopup)menuBar.Controls["Tools"];
        AddPopupCommand(toolsPopup, 
            "LineCounterAddin", 
            "Line Counter 2005", 
            "Display the Line Counter 2005 window.", 1);

        // Add new command bar with button
        CommandBar buttonBar = AddCommandBar("LineCounterAddinToolbar", 
            MsoBarPosition.msoBarFloating);
        AddToolbarCommand(buttonBar, 
            "LineCounterAddinButton", 
            "Line Counter 2005", 
            "Display the Line Counter 2005 window.", 1);
        }
    }
}

// The tool window object
private EnvDTE.Window m_toolWin;

The OnConnection method will be run several times at different points during the duration of Visual Studio's execution. We are concerned with two of the possible reasons for the method being called: once for UI Setup and once for Startup. When the OnConnection method is called for UI Setup, we will want to update Visual Studio's user interface with a menu item and toolbar button for our add-in. This is done in the second if statement of the OnConnection method. When the OnConnection method is called for Startup -- which has two different methods: when VS starts and after VS starts -- we want to display our add-in.

When performing UI Setup, I have created several private helper functions to simplify the process. Below, you can find numerous methods that will facilitate the creation of new CommandBars in Visual Studio, as well as adding commands to those bars. These functions include adding new menu items to menus. The code is commented well enough that it is pretty self-explanatory. One thing to note about these functions is that they assume your add-in project has a custom UI assembly that contains all of the images you wish to use for your commands, both menu items and buttons on toolbars. I'll explain how to add custom icons later.

/// <summary>
/// Add a command bar to the VS2005 interface.
/// </summary>
/// <param name="name">The name of the command bar</param>
/// <param name="position">Initial command bar positioning</param>
/// <returns></returns>
private CommandBar AddCommandBar(string name, MsoBarPosition position)
{
    // Get the command bars collection
    CommandBars cmdBars = 
        ((Microsoft.VisualStudio.CommandBars.CommandBars)
        _applicationObject.CommandBars);
    CommandBar bar = null;

    try
    {
        try
        {
            // Create the new CommandBar
            bar = cmdBars.Add(name, position, false, false);
        }
        catch (ArgumentException)
        {
            // Try to find an existing CommandBar
            bar = cmdBars[name];
        }
    }
    catch
    {
    }

    return bar;
}

/// <summary>
/// Add a menu to the VS2005 interface.
/// </summary>
/// <param name="name">The name of the menu</param>
/// <returns></returns>
private CommandBar AddCommandMenu(string name)
{
    // Get the command bars collection
    CommandBars cmdBars = 
        ((Microsoft.VisualStudio.CommandBars.CommandBars)
        _applicationObject.CommandBars);
    CommandBar menu = null;

    try
    {
        try
        {
            // Create the new CommandBar
            menu = cmdBars.Add(name, MsoBarPosition.msoBarPopup, 
                            false, false);
        }
        catch (ArgumentException)
        {
            // Try to find an existing CommandBar
            menu = cmdBars[name];
        }
    }
    catch
    {
    }

    return menu;
}

/// <summary>
/// Add a command to a popup menu in VS2005.
/// </summary>
/// <param name="popup">The popup menu to add the command to.</param>
/// <param name="name">The name of the new command.</param>
/// <param name="label">The text label of the command.</param>
/// <param name="ttip">The tooltip for the command.</param>
/// <param name="iconIdx">The icon index, which should match the resource ID 
in the add-ins resource assembly.</param>
private void AddPopupCommand(
    CommandBarPopup popup, string name, string label, 
string ttip, int iconIdx)
{
    // Do not try to add commands to a null menu
    if (popup == null)
        return;

    // Get commands collection
    Commands2 commands = (Commands2)_applicationObject.Commands;
    object[] contextGUIDS = new object[] { };

    try
    {
        // Add command
        Command command = commands.AddNamedCommand2(_addInInstance, 
            name, label, ttip, false, iconIdx, ref contextGUIDS,
            (int)vsCommandStatus.vsCommandStatusSupported + 
            (int)vsCommandStatus.vsCommandStatusEnabled, 
            (int)vsCommandStyle.vsCommandStylePictAndText, 
            vsCommandControlType.vsCommandControlTypeButton);
        if ((command != null) && (popup != null))
        {
            command.AddControl(popup.CommandBar, 1);
        }
    }
    catch (ArgumentException)
    {
        // Command already exists, so ignore
    }
}

/// <summary>
/// Add a command to a toolbar in VS2005.
/// </summary>
/// <param name="bar">The bar to add the command to.</param>
/// <param name="name">The name of the new command.</param>
/// <param name="label">The text label of the command.</param>
/// <param name="ttip">The tooltip for the command.</param>
/// <param name="iconIdx">The icon index, which should match the resource ID 
in the add-ins resource assembly.</param>
private void AddToolbarCommand(CommandBar bar, string name, string label, 
string ttip, int iconIdx)
{
    // Do not try to add commands to a null bar
    if (bar == null)
        return;

    // Get commands collection
    Commands2 commands = (Commands2)_applicationObject.Commands;
    object[] contextGUIDS = new object[] { };

    try
    {
        // Add command
        Command command = commands.AddNamedCommand2(_addInInstance, name,
            label, ttip, false, iconIdx, ref contextGUIDS,
            (int)vsCommandStatus.vsCommandStatusSupported + 
            (int)vsCommandStatus.vsCommandStatusEnabled, 
            (int)vsCommandStyle.vsCommandStylePict, 
            vsCommandControlType.vsCommandControlTypeButton);
        if (command != null && bar != null)
        {
            command.AddControl(bar, 1);
        }
    }
    catch (ArgumentException)
    {
        // Command already exists, so ignore
    }
}

Now that we have code to properly integrate our add-in into the Visual Studio user interface and display our add-in when requested, we need to add command handlers. Handling commands in a Visual Studio add-in is a pretty simple task. The IDTCommandTarget interface, which our Connect class implements, provides the necessary methods to properly process commands from Visual Studio. You will need to update the QueryStatus and Exec methods as follows to display the Line Counter add-in when its menu item or tool bar button is clicked.

public void QueryStatus(string commandName, 
    vsCommandStatusTextWanted neededText, 
    ref vsCommandStatus status, ref object commandText)
{
    if(neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
    {
        // Respond only if the command name is for our menu item 
        // or toolbar button
        if (commandName == "LineCounterAddin.Connect.LineCounterAddin" 
            || commandName == 
            "LineCounterAddin.Connect.LineCounterAddinButton")
        {
            // Disable the button if the Line Counter window 
                        // is already visible
            if (m_toolWin.Visible)
            {
                // Set status to supported, but not enabled
                status = (vsCommandStatus)
                    vsCommandStatus.vsCommandStatusSupported;
            }
            else
            {
                // Set status to supported and enabled
                status = (vsCommandStatus)
                    vsCommandStatus.vsCommandStatusSupported |
                    vsCommandStatus.vsCommandStatusEnabled;
            }
            return;
        }
    }
}

public void Exec(string commandName, vsCommandExecOption executeOption, 
ref object varIn, ref object varOut, ref bool handled)
{
    handled = false;
    if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
    {
        // Respond only if the command name is for our menu item or 
        // toolbar button
        if (commandName == "LineCounterAddin.Connect.LineCounterAddin" 
            || commandName == 
            "LineCounterAddin.Connect.LineCounterAddinButton")
        {
            // Only display the add-in if it is not already visible
            if (m_toolWin != null && m_toolWin.Visible == false)
            {
                m_toolWin.Visible = true;
            }

            handled = true;
            return;
        }
    }
}

With the OnConnection method is completed, your add-in will be created as a floating tool window. The complete user control will allow you to calculate the line counts and totals of each project in your solution, as well as a summary of all the lines in the whole solution. You can download the source code at the top of this article, compile it, and start the project through the debugger to test it out and examine control flow as the add-in starts up. As you can see, the volume of code that is necessary to create an add-in is relatively simple and straightforward. Let's continue on to some of the details of how the line counter itself -- the user control, essentially -- was written.

Using Custom Icons for your Commands

When you create a Visual Studio Add-in that provides a menu item or tool bar button, Visual Studio will default the commands to using the default Microsoft Office icons. In particular, the icon used will be a yellow smiley face (icon index #59, to be exact). Usually, the icons available as part of the MSO library will not be what you're looking for. Creating and using custom icons for your commands isn't particularly hard, but the documentation for doing so is well-hidden and not exactly straightforward.

The first step in adding your own custom icons with your commands is to add a new resource file to your add-in project. Right-click the LineCounterAddin project in the solution explorer, point to Add, and choose 'New Item...' from the menu. Add a new resource file called ResourceUI.resx. After you have added the resource file, select it in the solution explorer and change the 'Build Action' property to 'None.' We will perform our own processing of this file with a post-build event later on.

Screenshot - AddResourceFile.gif

Now that we have a new resource file, we need to add an image to it. If it is not already open, open the resources file and click the down arrow next to 'Add Resource.' Choose 'Bmp...' from the 'New Image' menu. When prompted to name the image, simply call it 1. All image resources that will be used by Visual Studio are referenced by their index and the resource ID should be the same as that index. For this add-in, we will only need one image. Once the image is added, open it up and change its size to 16x16 pixels and its color depth to 16 color. Visual Studio will only display images if they have a color depth of 4 or 24, and it will use a Lime color (RGB of 0, 254, 0) as the transparency mask for 16 color images. The 1.bmp image in the Resources folder of the LineCounterAddin project that you can download at the top of the page contains a simple icon for this add-in.

Once you have properly created a new resources file and added an image, you will need to set it up to build properly. This particular resources file must be compiled as a satellite assembly. We can accomplish this with a post-build event. To edit build events, right-click the LineCounterAddin project in the solution explorer and choose properties. A new tool will open in the documents area, with a tabbed interface for editing project properties. Find the Build Events tab as in the following figure.

Screenshot - PostBuildEvent.gif

In the 'Post-build event command line' area, add the following script:

f:
cd $(ProjectDir)
mkdir $(ProjectDir)$(OutDir)en-US
"$(DevEnvDir)..\..\SDK\v2.0\Bin\Resgen" $(ProjectDir)ResourceUI.resx
"$(SystemRoot)\Microsoft.NET\Framework\v2.0.50727\Al" 
     /embed:$(ProjectDir)ResourceUI.resources 
     /culture:en-US 
     /out:$(ProjectDir)$(OutDir)en-US\LineCounterAddin.resources.dll
del $(ProjectDir)Resource1.resources

NOTE: Make sure you change the first line, 'f:', to represent the drive you have the project on. This is important, as otherwise the Resgen command will not be able to find the files referenced by the ResourceUI.resx file. Also note that you will need to have the .NET 2.0 SDK installed, otherwise the Resgen command will not be available. The script should generally otherwise work, as it is based on macros rather than fixed paths. Once you have the post-build script in place, a satellite assembly for your add-in should be compiled every time you build your project or solution, and it will be put in the en-US subdirectory of your build output folder. When you run the project, Visual Studio will reference this satellite assembly to find any command bar images.

Counting Lines

Now that you've seen how to create an add-in that displays a new tool window, it's time to move on to some of the juicier code. The bulk of the add-in is written like any old Windows Forms application, with a user interface, event handlers and helper functions. The requirements for this application are fairly simple and a few basic design patterns will help us meet those requirements:

  • PRIMARY GOAL: Display line count information for each project in the loaded solution.
  • Display grand total counts for the solution, and summed counts for each project.
  • Display count information for each individual countable file in each project.
  • Count code lines, comment lines, blank lines, and show total and net code/comment lines.
  • Accurately count lines for different kinds of source files such as C++/C#, VB, XML, etc.
  • Allow the file list to be sorted by name, line counts, file extension.
  • Allow the file list to be grouped by file type, project, or not grouped at all.
  • Display processing progress during recalculation.

Let us start by giving ourselves a clean, structured source file for the user control. Your user control source file should have the following structure:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.IO;

using Microsoft.VisualStudio.CommandBars;

using Extensibility;
using EnvDTE;
using EnvDTE80;

namespace LineCounterAddin
{
    public partial class LineCounterBrowser : UserControl
    {
        #region Nested Classes
        // IComparer classes for sorting the file list
        #endregion
        
        #region Constructor
        #endregion
        
        #region Variables
        private DTE2 m_dte;     // Reference to the Visual Studio DTE object
        #endregion
        
        #region Properties
        /// <summary>
        /// Receives the VS DTE object
        /// </summary>
        public DTE2 DTE
        {
            set
            {
                m_dte = value;
            }
        }
        #endregion
        
        #region Handlers
        // UI Event Handlers 
        #endregion
        
        #region Helpers
        #region Line Counting Methods
        // Line counting methods for delegates
        #endregion
        
        #region Scanning and Summing Methods
        // Solution scanning and general line count summing
        #endregion
        #endregion
    }
    
    #region Support Structures
    // Delegate for pluggable line counting methods
    delegate void CountLines(LineCountInfo info);
    
    /// <summary>
    /// Encapsulates line count sum details.
    /// </summary>
    class LineCountDetails
    {
        // See downloadable source for full detail
    }
    
    /// <summary>
    /// Wraps a project and the line count total detail
    /// for that project. Enumerates all of the files
    /// within that project.
    /// </summary>
    class LineCountSummary
    {
        // See downloadable source for full detail
    }
    
    /// <summary>
    /// Wraps a project source code file and the line 
    /// count info for that file. Also provides details
    /// about the file type and what icon should be shown
    /// for the file in the UI.
    /// </summary>
    class LineCountInfo
    {
        // See downloadable source for full detail
    }
    #endregion
}

From the above preliminary code, you can deduce some of the tricks that will be used to allow multiple types of source code files to be accurately counted, and how we will be able to sort the file list in a variety of ways. The support structures at the bottom help simplify the code by encapsulating data. See the full source for the details.

First off, let's cover how we will allow multiple counting algorithms to be used seamlessly, without requiring ugly if/else or switch syntax. A GREAT feature of modern languages is function pointers, which in .NET are provided by delegates. Most of the time, I think the value of delegates is sorely overlooked in .NET applications these days. So, I'm going to provide a simple but elegant example of how they can make life easier for the clever developer. The concept is simple: create a list of mappings between file extensions and delegates to line-counting functions. With .NET 2.0 and Generics, we can do this very efficiently. Update your source code in the following places as so:

#region Constructor
public LoneCounterBrowser()
{
    // ...
  
    // Prepare counting algorithm mappings
    CountLines countLinesGeneric = new CountLines(CountLinesGeneric);
    CountLines countLinesCStyle = new CountLines(CountLinesCStyle);
    CountLines countLinesVBStyle = new CountLines(CountLinesVBStyle);
    CountLines countLinesXMLStyle = new CountLines(CountLinesXMLStyle);

    m_countAlgorithms = new Dictionary<string, CountLines>(33);
    m_countAlgorithms.Add("*", countLinesGeneric);
    m_countAlgorithms.Add(".cs", countLinesCStyle);
    m_countAlgorithms.Add(".vb", countLinesVBStyle);
    m_countAlgorithms.Add(".vj", countLinesCStyle);
    m_countAlgorithms.Add(".js", countLinesCStyle);
    m_countAlgorithms.Add(".cpp", countLinesCStyle);
    m_countAlgorithms.Add(".cc", countLinesCStyle);
    m_countAlgorithms.Add(".cxx", countLinesCStyle);
    m_countAlgorithms.Add(".c", countLinesCStyle);
    m_countAlgorithms.Add(".hpp", countLinesCStyle);
    m_countAlgorithms.Add(".hh", countLinesCStyle);
    m_countAlgorithms.Add(".hxx", countLinesCStyle);
    m_countAlgorithms.Add(".h", countLinesCStyle);
    m_countAlgorithms.Add(".idl", countLinesCStyle);
    m_countAlgorithms.Add(".odl", countLinesCStyle);
    m_countAlgorithms.Add(".txt", countLinesGeneric);
    m_countAlgorithms.Add(".xml", countLinesXMLStyle);
    m_countAlgorithms.Add(".xsl", countLinesXMLStyle);
    m_countAlgorithms.Add(".xslt", countLinesXMLStyle);
    m_countAlgorithms.Add(".xsd", countLinesXMLStyle);
    m_countAlgorithms.Add(".config", countLinesXMLStyle);
    m_countAlgorithms.Add(".res", countLinesGeneric);
    m_countAlgorithms.Add(".resx", countLinesXMLStyle);
    m_countAlgorithms.Add(".aspx", countLinesXMLStyle);
    m_countAlgorithms.Add(".ascx", countLinesXMLStyle);
    m_countAlgorithms.Add(".ashx", countLinesXMLStyle);
    m_countAlgorithms.Add(".asmx", countLinesXMLStyle);
    m_countAlgorithms.Add(".asax", countLinesXMLStyle);
    m_countAlgorithms.Add(".htm", countLinesXMLStyle);
    m_countAlgorithms.Add(".html", countLinesXMLStyle);
    m_countAlgorithms.Add(".css", countLinesCStyle);
    m_countAlgorithms.Add(".sql", countLinesGeneric);
    m_countAlgorithms.Add(".cd", countLinesGeneric);

    // ...
}
#endregion

#region Variables
// ...
private Dictionary<string, CountLines> m_countAlgorithms;
#endregion

Now that we have specified the mappings, we need to create the actual functions that will be called. These functions are very simple and only need to match the signature provided by the previous delegate declaration of delegate void CountLines(LineCountInfo info). In the Line Counting Methods region of your class, create four private methods:

private void CountLinesGeneric(LineCountInfo info)
private void CountLinesCStyle(LineCountInfo info)
private void CountLinesVBStyle(LineCountInfo info)
private void CountLinesXMLStyle(LineCountInfo info)

All four of these functions match the CountLines delegate signature and are mapped to the appropriate file extensions with the code we added to the default constructor. It is now a simple matter of passing in the right key to m_countAlgorithms and calling the delegate that is returned. In the event of a KeyNotFoundException, we just use the '*' key to get the default generic parser. No ugly, unmanageable if/else monstrosities or endless switch statements are to be found. We have also made it possible to add additional parsing routines in the future without much effort. This will be discussed in greater detail later.

The bulk of the line counting and summing code is housed in the rest of the helper functions. There are two parts to counting: scanning the solution for projects and files, and the actual summing. The methods are listed below. For now, I won't go into detail about how all of this code works. I'll cover the details either later on in an update or with a supplemental article. The main trick of counting many different kinds of source files using the generic dictionary and delegates as discussed above was the most important aspect for this article.

Sorting, Sorting, Sorting

The last concept I wish to cover in this article is the sorting of the file list. I often see .NET developers asking how to sort the items in a ListView. The answers are usually seldom and far between. As I believe this Line Counter add-in will be a very useful utility for many people, I'm hoping that my explanation of sorting a ListView gets broad exposure here. In the end, the concept is actually very simple. Using the Template Method pattern can make it very easy to sort multiple columns of different data in different ways. To start, let's add an abstract class to the Nested Classes region of the user control:

#region Nested Classes
abstract class ListViewItemComparer : System.Collections.IComparer
{
    public abstract int Compare(ListViewItem item1, ListViewItem item2);

    public ListView SortingList;

    #region IComparer Members
    int System.Collections.IComparer.Compare(object x, object y)
    {
        if (x is ListViewItem && y is ListViewItem)
        {
            int diff = Compare((ListViewItem)x, (ListViewItem)y);
            if (SortingList.Sorting == SortOrder.Descending)
                diff *= -1;

            return diff;
        }
        else
        {
            throw new ArgumentException("One or both of the arguments 
                are not ListViewItem objects.");
        }
    }
    #endregion
}

This class serves as the abstract home of our "Template Method." The Template Method pattern simply provides a common, skeleton method on an abstract class that defers some or all of the actual algorithmic code to subclasses. This will simplify our sorting by allowing us to use a single type and a single method when sorting, but with a different sorting algorithm for each column of the ListView. For this to be possible, we must implement several more nested classes for each type of column to be sorted. To see the details of each of these classes, see the full source code. Once we have our explicit sorting algorithms defined, we need to implement a simple event handler for the ListView.ColumnClick event:

private int lastSortColumn = -1;    // Track the last clicked column

/// <summary>
/// Sorts the ListView by the clicked column, automatically
/// reversing the sort order on subsequent clicks of the 
/// same column.
/// </summary>
/// <param name="sender"></param>
/// <param name="e">Provides the index of the clicked column.</param>
private void lvFileList_ColumnClick(object sender, ColumnClickEventArgs e)
{
    // Define a variable of the abstract (generic) comparer
    ListViewItemComparer comparer = null;

    // Create an instance of the specific comparer in the 'comparer' 
    // variable. Since each of the explicit comparer classes is
    // derived from the abstract case class, polymorphism applies.
    switch (e.Column)
    {
        // Line count columns
        case 1:
        case 2: 
        case 3: 
            comparer = new FileLinesComparer(); 
            break;
        // The file extension column
        case 4: 
            comparer = new FileExtensionComparer(); 
            break;
        // All other columns sort by file name
        default: 
            comparer = new FileNameComparer(); 
            break;
    }

    // Set the sorting order
    if (lastSortColumn == e.Column)
    {
        if (lvFileList.Sorting == SortOrder.Ascending)
        {
            lvFileList.Sorting = SortOrder.Descending;
        }
        else
        {
            lvFileList.Sorting = SortOrder.Ascending;
        }
    }
    else
    {
        lvFileList.Sorting = SortOrder.Ascending;
    }
    lastSortColumn = e.Column;

    // Send the comparer the list view and column being sorted
    comparer.SortingList = lvFileList;
    comparer.Column = e.Column;

    // Attach the comparer to the list view and sort
    lvFileList.ListViewItemSorter = comparer;
    lvFileList.Sort();
}

While it may not be readily apparent by that code, the "Template Method" of the ListViewItemComparer abstract base class -- which also happens to be the implementation of the IComparer.Compare(object, object) interface -- is called by the ListView.Sort() method when it compares each list view item. Since each of our explicit comparer classes derives from the ListViewItemComparer abstract class, and since each one overrides the abstract Compare(ListViewItem item1, ListViewItem item2) method, the explicit classes implementation of the compare method is used. As long as the appropriate explicit class is created and set to the comparer variable, sorting multiple columns of diverse data is possible. Not only that, it is possible to perform more complex sorting. For example, you can sort by line count first and if the two line counts are equal, you can start sorting by file name to ensure an accurately sorted file listing. This is exactly what the Line Counter add-in does, so check the full source code for details.

Refactoring: Adding Customizable Configuration

When this article was first posted, all of the configuration for this add-in was hard coded. The list of extensions that could be counted, which counting algorithms to use for different file types, etc. was all set up in the constructor of the UserControl. This does not lend itself to much flexibility, so the configuration has been refactored out and a configuration manager has been implemented. The actual configuration is stored in an XML configuration file and the following things are configurable: project types, file types, line counting parsers, and metrics parsers.

The configuration manager itself, ConfigManager, is a singleton object that will load the XML configuration when it is first created. The ConfigManager class provides several methods to map project and file types to their human-readable names and icons for display in list views. The ConfigManager also supplies a few methods to determine if a particular file type is allowed to have different counting and metrics parsing methods performed on it. The full set of methods available in the ConfigManager is as follows:

CountParserDelegate MapCountParser(string method) 
int MapProjectIconIndex(string projectTypeKey, ImageList imgList) 
string MapProjectName(string projectTypeKey) 
int MapFileTypeIconIndex(string fileTypeKey, ImageList imgList) 
bool IsFor(string extension, string what) 
bool AllowedFor(string extension, string method, string what) 
string AllowedMethod(string extension, string what, int index) 

After creating a configuration file, LineCounterAddin.config, and writing the ConfigManager singleton class, the next step is to update the LineCounterBrowser UserControl. The constructor can now be a lot simpler, so removing all of the current code and adding a line cache for the instance of the ConfigManager is all that is needed:

public LineCounterBrowser()
{
    InitializeComponent();
    m_cfgMgr = ConfigManager.Instance;
}

In addition to updating the LineCounterBrowser constructor, numerous changes must be made within the core code that groups and counts files. There are too many small changes to list them all here, so I am uploading a new archive for the current source code and keeping the original source code available as well. Running a diff tool will help you identify all of the areas that were refactored to use the ConfigManager.

Refactoring: Adding a File Volume Display

In addition to knowing how many lines your projects have, it's also nice to know how many of each type of file there are. A simple improvement to the line counter is adding a properties window for the projects and the solution listed in the bottom part of the LineCounter add-in. This dialog will calculate the total and overall percentage of each file type in your project or full solution. The code for this popup is in the ProjectDetails.cs file, if you wish to see how it was implemented.

Installing an Add-in

Running an add-in while creating it for testing purposes is very easy and straightforward, as the wizard that helped you create the add-in initially configured a "For Testing" version of the AddIn file. This makes it as easy as running the project and messing with the add-in in the copy of Visual Studio that appears. Any users of your add-in will not be so lucky, as they will most probably not have the source solution to play with. Creating a setup project for your add-in is just like creating one for any other project, but there are some tricks that can keep things simple.

Create a setup project for the LineCounterAddin called LineCounterSetup. Once the project is created, open the File System Editor and remove all of the folders except the Application Folder. Select the Application Folder and change the DefaultLocation property to '[PersonalFolder]\Visual Studio 2005\Addins'. This will cause the add-in to be installed in the user's AddIns folder by default. Since Visual Studio automatically scans that folder for AddIn files, it will make installation simple and convenient. Back in the File System Editor, right-click the Application Folder and add a new folder. Name it 'LineCounterAddin', as this will be where we install the actual DLL for our add-in, along with any additional files such as the satellite assembly with our image resources. Create another folder under LineCounterAddin called 'en-US'.

Now that we have configured the installation folders, we need to add the stuff we want to install. Right-click the setup project in the solution explorer and choose 'Project Output...' under the Add menu. Choose the Primary Output for the LineCounterAddin project. Now add several files -- choose 'File...' from the Add menu -- from the LineCounterAddin project, including:

  • For Installation\AddRemove.ico
  • For Installation\LineCounterAddin.AddIn
  • bin\en-US\LineCounterAddin.resources.dll

Once you have added all of the files to include, you will need to exclude several dependencies from the Detected Dependencies folder. The only thing we will need to keep is the Microsoft .NET Framework, as all the rest will be available on any system that has Visual Studio 2005 installed. To exclude a dependency, simply select it and change the Exclude property to true. NOTE: You can select multiple dependencies at once and change the Exclude property for all of them at once. The last step in configuring our setup project is to put all of the files in the right folders. Put the files in the following locations:

  • LineCounterAddin.AddIn -> Application Folder\
  • Primary output from LineCounterAddin -> Application Folder\LineCounterAddin\
  • AddRemove.ico -> Application Folder\LineCounterAddin\
  • LineCounterAddin.resources.dll -> Application Folder\LineCounterAddin\en-US\

Once all of the files are in their proper location, you can build the setup project to create the LineCounterSetup.msi and Setup.exe files for distribution. If you want to configure a custom icon to appear in the Add/Remove Programs control panel, select the LineCounterSetup project in the solution explorer and change the AddRemoveProgramsIcon property to use the AddRemove.ico file from the LineCounterAddin project. You should do this before you add any other files, as the AddRemove.ico file will be added to the setup project for you if you do. You will need to manually rebuild your setup project to update it after changing other projects in the solution, as it will not be included in normal builds. I am including a compiled Setup download at the top of this article for those who do not wish to download and compile the source. This will allow you to use the add-in for what it is, a line counter.

Final Words

Well, that's it for now. I hope this article will give those of you who read this some insight into writing add-ins for Visual Studio. If you read this far, I also hope that the examples of using delegates and template methods as a means of code simplification will be useful. This article is a work in progress and I hope to add more too it, particularly in regards to creating menu items and toolbar buttons for starting the add-in, etc. Please feel free to improve on my code. This was a 4 hour project, with a few hours spent writing this article and improving my original code's commenting and structure. It can be improved and enhanced, and I welcome suggestions, new features and the code for them!

Points of Interest

At the moment, this add-in does not have a menu item or toolbar button, so you have to start it manually. To do so, simply open the Add-in Manager from the tools menu, and check the Line Counter add-in. You should see the tool window appear. I recommend right-clicking its title bar and changing it to a tabbed document window. It is easier to use that way.

Using the Add-in

For those of you who have had trouble getting the add-in to work, I have uploaded a new copy of the source code in case something was wrong with the original. In addition, here are some things to double-check after you open and run the project, to make sure it is working right. First, the solution should look like the following figure. The LineCounterAddin project should be the default project and all of the references and files should look like so:

Screenshot - SolutionExplorer.gif

NOTE: The [ProjectOutputPath] should match the output path of the project on your own system, so you will probably have to edit it. A key file to note is the LineCounterAddin - For Testing.AddIn file. This is important for when VS tries to register the add-in. If it is missing, then the add-in will not register. This particular file is somewhat unique in that it is a shortcut. The actual location of this file should be in your {MyDocuments}\Visual Studio 2005\Addins\ folder and the file should contain the following XML:

<?xml version="1.0" encoding="UTF-16" standalone="no"?>
<Extensibility xmlns="http://schemas.microsoft.com/AutomationExtensibility">
    <HostApplication>
        <Name>Microsoft Visual Studio Macros</Name>
        <Version>8.0</Version>
    </HostApplication>
    <HostApplication>
        <Name>Microsoft Visual Studio</Name>
        <Version>8.0</Version>
    </HostApplication>
    <Addin>
        <FriendlyName>Line Counter 2005</FriendlyName>
        <Description>Line Counter for Visual Studio 2005</Description>
        <Assembly>[ProjectOutputPath]\LineCounterAddin.dll</Assembly>
        <FullClassName>LineCounterAddin.Connect</FullClassName>
        <LoadBehavior>0</LoadBehavior>
        <CommandPreload>1</CommandPreload>
        <CommandLineSafe>0</CommandLineSafe>
    </Addin>
</Extensibility>

If you need to add this file to the project, place it in the proper location under your My Documents folder first. When you add the file to the LineCounterAddin project, instead of clicking the Add button, use the down arrow next to it and choose "Add As Link."

Screenshot - AddAsLink.gif

After you have checked that the project is valid, do a full rebuild. This will create the DLL file for the add-in. Go to the Tools menu and find the Add-in Manager menu option. See the screenshot below.

Screenshot - AddInManagerMenu.gif

Finally, when the add-in manager is open, check the FIRST checkbox for the Line Counter 2005 add-in, as you can see in this final screenshot:

Screenshot - AddInManager.gif

Oz Solomon gets most of the credit for the line counting algorithms I used in this tool. While I was browsing his source code for PLC, I came across his counting algorithms. They were quite efficient and simple, so I used the same code for the C-style and VB-style algorithms. I used the same style to count XML files.

History

  • 05/12/2009: Updated Line Counter 2008 download file
  • 05/10/2009: Added Line Counter 2008 (precompiled) download file 
  • 05/31/2007: Edited
    • Article edited and moved to the main CodeProject.com article base
  • 06/11/2006: Version 1.1
    • Added XML configuration
    • Started adding some simple code metrics features. Incomplete.
  • 05/07/2006: Version 1.0
    • Added a functional menu item and tool bar button
    • Added information to the article on using custom icons for commands you add to Visual Studio's interface
    • Added information on creating a setup project for add-ins, and included a setup project in the source
    • Added a setup download for those who only wish to install and use the Line Counter add-in
    • Should be pretty stable, and can be used for large projects with hundreds of thousands to millions of lines
  • 04/28/2006: Version 0.9
    • Initial add-in version, a little messy but it works
    • Might have some bugs that cause a fatal crash of Visual Studio, so use at your own risk!

Future Plans

This project is far from over and I have plans to improve this tool, as well as add new feature to it as I find the time. I am also open to reviewing improvements from the community, and those that are well-coded and useful I'll see about adding. Here are some things I hope to add:

  • Add an XML configuration file for defining all of the mappings, instead of hard-coding them in the constructor.
  • Allow custom summaries by letting users multi-select files in the file listing, and have the summary of those files displayed in the project summary list.
  • Add some XML/XSLT reporting capabilities, so that line count reports can be saved off at regular intervals (to show code volume and/or development progression).
  • Possibly add some simple code complexity or code metrics features. Stuff I've never done before, and not sure if it would fit. If anyone in the community knows how to determine code complexity or metrics, feel free to poke away and send me the code.
  • Add a visual configuration tool that one can use to define countable files and their counting algorithms. Possibly add the ability to use .NET 2.0 anonymous delegates (which are essentially closures) to "script" in additional counting algorithms for additional file types.

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