行计数器 - 编写 Visual Studio 2005 & 2008 插件
提供一个完整的解决方案和项目行计数及摘要工具,作为 Visual Studio 2005/2008 托管插件编写。

背景
我一直是来自 WndTabs.com 的 PLC (项目行计数器) 的粉丝。这个小工具帮助我跟踪并甚至评估开发项目的进展已经好几年了。我一直在耐心等待 PLC 的作者 Oz Solomon 发布 Visual Studio 2005 的更新。我终于在今天找到了一些空闲时间,决定看看我是否可以自己更新它。我很快意识到,我编写自己的行计数器插件所需的时间可能比我弄清楚 Oz 的代码并将其现有代码迁移到 VS 2005 版本所需的时间还要少。所以,我在这里,向所有优秀的编码员介绍我的第一个 VS 插件。我希望您觉得这篇文章和它背后的产品都很有用。我欢迎评论、改进和建议,因为我将继续随着时间的推移改进这个小工具。
Visual Studio 自动化和扩展
Visual Studio 最伟大的事情之一就是它的可扩展性。你们中的许多人可能已经对本文中将要介绍的一些功能有所了解。如果您以前为任何版本的 Visual Studio 编写过插件,甚至如果您编写过任何宏来帮助简化您的工作流程,那么您已经使用了 Visual Studio 提供的自动化和可扩展性对象。这些功能通常被称为 DTE,即设计时环境。这个对象向聪明的程序员公开了 Visual Studio UI 和工具的所有不同部分。
使用 DTE 对象,您可以编程控制 Visual Studio 中的几乎所有内容,从工具栏、停靠工具窗口,甚至编辑文件或启动编译。DTE 对象最简单的用途之一是通过宏。使用宏,您可以做很多事情,从简单的任务(如查找和替换)到复杂的任务(如为除特定类型之外的所有变量创建带注释的属性)。通过宏公开的 DTE 对象也通过插件可扩展性项目公开。使用插件向导创建 Visual Studio 插件,您可以创建可以称之为非常高级的宏的基本外壳。

Visual Studio 插件可以用任何语言编写,您可以在运行插件向导时选择。该向导还将为您提供其他几个选项。本文的此版本暂不涵盖这些其他选项的具体细节。总而言之,您可以选择让您的插件在 Visual Studio 启动时运行。您还可以为您的插件添加一个工具栏按钮,该按钮将在 VS 启动时出现,无论是手动还是自动。

