本教程将深入探讨您提供的 VisualStudio 静态类代码,并详细解释 Visual Studio 自动化的关键概念和技术,帮助您掌握使用 C# 代码控制 Visual Studio 的方法。

1. Visual Studio 自动化 (DTE) 简介

Visual Studio 自动化 (DTE - Development Tools Environment) 提供了一套强大的接口,允许您通过编程方式访问和控制 Visual Studio IDE 的几乎所有功能。您可以利用 DTE 来:

  • 自动化重复性任务: 例如,批量创建项目、添加文件、编译代码、运行测试、部署应用程序等。

  • 扩展 Visual Studio 功能: 开发自定义的 Visual Studio 扩展,增强 IDE 的功能。

  • 与其他工具集成: 将 Visual Studio 集成到更大的自动化工作流程中。

DTE 采用 COM (Component Object Model) 技术,这意味着您可以从各种编程语言(包括 C#、VB.NET、Python 等)访问它。

2. COM 互操作和 ProgID

由于 DTE 是一个 COM 组件,我们需要使用 COM 互操作 (COM Interop) 技术来从 .NET 代码中访问它。 COM 互操作允许 .NET 应用程序与 COM 组件进行通信。

ProgID (Programmatic Identifier) 是 COM 组件的字符串标识符,用于在系统中唯一标识一个 COM 组件。 在代码中,_progID = "VisualStudio.DTE" 这行代码定义了一个字符串常量 _progID,其值为 "VisualStudio.DTE""VisualStudio.DTE" 正是 Visual Studio 自动化对象的 ProgID。 通过这个 ProgID,我们可以告诉 .NET 运行时我们要连接或创建哪个 COM 组件。

3. EnvDTE80.DTE2 接口

EnvDTE80.DTE2 是 Visual Studio 自动化对象模型中最核心的接口。 DTE2 接口代表了 Visual Studio IDE 的 顶级对象,通过它,您可以访问 Visual Studio 的各种子系统和功能,例如:

  • Solution: 访问和操作当前加载的解决方案。

  • Projects: 访问解决方案中的项目集合。

  • Documents: 访问当前打开的文档集合。

  • Commands: 执行 Visual Studio 命令 (例如,构建、运行、调试等)。

  • ItemOperations: 执行项目和项(文件、文件夹等)的操作。

  • MainWindow: 访问 Visual Studio 的主窗口。

在代码中,private static EnvDTE80.DTE2 _vsInstance = null; 声明了一个静态私有变量 _vsInstance,其类型为 EnvDTE80.DTE2。 这个变量将用来存储获取到的 Visual Studio 实例对象。

4. GetRunningObjectTableCreateBindCtx WinAPI 函数

代码中使用了两个 Windows API 函数:GetRunningObjectTableCreateBindCtx,它们通过 DllImport 特性从 ole32.dll 导入。 这两个函数是 COM 技术中用于 查找正在运行的 COM 对象 的关键 API。

  • GetRunningObjectTable(uint reserved, out IRunningObjectTable pprot):

    • 作用: 获取 Running Object Table (ROT) 的接口。ROT 是一个系统级别的表,记录了当前正在运行的 COM 对象。

    • IRunningObjectTable pprot: out 参数,用于接收获取到的 IRunningObjectTable 接口指针。

    • 返回值: int 类型,表示操作结果的 HRESULT 代码。小于 0 表示失败。

  • CreateBindCtx(uint reserved, out IBindCtx ppbc):

    • 作用: 创建一个 Bind Context (绑定上下文) 对象。Bind Context 在 COM 对象绑定过程中提供上下文信息,例如 moniker 解析和安全设置。

    • IBindCtx ppbc: out 参数,用于接收创建的 IBindCtx 接口指针。

    • 返回值: int 类型,表示操作结果的 HRESULT 代码。小于 0 表示失败。

5. IRunningObjectTable, IEnumMoniker, IBindCtx, IMoniker 接口

这些是在 COM 互操作中常用的接口,代码中使用它们来枚举和访问正在运行的 Visual Studio 实例。

  • IRunningObjectTable: 表示 Running Object Table,用于注册、撤销注册和枚举正在运行的 COM 对象。

  • IEnumMoniker: 用于枚举 Moniker 的接口。Moniker 是 COM 中用于持久化命名的对象,可以理解为 COM 对象的 "名字" 或 "地址"。 IRunningObjectTable.EnumRunning() 方法返回一个 IEnumMoniker 接口,用于枚举 ROT 中的 Moniker。

  • IBindCtx: 表示 Bind Context,在 COM 对象绑定过程中提供上下文信息。

  • IMoniker: 表示 Moniker 接口,用于唯一标识一个 COM 对象。 IEnumMoniker.Next() 方法返回一个 IMoniker 数组,每个 IMoniker 代表 ROT 中的一个 COM 对象。 IMoniker.GetDisplayName() 方法可以获取 Moniker 的显示名称,通常包含 ProgID。 IRunningObjectTable.GetObject() 方法可以使用 Moniker 获取实际的 COM 对象。

OpenVisualStudio 方法中,代码使用这些接口的步骤如下:

  1. GetRunningObjectTable: 获取 ROT 接口 (rot)。

  2. rot.EnumRunning: 获取枚举 ROT 中 Moniker 的接口 (monikerTable)。

  3. CreateBindCtx: 创建 Bind Context 接口 (bindCtx)。

  4. 循环枚举 Moniker (monikerTable.Next):

    • currentMoniker[0]?.GetDisplayName(bindCtx, null, out name): 获取当前 Moniker 的显示名称 (name)。

    • name.Contains(_progID): 判断显示名称是否包含 Visual Studio 的 ProgID (_progID = "VisualStudio.DTE"),以确定是否是 Visual Studio 实例。

    • rot.GetObject(currentMoniker[0], out object obj): 如果是 Visual Studio 实例,则使用 Moniker 获取实际的 COM 对象 (obj)。

    • EnvDTE80.DTE2 dte = obj as EnvDTE80.DTE2: 将获取到的 COM 对象转换为 EnvDTE80.DTE2 接口类型。

    • dte.Solution.FullName == solutionPath: 如果提供了 solutionPath 参数,则进一步判断当前 Visual Studio 实例是否已经打开了指定的解决方案。

    • _vsInstance = dte; break;: 如果找到匹配的 Visual Studio 实例,则将其赋值给 _vsInstance 变量并跳出循环。

6. Type.GetTypeFromProgIDActivator.CreateInstance

如果在 ROT 中没有找到正在运行的 Visual Studio 实例,代码会使用 Type.GetTypeFromProgIDActivator.CreateInstance创建一个新的 Visual Studio 实例

  • Type visualStudioType = Type.GetTypeFromProgID(_progID, true);:

    • Type.GetTypeFromProgID(_progID, true): 根据 ProgID (_progID = "VisualStudio.DTE") 获取 Visual Studio 类型的 Type 对象。 第二个参数 true 表示如果找不到类型则抛出异常。

  • _vsInstance = Activator.CreateInstance(visualStudioType) as EnvDTE80.DTE2;:

    • Activator.CreateInstance(visualStudioType): 使用获取到的 Type 对象创建 Visual Studio 类型的实例。

    • as EnvDTE80.DTE2: 将创建的实例转换为 EnvDTE80.DTE2 接口类型,并赋值给 _vsInstance 变量。

7. SolutionProject 对象

AddFilesToSolution 方法中,代码使用了 SolutionProject 对象来操作 Visual Studio 解决方案和项目。

  • _vsInstance.Solution: 访问当前 Visual Studio 实例的 Solution 对象,代表了当前加载的解决方案。

  • _vsInstance.Solution.IsOpen: 判断当前解决方案是否已打开。

  • _vsInstance.Solution.Open(solution): 打开指定的解决方案文件 (solution 参数)。

  • _vsInstance.Solution.Close(): 关闭当前解决方案。

  • _vsInstance.Solution.Projects: 访问当前解决方案中所有项目的集合。

  • item.UniqueName.Contains(projectName): 遍历项目集合,判断项目的唯一名称 (UniqueName) 是否包含指定的项目名称 (projectName),以查找目标项目。

  • item.ProjectItems.AddFromFile(file): 在目标项目 (item) 中,使用 ProjectItems.AddFromFile(file) 方法将指定路径的文件 (file) 添加到项目中。

8. ItemOperations.OpenFile

AddFilesToSolution 方法中,代码使用了 ItemOperations.OpenFile 方法来打开文件。

  • _vsInstance.ItemOperations: 访问 ItemOperations 对象,用于执行项目和项的操作。

  • _vsInstance.ItemOperations.OpenFile(cpp): 使用 OpenFile 方法打开指定路径的文件 (cpp 变量,通常是 .cpp 文件路径)。

  • .Visible = true: 设置打开的文件视图为可见。

9. 错误处理 (try-catch-finally)

代码在 OpenVisualStudioAddFilesToSolution 方法中都使用了 try-catch-finally 块进行错误处理。

  • try: 包含可能发生异常的代码块。

  • catch (Exception ex): 捕获 try 块中发生的任何异常,并将异常信息输出到调试窗口 (Debug.WriteLine(ex.Message)) 并记录到日志 (Logger.Log(MessageType.Error, ...)).

  • finally: 无论 try 块是否发生异常,finally 块中的代码都会被执行。 在代码中,finally 块用于 释放 COM 对象 (Marshal.ReleaseComObject),确保 COM 资源被正确释放,防止资源泄漏。

10. 资源释放 (Marshal.ReleaseComObject)

由于 COM 对象是 非托管资源,.NET 的垃圾回收器 (Garbage Collector) 无法自动管理 COM 对象的生命周期。 因此,必须显式地释放 COM 对象,防止资源泄漏和程序崩溃。

代码中使用 Marshal.ReleaseComObject(monikerTable);, Marshal.ReleaseComObject(rot);, Marshal.ReleaseComObject(bindCtx); 来释放获取到的 COM 接口对象 (monikerTable, rot, bindCtx)。 务必在不再使用 COM 对象后立即释放它们。 通常在 finally 块中进行释放操作,确保即使发生异常也能释放资源。

11. 代码结构和 VisualStudio

代码将 Visual Studio 自动化相关的功能封装在一个静态类 VisualStudio 中。 这种静态类的设计模式适用于提供一组相关的实用工具函数,而不需要创建类的实例。

  • static class VisualStudio: 定义静态类 VisualStudio

  • private static EnvDTE80.DTE2 _vsInstance = null;: 静态私有变量,存储 Visual Studio 实例。

  • private readonly static string _progID = "VisualStudio.DTE";: 静态只读私有常量,存储 Visual Studio ProgID。

  • [DllImport("ole32.dll")] private static extern ...: 导入 WinAPI 函数。

  • public static void OpenVisualStudio(string solutionPath): 公共静态方法,打开 Visual Studio 实例。

  • public static void CloseVisualStudio(): 公共静态方法,关闭 Visual Studio 实例。

  • internal static bool AddFilesToSolution(string solution, string projectName, string[] files): 内部静态方法,向解决方案添加文件。

12. 其他知识点

  • Debug.Assert(files?.Length > 0);: 使用 Debug.Assert 进行 断言,在调试版本中检查条件是否为真,用于快速发现代码中的错误。

  • Debug.WriteLine(ex.Message);: 使用 Debug.WriteLine 将调试信息输出到 调试窗口,方便开发人员查看。

  • Logger.Log(MessageType.Error, ...);: 使用自定义的 Logger 类进行 日志记录,将错误信息记录到日志文件或其他日志系统中,用于错误跟踪和诊断。

  • DllImport 特性: [DllImport("ole32.dll")] 特性用于 声明导入非托管 DLL (Dynamic Link Library) 中的函数,使得 .NET 代码可以调用 WinAPI 函数。

  • IntPtr 数据类型: IntPtrInteger Pointer 的缩写,用于表示 非托管指针或句柄。 在 COM 互操作和 WinAPI 调用中,经常需要使用 IntPtr 来传递或接收指针类型的数据。

  • COMException: COMException 是 .NET 中用于 表示 COM 异常的类。 当 COM 方法调用返回错误 HRESULT 代码时,通常会抛出 COMException 异常。

  • Marshal.ReleaseComObject 方法: Marshal.ReleaseComObject 方法用于 显式释放 COM 对象的 Runtime Callable Wrapper (RCW)。 RCW 是 .NET 运行时为了管理 COM 对象而创建的代理对象。 释放 RCW 会减少 COM 对象的引用计数,当引用计数降为零时,COM 对象才会被真正释放。

13. using 语句 vs. finally 块 for COM 对象释放

虽然在 finally 块中使用 Marshal.ReleaseComObject 可以确保 COM 对象被释放,但更推荐使用 using 语句 来管理实现了 IDisposable 接口的 COM 对象 (如果适用)。 using 语句可以更简洁和安全地管理资源,确保资源在使用完毕后被及时释放,即使发生异常也能保证释放。

然而,EnvDTE 接口及其相关的 COM 对象 通常不直接实现 IDisposable 接口,因此,在 EnvDTE 自动化编程中,finally 块 + Marshal.ReleaseComObject 仍然是常用的资源释放方式。 在其他 COM 场景中,如果 COM 对象实现了 IDisposable,优先使用 using 语句。

14. Visual Studio 自动化最佳实践

  • 尽早释放 COM 对象: 在不再需要 COM 对象时,立即调用 Marshal.ReleaseComObject 释放资源,避免资源泄漏。

  • 错误处理: 始终使用 try-catch-finally 块进行错误处理,确保即使发生异常也能正确释放 COM 对象。

  • 延迟加载: 只在需要时才获取 Visual Studio 实例,避免在程序启动时就加载 DTE 自动化对象,提高启动速度。

  • 谨慎使用 ExecuteCommand: ExecuteCommand 方法虽然方便,但依赖于 Visual Studio 命令的字符串名称,命令名称可能会在不同 Visual Studio 版本之间发生变化,建议尽量使用对象模型提供的更稳定的 API。

  • 参考官方文档: Visual Studio DTE 对象模型非常庞大和复杂,遇到问题时,查阅 Microsoft 官方文档 (Visual Studio DTE 文档) 是最权威和可靠的解决方案。

static class VisualStudio
{
	private static EnvDTE80.DTE2 _vsInstance = null;
	private readonly static string _progID = "VisualStudio.DTE";

	[DllImport("ole32.dll")]
	private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);

	[DllImport("ole32.dll")]
	private static extern int GetRunningObjectTable(uint reserved, out IRunningObjectTable pprot);


	public static void OpenVisualStudio(string solutionPath)
	{
		IRunningObjectTable rot = null;
		IEnumMoniker monikerTable = null;
		IBindCtx bindCtx = null;
		try
		{

			if (_vsInstance == null)
			{

				var hResult = GetRunningObjectTable(0, out rot);
				if (hResult < 0 || rot == null) throw new COMException($"GetRunningObjectTable() return HRESULT: {hResult:X8}");

				rot.EnumRunning(out monikerTable);
				monikerTable.Reset();

				hResult = CreateBindCtx(0, out bindCtx);
				if (hResult < 0 || bindCtx == null) throw new COMException($"CreateBindCtx() return HRESULT: {hResult:X8}");

				IMoniker[] currentMoniker = new IMoniker[1];
				bool isOpen = false;
				while (monikerTable.Next(1, currentMoniker, IntPtr.Zero) == 0)
				{
					string name = string.Empty;
					currentMoniker[0]?.GetDisplayName(bindCtx, null, out name);
					if (name.Contains(_progID))
					{
						hResult = rot.GetObject(currentMoniker[0], out object obj);
						if (hResult < 0 || obj == null) throw new COMException($"Running object table`s GetObject() return HRESULT: {hResult:X8}");

						EnvDTE80.DTE2 dte = obj as EnvDTE80.DTE2;
						var solutionName = dte.Solution.FullName;
						if (solutionName == solutionPath)
						{
							_vsInstance = dte;
							break;
						}
					}
				}

				if (_vsInstance == null)
				{
					Type visualStudioType = Type.GetTypeFromProgID(_progID, true);
					_vsInstance = Activator.CreateInstance(visualStudioType) as EnvDTE80.DTE2;
				}
			}
		}
		catch (Exception ex)
		{
			Debug.WriteLine(ex.Message);
			Logger.Log(MessageType.Error, "Failed to open Visual Studio");
		}
		finally
		{
			if (monikerTable != null) Marshal.ReleaseComObject(monikerTable);
			if (rot != null) Marshal.ReleaseComObject(rot);
			if (bindCtx != null) Marshal.ReleaseComObject(bindCtx);
		}
	}

	public static void CloseVisualStudio()
	{
		if (_vsInstance?.Solution.IsOpen == true)
		{
			_vsInstance.ExecuteCommand("File.SaveAll");
			_vsInstance.Solution.Close();
		}
		_vsInstance?.Quit();
	}

	internal static bool AddFilesToSolution(string solution, string projecName, string[] files)
	{
		Debug.Assert(files?.Length > 0);
		OpenVisualStudio(solution);
		try
		{
			if (_vsInstance != null)
			{
				if (!_vsInstance.Solution.IsOpen) _vsInstance.Solution.Open(solution);
				else _vsInstance.ExecuteCommand("File.SaveAll");

				foreach (EnvDTE.Project item in _vsInstance.Solution.Projects)
				{
					if (item.UniqueName.Contains(projecName))
					{
						foreach (var file in files)
						{
							item.ProjectItems.AddFromFile(file);
						}
					}
				}

				var cpp = files.FirstOrDefault(x => Path.GetExtension(x) == ".cpp");
				if (!string.IsNullOrEmpty(cpp))
				{
					_vsInstance.ItemOperations.OpenFile(cpp).Visible = true;
				}
				_vsInstance.MainWindow.Activate();
				_vsInstance.MainWindow.Visible = true;
			}
		}
		catch (Exception ex)
		{
			Debug.WriteLine(ex.Message);
			Logger.Log(MessageType.Error, "Failed to addd Files To solution");
			return false;
		}
		return true;
	}
}