Fork me on GitHub

Line Counter - Writing a Visual Studio 2005 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 guage 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 extensability. 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 extensability objects that Visual Studio provides. These features are most commonly referred to as DTE, or the design time environment. This object that exposes all 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 extensability 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.

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 to cause your add-in to run when Visual Studio starts up, and you can add a tool bar button for your add-in, which will appear when it starts up (whether that be manually, or automatically when VS starts).

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 (something which I myself havn't fully mapped out yet, but sometimes things need to be done a certain way). Currently, this method should be prepopulated 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. A couple important things to note, however, are 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, once 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).

Th 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 toolwindow add-in, create a new add-in project now with the following settings:

Project Name: LineCounterAddin
Language: C#
Name: Line Counter
Descriotion: 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

And 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 souce 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>
/// Recieves 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 corrosponding 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, and its well commented, so further explanation should not be neccesary.

Collapse
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 
                                                        // occurr.
		                                    
		    // 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 the method will be 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 CommandBar's 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 its pretty self explanitory. One thing to note about these functions is 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 custon icons later.

Collapse
/// <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 neccesary 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 addin when its menu item or tool bar button is clicked.

Collapse
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 eneabled
				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 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 neccesary to create an add-in is relatively simple and strait forward. Lets 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 your 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 strait forward.

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.

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', and 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 set 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, and 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.

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 satelite 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, its time to move on to some of the jucier code. The bulk of the add-in is just written like any old windows forms app, 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, like 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.

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

Collapse
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>
		/// Recieves 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 full source for the details).

First off, lets 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 a delegate 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 on 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:

Collapse
#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 (or in the event that a KeyNotFoundException, we just use the '*' key to get the default generic parser). No ugly, unmanageable if/else monstrosities or endless switch statements, and we have also made it possible to add additional parsing routeens in the future without much effort (more on this 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 details 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 multiple different kinds of source files used the generic dictionady and delegate from above, and 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, and the answers are usually seldom and far between. As I beleive this Line Counter add-in will be a very useful utility for many people, I'm hoping 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, lets 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 deferrs 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 as simple event handler for the ListView.ColumnClick event:

Collapse
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, sorting by line count first, and if the two line counts are equal, 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).

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 flexability, 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 whne 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 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 the areas that were refactord to use the ConfigManager.

Refactoring: Adding a File Volume Display

In addition to knowing how many lines your projects have, its also nice to know how many of each type of file there is, too. 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 addin. 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 addin while creating it for testing purposes is very easy and strait forward, 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 filders 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 users AddIns folder by default, and 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 addin (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 dependancies from the Detected Dependancies 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 dependancy, simply select it, and change the Exclude property to true. (NOTE: You can select multiple dependancies 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 the proper location, you can build the setup project to create LineCounterSetup.msi file and a Setup.exe 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 addin for what it is, a line counter.

Final Words

Well, thats 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 codes commenting and structure. It can be improved and enhanced, and I welcome suggestions, new features (and the code for them), etc.

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 titlebar, 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 am uploading a new copy of the source code today, 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 the references and files should look like so:

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. It should contain the following xml (NOTE: The [ProjectOutputPath] should match the output path of the project on your own system, so you will probably have to edit it):

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

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

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:


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 accross 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/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 featurs 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 multiselect 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.

About Jon Rista


Jon Rista has been programming since the age of 8 (first Pascal program), and has been a programmer since the age of 10 (first practical program). In the last 17 years, he has learned to love C++, embrace object orientation, and finally enjoy the freedom of C#. He knows over 10 languages, and vows that his most important skill in programming is creativity, even more so than logic. Jon works on large-scale enterprise systems design and implementation, and employs Design Patterns, C#, .NET, and SQL Server in his daily doings. Visio is his favorite tool.

Click here to view Jon Rista's online profile.


http://www.codeproject.com/useritems/LineCounterAddin.asp
posted @ 2006-09-05 22:05  张善友  阅读(3200)  评论(0编辑  收藏  举报