创建插件
完成插件向导后,您将获得一个新项目,其中包含一个重要的文件:Connect.cs。这个小文件是任何 Visual Studio 插件的起点。它实现了几个关键接口,并在几个关键方法中提供了一些起始代码。目前最重要的方是
OnConnection(object application, ext_ConnectMode connectMode,
object addInInst, ref Array custom)
当 Visual Studio 启动您的插件时,此方法是它调用的第一个方法。任何初始化代码都需要放在这里。从技术上讲,您可以在这里做任何您需要做的事情,只要它在 Visual Studio 自动化模型施加的范围内工作。这是我自己尚未完全摸清的东西,但有时事情需要以某种方式完成。目前,此方法应该预填充插件向导创建的代码,这些代码开始实现您选择的任何选项(例如添加“工具”菜单项)。OnConnection
中的大部分代码都经过了充分的注释,因此我们不会详细解释所有这些代码。然而,需要注意的一件重要事情是前三行
_applicationObject = (DTE2)application;
_addInInstance = (AddIn)addInInst;
if(connectMode == ext_ConnectMode.ext_cm_UISetup)
{
// ...
}
第一行缓存 DTE 对象,该对象由 Visual Studio 在启动插件时提供。第二行缓存插件本身的实例,这通常是您可能从插件代码中进行的许多调用所必需的。第三行,即 if
语句,允许在插件启动时进行条件处理。Visual Studio 通常会启动插件几次。第一次允许它使用菜单项、工具栏按钮等设置自己的 UI。当插件实际运行时,会发生额外的启动,这可以通过两种不同的方式发生:VS 启动时自动启动,或 VS 启动后通过其他进程启动。
OnConnection
方法中已有的其余代码都已注释,并且会根据您在向导中选择的选项而有所不同。对于 Line Counter 插件,我们将删除所有生成的代码并用我们自己的代码替换。如果您希望在我解释如何创建工具窗口插件时跟着本文一起操作,请立即使用以下设置创建一个新的插件项目
项目名称:LineCounterAddin
语言:C#
名称:行计数器
描述:Line Counter 2005 - 源代码行计数器
其他选项:保留默认值
项目创建后,添加以下引用
System.Drawing
System.Windows.Forms
最后,添加一个名为 LineCounterBrowser
的新用户控件。这个用户控件将是我们的插件的主要界面,它的工作方式与任何普通的 Windows 窗体一样。您可以使用可视化设计器进行设计、添加事件处理程序等。由于您可以在本页顶部下载完整的源代码,因此本文不会详细介绍如何构建用户控件。现在,只需打开新用户控件的源代码并添加此代码
#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
目前,用户控件源代码中不需要其他任何内容。此属性和相应的变量提供了一种将 DTE 对象引用从 Connect
类传递到我们的 UI 类的方法。我们实际上将在 Connect
类的 OnConnection
方法中设置该属性。OnConnection
的完整代码应如下所示。它注释得很清楚,因此不需要进一步解释。
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;
OnConnection
方法将在 Visual Studio 执行期间的不同时间点运行多次。我们关注该方法被调用的两种可能原因:一次用于 UI 设置,一次用于启动。当 OnConnection
方法被调用用于 UI 设置时,我们将希望使用我们插件的菜单项和工具栏按钮来更新 Visual Studio 的用户界面。这在 OnConnection
方法的第二个 if
语句中完成。当 OnConnection
方法被调用用于启动时——它有两种不同的方法:当 VS 启动时和 VS 启动后——我们希望显示我们的插件。
在执行 UI 设置时,我创建了几个 private
辅助函数来简化该过程。下面,您可以找到许多方法,它们将促进在 Visual Studio 中创建新的 CommandBar
,以及向这些栏添加命令。这些功能包括向菜单添加新的菜单项。代码注释得很清楚,因此不言自明。关于这些函数,需要注意的一点是,它们假定您的插件项目有一个自定义 UI 程序集,其中包含您希望用于命令(菜单项和工具栏按钮)的所有图像。我稍后将解释如何添加自定义图标。
/// <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
}
}
现在我们有了将插件正确集成到 Visual Studio 用户界面并在请求时显示插件的代码,我们需要添加命令处理程序。在 Visual Studio 插件中处理命令是一项相当简单的任务。我们的 Connect
类实现的 IDTCommandTarget
接口提供了正确处理 Visual Studio 命令所需的方法。您需要按如下方式更新 QueryStatus
和 Exec
方法,以便在单击行计数器插件的菜单项或工具栏按钮时显示它。
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;
}
}
}
完成 OnConnection
方法后,您的插件将作为浮动工具窗口创建。完整的用户控件将允许您计算解决方案中每个项目的行数和总数,以及整个解决方案中所有行的摘要。您可以在本文顶部下载源代码,编译它,并通过调试器启动项目以进行测试,并检查插件启动时的控制流。如您所见,创建插件所需的代码量相对简单明了。让我们继续讨论行计数器本身(本质上是用户控件)是如何编写的一些细节。
为命令使用自定义图标
当您创建提供菜单项或工具栏按钮的 Visual Studio 插件时,Visual Studio 将默认使用 Microsoft Office 默认图标。特别是,将使用的图标是一个黄色笑脸(确切地说是图标索引 #59)。通常,MSO 库中可用的图标不会是您想要的。为您的命令创建和使用自定义图标并不特别困难,但这样做的文档隐藏得很深,而且不太直观。
为命令添加自定义图标的第一步是向插件项目添加一个新的资源文件。在解决方案资源管理器中右键单击 LineCounterAddin
项目,指向“添加”,然后从菜单中选择“新建项...”。添加一个名为 ResourceUI.resx 的新资源文件。添加资源文件后,在解决方案资源管理器中选择它,并将“生成操作”属性更改为“无”。稍后我们将使用生成后事件对该文件进行自己的处理。

现在我们有了一个新的资源文件,我们需要向其中添加一个图像。如果它尚未打开,请打开资源文件并单击“添加资源”旁边的向下箭头。从“新建图像”菜单中选择“位图...”。当提示命名图像时,只需将其命名为 1。Visual Studio 将使用的所有图像资源都通过其索引引用,资源 ID 应与该索引相同。对于此插件,我们只需要一个图像。添加图像后,将其打开并将其大小更改为 16x16 像素,颜色深度更改为 16 色。Visual Studio 只会显示颜色深度为 4 或 24 的图像,并且它将使用石灰色(RGB 为 0, 254, 0)作为 16 色图像的透明蒙版。您可以在页面顶部下载的 LineCounterAddin
项目的 Resources 文件夹中的 1.bmp 图像包含此插件的简单图标。
创建新的资源文件并添加图像后,您需要将其设置为正确构建。此特定的资源文件必须编译为附属程序集。我们可以通过构建后事件来实现此目的。要编辑构建事件,请在解决方案资源管理器中右键单击 LineCounterAddin
项目并选择属性。一个新的工具将在文档区域中打开,其中包含用于编辑项目属性的选项卡式界面。找到构建事件选项卡,如下图所示。

在“生成后事件命令行”区域中,添加以下脚本
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
注意:请确保将第一行“f:”更改为表示项目所在的驱动器。这很重要,否则 Resgen 命令将无法找到 ResourceUI.resx 文件引用的文件。另请注意,您需要安装 .NET 2.0 SDK,否则 Resgen 命令将不可用。该脚本通常应该仍然可以工作,因为它基于宏而不是固定路径。一旦您设置了构建后脚本,您的插件的附属程序集将在每次构建项目或解决方案时编译,并放置在您的构建输出文件夹的 en-US 子目录中。当您运行项目时,Visual Studio 将引用此附属程序集以查找任何命令栏图像。
计算行数
现在您已经了解了如何创建显示新工具窗口的插件,是时候转向一些更有趣的代码了。插件的主体像任何旧的 Windows 窗体应用程序一样编写,具有用户界面、事件处理程序和辅助函数。此应用程序的要求相当简单,一些基本设计模式将帮助我们满足这些要求
- 主要目标:显示加载解决方案中每个项目的行数信息。
- 显示解决方案的总行数,以及每个项目的总行数。
- 显示每个项目中每个可计数文件的计数信息。
- 计算代码行、注释行、空行,并显示总代码/注释行和净代码/注释行。
- 准确计算不同类型源文件(如 C++/C#、VB、XML 等)的行数。
- 允许按名称、行数、文件扩展名对文件列表进行排序。
- 允许按文件类型、项目对文件列表进行分组,或者完全不分组。
- 在重新计算期间显示处理进度。
让我们首先为用户控件提供一个干净、结构化的源文件。您的用户控件源文件应具有以下结构
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
}
从上述初步代码中,您可以推断出一些将用于精确计数多种类型源代码文件的方法,以及我们将如何以各种方式对文件列表进行排序。底部的支持结构通过封装数据来简化代码。有关详细信息,请参阅完整源代码。
首先,让我们探讨一下我们如何无缝地使用多种计数算法,而无需丑陋的 if
/else
或 switch
语法。现代语言的一个很棒的特性是函数指针,在 .NET 中由委托提供。大多数时候,我认为委托在当今的 .NET 应用程序中被严重忽视了。因此,我将提供一个简单但优雅的示例,说明它们如何使聪明的开发人员的生活更轻松。概念很简单:创建文件扩展名与行计数函数委托之间的映射列表。使用 .NET 2.0 和泛型,我们可以非常高效地完成此操作。按如下方式更新源代码中的以下位置
#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
现在我们已经指定了映射,我们需要创建实际将被调用的函数。这些函数非常简单,只需要匹配前面委托声明 delegate void CountLines(LineCountInfo info)
提供的签名。在类的“行计数方法”区域中,创建四个 private
方法
private void CountLinesGeneric(LineCountInfo info)
private void CountLinesCStyle(LineCountInfo info)
private void CountLinesVBStyle(LineCountInfo info)
private void CountLinesXMLStyle(LineCountInfo info)
所有这四个函数都匹配 CountLines
委托签名,并使用我们添加到默认构造函数中的代码映射到相应的文件扩展名。现在,只需将正确的键传递给 m_countAlgorithms
并调用返回的委托即可。如果出现 KeyNotFoundException
,我们只需使用“*”键获取默认的通用解析器。没有丑陋、难以管理的 if
/else
怪物或无休止的 switch
语句。我们还可以在将来轻松添加额外的解析例程。稍后将更详细地讨论这一点。
大部分行计数和求和代码都放在其余的辅助函数中。计数有两个部分:扫描解决方案中的项目和文件,以及实际的求和。方法列在下面。目前,我不会详细介绍所有这些代码是如何工作的。我将在后续更新或补充文章中介绍细节。使用通用字典和上面讨论的委托来计数许多不同类型的源文件的主要技巧是本文最重要的方面。
排序,排序,排序
本文我想介绍的最后一个概念是文件列表的排序。我经常看到 .NET 开发人员询问如何对 ListView
中的项目进行排序。答案通常很少见且相隔甚远。我相信这个行计数器插件对许多人来说是一个非常有用的实用程序,我希望我对 ListView
排序的解释能在这里得到广泛的传播。归根结底,这个概念实际上非常简单。使用模板方法模式可以非常容易地以不同的方式对不同数据的多列进行排序。首先,让我们在用户控件的“嵌套类”区域添加一个 abstract
类
#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
}
这个类是我们“模板方法”的抽象主页。模板方法模式只是在 abstract
类上提供一个通用的骨架方法,该方法将部分或全部实际算法代码委托给子类。这将通过允许我们在排序时使用单个类型和单个方法来简化我们的排序,但对于 ListView
的每一列使用不同的排序算法。为了实现这一点,我们必须为每种要排序的列类型实现更多的嵌套类。要查看这些类的详细信息,请参阅完整的源代码。一旦我们定义了显式排序算法,我们还需要为 ListView.ColumnClick
事件实现一个简单的事件处理程序
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();
}
虽然这段代码可能不明显,但是 ListViewItemComparer
抽象基类的“模板方法”(它也恰好是 IComparer.Compare(object, object)
接口的实现)在 ListView.Sort()
方法比较每个列表视图项时被调用。由于我们的每个显式比较器类都派生自 ListViewItemComparer
抽象类,并且每个类都重写了抽象的 Compare(ListViewItem item1, ListViewItem item2)
方法,因此使用了显式类比较方法的实现。只要创建适当的显式类并将其设置为 comparer
变量,就可以对不同数据的多列进行排序。不仅如此,还可以执行更复杂的排序。例如,您可以先按行数排序,如果两个行数相等,则可以开始按文件名排序,以确保文件列表准确排序。这正是 Line Counter 插件所做的,所以请查看完整的源代码了解详细信息。
重构:添加可自定义配置
本文首次发布时,此插件的所有配置都是硬编码的。可以计数的扩展名列表、用于不同文件类型的计数算法等都是在 UserControl
的构造函数中设置的。这不具有太大的灵活性,因此配置已被重构出来,并已实现配置管理器。实际配置存储在 XML 配置文件中,以下内容是可配置的:项目类型、文件类型、行计数解析器和指标解析器。
配置管理器本身,ConfigManager
,是一个单例对象,它在首次创建时加载 XML 配置。ConfigManager
类提供了几种方法来将项目和文件类型映射到其人类可读的名称和图标,以便在列表视图中显示。ConfigManager
还提供了一些方法来确定特定文件类型是否允许对其执行不同的计数和指标解析方法。ConfigManager
中可用的完整方法集如下
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)
创建配置文件 LineCounterAddin.config 并编写 ConfigManager
单例类后,下一步是更新 LineCounterBrowser UserControl
。现在构造函数可以简单得多,所以只需删除所有当前代码并为 ConfigManager
实例添加一个行缓存即可
public LineCounterBrowser()
{
InitializeComponent();
m_cfgMgr = ConfigManager.Instance;
}
除了更新 LineCounterBrowser
构造函数之外,还必须在分组和计数文件的核心代码中进行许多更改。这里列出所有这些小更改太多了,所以我正在上传当前源代码的新存档,并保留原始源代码。运行差异工具将帮助您识别所有重构以使用 ConfigManager
的区域。
重构:添加文件体积显示
除了知道项目有多少行之外,知道每种文件类型有多少文件也很好。对行计数器的一个简单改进是为 LineCounter
插件底部列出的项目和解决方案添加一个属性窗口。此对话框将计算项目中或整个解决方案中每种文件类型的总数和总百分比。如果您想查看它是如何实现的,此弹出窗口的代码在 ProjectDetails.cs 文件中。
安装插件
在创建插件时运行它进行测试非常容易和直接,因为帮助您最初创建插件的向导配置了插件文件的“用于测试”版本。这使得它像运行项目并在出现的 Visual Studio 副本中操作插件一样简单。您的插件的任何用户都不会那么幸运,因为他们很可能没有源代码解决方案可以玩。为您的插件创建安装项目就像为任何其他项目创建安装项目一样,但有一些技巧可以使事情保持简单。
为 LineCounterAddin
创建一个名为 LineCounterSetup
的安装项目。项目创建后,打开文件系统编辑器并删除除应用程序文件夹外的所有文件夹。选择应用程序文件夹并将 DefaultLocation
属性更改为“[PersonalFolder]\Visual Studio 2005\Addins”。这将导致插件默认安装在用户的 AddIns 文件夹中。由于 Visual Studio 会自动扫描该文件夹以查找 AddIn 文件,因此安装将变得简单方便。回到文件系统编辑器,右键单击应用程序文件夹并添加一个新文件夹。将其命名为“LineCounterAddin
”,因为这将是我们安装插件实际 DLL 的位置,以及任何其他文件,例如带有图像资源的附属程序集。在 LineCounterAddin
下创建另一个名为“en-US”的文件夹。
现在我们已经配置了安装文件夹,我们需要添加要安装的内容。在解决方案资源管理器中右键单击安装项目,然后从“添加”菜单中选择“项目输出...”。选择 LineCounterAddin
项目的主输出。现在从 LineCounterAddin
项目中添加几个文件——从“添加”菜单中选择“文件...”——包括
- 适用于 Installation\AddRemove.ico
- 适用于 Installation\LineCounterAddin.AddIn
- bin\en-US\LineCounterAddin.resources.dll
添加所有要包含的文件后,您需要从“检测到的依赖项”文件夹中排除几个依赖项。我们唯一需要保留的是 Microsoft .NET Framework,因为所有其余部分都将在安装了 Visual Studio 2005 的任何系统上可用。要排除依赖项,只需选择它并将 Exclude
属性更改为 true
。注意:您可以一次选择多个依赖项,并一次更改所有它们的 Exclude
属性。配置安装项目的最后一步是将所有文件放在正确的文件夹中。将文件放在以下位置
- LineCounterAddin.AddIn -> Application Folder\
LineCounterAddin
的主要输出 -> Application Folder\LineCounterAddin\- AddRemove.ico -> Application Folder\LineCounterAddin\
- LineCounterAddin.resources.dll -> Application Folder\LineCounterAddin\en-US\
所有文件都在其正确位置后,您可以构建安装项目以创建 LineCounterSetup.msi 和 Setup.exe 文件以供分发。如果您想配置一个自定义图标以显示在“添加/删除程序”控制面板中,请在解决方案资源管理器中选择 LineCounterSetup
项目,并将 AddRemoveProgramsIcon
属性更改为使用 LineCounterAddin
项目中的 AddRemove.ico 文件。您应该在添加任何其他文件之前执行此操作,因为如果您这样做,AddRemove.ico 文件将添加到安装项目中。您将需要在更改解决方案中的其他项目后手动重建安装项目以进行更新,因为它不会包含在正常构建中。我在本文顶部包含了一个已编译的安装下载,供那些不希望下载和编译源代码的人使用。这将允许您使用插件,因为它是一个行计数器。
结束语
好了,目前就这些了。我希望本文能让阅读本文的各位对编写 Visual Studio 插件有所了解。如果您读到这里,我也希望使用委托和模板方法作为简化代码的示例会很有用。本文是一个正在进行中的工作,我希望添加更多内容,特别是在为启动插件等创建菜单项和工具栏按钮方面。请随时改进我的代码。这是一个 4 小时的项目,花了几个小时撰写本文并改进了我的原始代码的注释和结构。它可以改进和增强,我欢迎建议、新功能以及它们的代码!
关注点
目前,此插件没有菜单项或工具栏按钮,因此您必须手动启动它。为此,只需从工具菜单打开插件管理器,并勾选行计数器插件。您应该会看到工具窗口出现。我建议右键单击其标题栏并将其更改为选项卡式文档窗口。这样使用起来更方便。
使用加载项
对于那些在使插件工作方面遇到困难的人,我已上传了源代码的新副本,以防原始文件出现问题。此外,这里有一些在打开和运行项目后需要仔细检查的事项,以确保它正常工作。首先,解决方案应如下图所示。LineCounterAddin
项目应为默认项目,所有引用和文件应如下所示
注意:[ProjectOutputPath] 应该与您系统上项目的输出路径匹配,因此您可能需要编辑它。需要注意的一个关键文件是 LineCounterAddin
- 用于 Testing.AddIn 文件。这对于 VS 尝试注册插件很重要。如果它丢失,则插件将无法注册。这个特定的文件有些独特,因为它是一个快捷方式。此文件的实际位置应在您的 {MyDocuments}\Visual Studio 2005\Addins\ 文件夹中,并且该文件应包含以下 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>
如果您需要将此文件添加到项目中,请先将其放置在您的“我的文档”文件夹下的正确位置。当您将文件添加到 LineCounterAddin
项目时,不要单击“添加”按钮,而是使用其旁边的向下箭头,然后选择“添加为链接”。

