|
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.
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.
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.
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 theOnConnection
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 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.
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.
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.
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.
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:
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 uglyif
/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 fourprivate
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 tom_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.
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 theListView.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.
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 theUserControl
. 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
.
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.
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 underLineCounterAddin
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 theLineCounterAddin
project, including:
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
-> Application Folder\LineCounterAddin\ Once all of the files are in their proper location, you can build the setup project to create the LineCounterSetup.msiand 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 theAddRemoveProgramsIcon
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.
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!
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.
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. TheLineCounterAddin
project should be the default project and all of the references and files should look like so:
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."
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 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.
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:
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)
|
Jon Rista
Architect
United States
![]()
Member
|