检查项目有效后,进行完全重建。这将为插件创建 DLL 文件。转到“工具”菜单并找到“插件管理器”菜单选项。请参见下面的屏幕截图。

最后,当插件管理器打开时,勾选 Line Counter 2005 插件的第一个复选框,如最后一张截图所示

Oz Solomon 在我使用的行计数算法中获得了大部分功劳。当我在浏览他的 PLC 源代码时,我偶然发现了他的计数算法。它们非常高效和简单,所以我将相同的代码用于 C 风格和 VB 风格的算法。我使用相同的风格来计算 XML 文件。
历史
- 2009年5月12日:更新了 Line Counter 2008 下载文件
- 2009年5月10日:添加了 Line Counter 2008(预编译)下载文件
- 2007年5月31日:编辑
- 文章已编辑并移至 CodeProject.com 主文章库
- 2006年6月11日:版本 1.1
- 添加了 XML 配置
- 开始添加一些简单的代码度量功能。未完成。
- 2006年5月7日:版本 1.0
- 添加了功能性菜单项和工具栏按钮
- 文章中添加了关于为添加到 Visual Studio 界面的命令使用自定义图标的信息
- 添加了关于为插件创建安装项目的信息,并在源代码中包含了安装项目
- 为那些只想安装和使用 Line Counter 插件的人提供了安装下载
- 应该非常稳定,可用于数十万到数百万行代码的大型项目
- 2006年4月28日:版本 0.9
- 初始插件版本,有点混乱但能用
- 可能存在一些导致 Visual Studio 严重崩溃的错误,请自行承担风险!
未来计划
这个项目远未结束,我计划改进这个工具,并在有时间的时候添加新功能。我也乐于审查社区的改进,对于那些编码良好且有用的,我将考虑添加。以下是我希望添加的一些内容
- 添加一个 XML 配置文件来定义所有映射,而不是在构造函数中硬编码它们。
- 允许自定义摘要,让用户在文件列表中多选文件,并将这些文件的摘要显示在项目摘要列表中。
- 添加一些 XML/XSLT 报告功能,以便可以定期保存行数报告(以显示代码量和/或开发进度)。
- 可能会添加一些简单的代码复杂性或代码度量功能。这是我以前从未做过的事情,不确定它是否合适。如果社区中有人知道如何确定代码复杂性或度量,请随意尝试并发送代码给我。
- 添加一个可视化配置工具,可以使用它来定义可计数文件及其计数算法。可能添加使用 .NET 2.0 匿名委托(本质上是闭包)来“脚本”额外计数算法以用于额外文件类型的功能。