我们在第6和第7篇创建的Calculate小工具窗还有很多可以改进的地方,所以在这篇文章里,我们不会开发新的功能,而是重构我们的代码,封装出可以重用的类和方法。
VSX背后的对象模型是非常丰富的:有几百个类和几千个方法。但我们在开发VS add-in和package的时候,光记住类和方法的名字是不够的,我们还需要知道相应的GUID以及其他相关的常数。
我觉得在VSX的开发中最难的是开发者必须要把.NET和COM混着用。如果VSX的编程模型(对象模型)更简洁一点话,对开发人员是非常好的事情。
微软在interop程序集之上,开发了一些用于托管代码的层(其中一个叫做MPF,全称是Managed Package Framework)。我认为MPF里提供的类和方法是非常棒的,但它们只会涉及到VSX的某些方面,还不够。
所以在这篇文章里,我会告诉你如何把常用的功能封装出来,供我们以后开发VSX时使用。我希望你也能够在开发过程中,逐步创建你自己需要的工具集。
从这篇文章开始,我会创建一个叫做VsxTools的类库。这一次我仅仅出于演示目的来使用这个类库,但是既然我们是一起学习VSX的,所以我打算把这个类库弄成一个真正可用的工具。在这篇文章里我会做如下的重构:
当你在看这篇文章的时候,我已经把所有的示例代码和文章放到了CodePlex上了(http://www.codeplex.com/LearnVSXNow)。如果下载了最新的源码,你会看到在PackageStartupSamples目录下有一个PackageStartupSamples.sln文件。它包含了这系列文章里的所有的例子。我会随着VS 2008 SDK版本的更新来相应的更新这些例子(当然如果发现了bug的话,我也会更新它们)。
我们最好把可重用的代码放到一个单独的类库里。所以,让我们创建一个名为VsxTools的C# class library项目,并把它添加到StartupToolsetRefactored项目所在的解决方案中。由于我们需要向这个VsxTools中添加VSX代码,所以我们要向这个项目中添加VS SDK interop和MPF程序集引用:
— Microsoft.VisualStudio.OLE.Interop
— Microsoft.VisualStudio.Shell.9.0
— Microsoft.VisualStudio.Interop
— Microsoft.VisualStudio.Interop.8.0
— Microsoft.VisualStudio.Interop.9.0
接下来,我们可以向这个类库里添加功能了。
如果想往活动日志里写日志的话,我们需要写差不多半打行数的代码,例如:
private void LogCalculation(string firstArg, string secondArg, string operation,
string result)
{
string message = String.Format("Calculation executed: {0} {1} {2} = {3}",
firstArg, operation, secondArg, result);
IVsActivityLog log =
Package.GetGlobalService(typeof(SVsActivityLog)) as IVsActivityLog;
if (log == null) return;
log.LogEntry(
(result == "#Error")
?(UInt32) __ACTIVITYLOG_ENTRYTYPE.ALE_ERROR
: (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION,
"Calculation", message);
}
但是这个方法有很多“噪音”:
所以必须得想办法去掉这些“噪音”。如果能用下面这段代码岂不是很好?
string message = String.Format("Calculation executed: {0} {1} {2} = {3}", firstArg, operation, secondArg, result);
ActivityLog.Write(result == "#Error" ? ActivityLogType.Error : ActivityLogType.Information, "Calculation", message);
在这段代码里,我们减少了如下“噪音”:
另外,这种非常简单的、带智能感知的方式可以提高我们敲代码的速度。
改进的ActivityLog模式基于3个类型:
// --- Represents entry types instead of __ACTIVITYLOG_ENTRYTYPE constants
public enum ActivityLogType
{
Information,
Warning,
Error
}
// --- Represents an entity holding all log entry properties
public sealed class ActivityLogEntry
{
...
public ActivityLogType Type { get; set; }
public string Source { get; set; }
public string Message { get; set; }
public Guid? Guid { get; set; }
public int? Hr { get; set; }
public string Path { get; set; }
...
}
// --- Provides log services through static Write methods
public static class ActivityLog
{
public static void Write(ActivityLogEntry entry);
public static void Write(string source, string message);
...
public static void Write(string source, string message, Guid guid, int hr);
...
public static void Write(ActivityLogType type, string source, string message);
...
}
ActivityLogType枚举的功能是显而易见的,所以就不说它了。静态类ActivityLog通过Write方法供外面调用,这个方法有很多重载版本,可以适应不同的参数组合。如果我们在编程的时候不能确定要记录日志的哪些属性,可以调用接收ActivityLogEntry类型的Write方法的重载版本。在这个方法内部判断应该调用IVsActivityLog的哪个方法,例如,如果只用到了Hr和Path属性,我们可以调用LogEntryHrPath方法。
在VsxTools项目里添加一个ActivityLog.cs文件,并在里面添加上面的三个类型。在ActivityLogEntry类里,我弄了几个构造函数,每一个负责设置不同的属性。最主要的“逻辑”是写在ActivityLog静态类里的,在这个类里,我添加了一些私有属性和私有方法:
public static class ActivityLog
{
...
private static UInt32 MapLogTypeToAle(ActivityLogType logType)
{
switch (logType)
{
case ActivityLogType.Information:
return (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION;
case ActivityLogType.Warning:
return (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_WARNING;
default:
return (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR;
}
}
private static IVsActivityLog Log
{
get
{
return Package.GetGlobalService(typeof (SVsActivityLog)) as IVsActivityLog;
}
}
private static void LogEntry(ActivityLogType type, string source,
string message)
{
IVsActivityLog log = Log;
if (log != null)
{
log.LogEntry(MapLogTypeToAle(type), source, message);
}
}
...
}
这些方法都很简单,就不解释它们了。和LogEntry方法一样,我还添加了IVsActivityLog服务中其他的方法,例如LogEntryGuid。在Write方法里,可以调用这些私有方法:
public static void Write(string source, string message)
{
Write(ActivityLogType.Information, source, message);
}
public static void Write(ActivityLogType type, string source, string message)
{
LogEntry(type, source, message);
}
就这些就行了。通过实现这个东西,我们就拥有了一个非常简单并且容易记住的活动日志的模型。
现在可以修改CalculationControl.cs文件中的LogCalculation方法了:
private void LogCalculation(string firstArg, string secondArg, string operation,
string result)
{
string message = String.Format("Calculation executed: {0} {1} {2} = {3}", firstArg, operation, secondArg, result);
ActivityLog.Write(result == "#Error" ? ActivityLogType.Error : ActivityLogType.Information, "Calculation", message);
}
现在,你可以编译并运行一下StartupToolsetRefactored例子了。别忘了我们曾在第7章讲过怎样查看活动日志,还有,别忘了在项目属性的Debug页签里加上/log开关,这样它才能记录活动日志。
在这篇文章开始的时候,我说过我要简化一下output window的使用,所以让我们开始吧。在第7篇文章中,我们已经用IVsOutputWindow和IVsOutputWindowPane接口向VS的output window写了日志了:
private void LogCalculationToOutput(string firstArg, string secondArg,
string operation, string result)
{
string message = String.Format("Calculation executed: {0} {1} {2} = {3} ", firstArg, operation, secondArg, result);
IVsOutputWindow outWindow =
Package.GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow;
Guid generalWindowGuid = VSConstants.GUID_OutWindowGeneralPane;
IVsOutputWindowPane windowPane;
outWindow.GetPane(ref generalWindowGuid, out windowPane);
windowPane.OutputString(message);
}
就像活动日志的调用方式那样,上面红色的代码也有着类似的“噪音”。但在我们去掉这些噪音之前,让我们先来瞧一瞧VS output window的结构和与它相关的服务。
Visual Studio只有一个output window,但是它却可以包含多个pane来隔离多种output。Visual Studio它自己定义了一些output window pane,VSPackage也可以定义他们自己的pane。一个package可以向任何已有的pane中(包括VS IDE定义的和第三方package定义的)输出消息。下图展示了VS IDE定义的“常规”pane和一个自定义的“My Debug”pane:
要使用output window,用到两个简单的服务:
这些接口的功能如下:
服务接口 | 功能 |
IVsOutputWindow | 这个接口只有3个方法,用来管理output window pane的实例,分别是: CreatePane, DeletePane, GetPane |
IVsOutputWindow2 | 扩展IVsOutputWindow接口,添加了一个新的方法,用于获取当前在用的pane的ID:GetActivePaneGUID |
IVSOutputWindowPane | 这个接口用于管理对应的pane的内容和可见性。 可以调用Activate 方法显示一个pane,调用Hide方法来隐藏一个pane。每个pane都有一个名字,可以通过GetName和SetName 方法来获取名字或设置名字。pane里面的内容可以通过Clear、OutputString和OutputStringThreadSafe方法来管理。 发送到window pane里的信息也可以通过调用OutputTaskItemString、 OutputTaskItemStringEx和FlushToTaskList方法来放到任务列表中。 |
IVsOutputWindowPane2 | 扩展IVsOutputWindowPane接口,添加了OutputTaskItemStringEx2方法,可以把output信息和错误列表中的消息关联起来。 |
总结一下上述表格:用IVsOutputWindow来管理pane,用IVsOutputWindowPane来管理每个pane中的output信息。
window pane由GUID来标识。在Microsoft.VisualStudio.VSConstants类里,定义了3个VS IDE中常用的pane的GUID:、
Window Pane | GUID |
General | GUID_OutWindowGeneralPane |
Build | GUID_BuildOutputWindowPane |
Debug | GUID_OutWindowDebugPane |
如果一个package创建了一个window pane,必须有它自己的GUID。我们可以用这个GUID来获取这个pane的引用,就像其他VS IDE内置的pane一样。但如果这个package没有公开出这个GUID的话,我们也可以用IVSOutputWindow2的GetActivePaneGUID来得到这个GUID。
通过SVsOutputWindow得到的IVsOutputWindow接口实例有3个用于管理pane的方法:
public interface IVsOutputWindow
{
int GetPane(ref Guid rguidPane, out IVsOutputWindowPane ppPane);
int CreatePane(ref Guid rguidPane, string pszPaneName, int fInitVisible, int fClearWithSolution);
int DeletePane(ref Guid rguidPane);
}
每一个方法的第一个参数都是pane的GUID。这3个方法的名字已经很清楚的告诉我们它们是干嘛的了。调用CreatePane方法的时候,你需要传递3个额外的参数:
你也许认为,如果我们对VS内置的output pane调用CreatePane和DeletePane的话,VS会报错。但是不是这样的,这两个方法也可以删除和重新创建原本已经内置的pane。所以在用的时候你必须意识到这一点。
最常用的方法是GetPane,它可以获取一个IVsOutputWindowPane的实例,从而向相应的pane中写消息。
IVsOutputWindowPane接口提供了往pane中写消息的功能。你可以把文本消息输出到pane中,也可以输出到任务列表中,但是在这篇文章中,我仅仅把消息直接输出到pane中(处理任务列表是以后的文章的主题)。通过调用OutputString或OutputStringThreadSafe这两个方法,你可以用线程安全或线程不安全的形式把消息输出到pane中。什么时候需要用线程安全的方法,什么时候不需要用,这个要搞清楚。如果你搞不清楚的话,那就用OutputStringThreadSafe吧。
正如你看到的那样,为了管理output pane并往里面写消息,我们需要写好几行有噪音的代码。现在让我告诉你一个去掉这些噪音的解决方案。我并不认为这是最好的方案,但这肯定是一个解决方案。如果你有更好的主意,请告诉我。
由于你们是开发人员,所以没有什么比直接看代码能够说的更清楚了。我的解决方案可以通过CalculationControl.cs文件里的这几行代码来描述清楚:
public partial class CalculationControl : UserControl
{
...
private void LogCalculationToOutput(string firstArg, string secondArg,
string operation, string result)
{
string message = String.Format("Calculation executed: {0} {1} {2} = {3}",
firstArg, operation, secondArg, result);
OutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane));
pane.WriteLine(message);
}
...
[PaneName("My Debug")]
[InitiallyVisible(true)]
[ThreadSafe(true)]
private sealed class MyDebugPane: OutputPaneDefinition
{}
...
}
红色的代码创建了一个叫做“My Debug”的output window pane ,并且用线程安全的形式把消息输出进去。OutputWindow类的GetPane方法会在需要的时候创建pane。pane以一个简单的类的形式定义,并标记上一些属性。所有使轮子转起来的工作被放到了后台,调用这不用关心。
如果你想往“General”这个pane中写消息的话,上面的代码还可以更短:
OutputWindow.General.WriteLine(message);
这个解决方案的基础是3个类,如下:
类型 | 功能 |
OutputPaneDefinition | 可以用这个类来继承output window pane definition(OWPD)。一个OWPD类型仅仅是一个定义,在它上面可以添加这个pane的特性的attribute。 OutputWindow和OutputWindowPane用这个类的属性去获取这些attribute的值。 |
OutputWindow | 这个静态类负责管理output window pane,就像IVsOutputWindow接口那样。这个类也提供了静态属性,用这些属性可以直接访问到VS内置的pane。同时,这个类提供了一个异常处理机制,可以把消息转发到“Genernal”或“Debug” pane中,甚至转发到一个虚拟的pane中(Silent pane)。 |
OutputWindowPane | 这个类负责把消息输出到它对应的pane中,和IVsOutputWindowPane接口一样(不过它不支持任务列表的处理)。它提供Write和WriteLine方法,类似System.Console类。 你可以把这个类看成IVsOutputWindowPane的包装类(Wrapper class)。 |
当用OutputPaneDefinition来定义一个pane时,我们可以把这个pane弄成默认线程安全的。这样的话,就会以线程安全的方式当向pane中输出消息。
如果我们需要用VS的标准pane,只需要用OutputWindow类中的General、Debug或Build静态属性就行了。
不过如果我们创建VSPackage的话,我们也许需要自己的output window pane。在“传统”方式下,我们用一个GUID来代表这个pane,但在我的方案下,我用一个继承自OutputWindowDefinition的类来代表这个pane,这个类上可以添加关于这个pane特性的attribute。在OutputWindowDefinition的默认构造函数里,通过读取这些attribute来设置属性值。 下面是这个类的定义:
public abstract class OutputPaneDefinition
{
protected OutputPaneDefinition();
public virtual Guid GUID { get; }
public string Name { get; }
public bool InitiallyVisible { get; }
public bool ClearWithSolution { get; }
public bool ThreadSafe { get; }
public bool IsSilent { get; internal set; }
}
我们可以把一个pane定义成安静的,也就说并没有物理上的pane,任何输出到这个pane上的消息都会以安静的模式处理掉。另外,为了定义一个已经存在的pane(例如VS内置的pane或由第三方package定义的pane),我们可以重写Guid属性。
为了演示这些属性的用法,让我们看一下OutputWindow类中的“Debug” pane和Silent pane是怎么定义的:
public static class OutputWindow
{
...
private sealed class DebugPane : OutputPaneDefinition
{
public override Guid GUID
{
get { return VSConstants.GUID_OutWindowDebugPane; }
}
}
...
private sealed class SilentPane : OutputPaneDefinition
{
public SilentPane()
{
IsSilent = true;
}
}
...
}
OutputWindowDefinition可以识别如下attribute:
public sealed class PaneNameAttribute: StringAttribute {...}
public sealed class InitiallyVisibleAttribute: BoolAttribute {...}
public sealed class ClearWithSolutionAttribute: BoolAttribute {...}
public sealed class ThreadSafeAttribute: BoolAttribute {...}
为了定义一个自己的pane,可以像下面的代码那样创建一个类:
[Guid("6D71C5F7-200C-4322-A264-65C78CF511AA")]
[PaneName("My Own Pane")]
[InitiallyVisible(false)]
[ClearWithSolution(true)]
[ThreadSafe(true)]
private sealed class MyOwnPane: OutputPaneDefinition
{}
更详细的代码细节,请参考OutputWindowDefinition.cs文件。
我参考IVsOutputWindow提供的功能,创建了OutputWindow类,并额外添加了一些小的功能。我声明了一个OutputPaneHandling属性,是枚举类型的,代表当物理上的pane无法取得时,如何处理消息。这个枚举有如下的枚举值:
枚举值 | 含义 |
Silent | 不产生任何异常,待输出的信息也不发送到任何pane中。 |
ThrowException | 抛出WindowPaneNotFoundException异常。 |
RedirectToGeneral | 输出信息转到General pane中。 |
RedirectToDebug | 输出信息转到Debug pane中。 |
这个类的结构如下:
public static class OutputWindow
{
public static OutputPaneHandling OutputPaneHandling { get; set; }
public static OutputWindowPane General { get; }
public static OutputWindowPane Build { get; }
public static OutputWindowPane Debug { get; }
public static OutputWindowPane Silent { get; }
public static OutputWindowPane CreatePane(Type type);
public static OutputWindowPane GetPane(Type type);
public static bool DeletePane(Type type);
}
CreatePane、GetPane和DeletePane方法接受一个Type类型的参数,这个类型必须继承自WindowPaneDefinition类。代表内置的pane的类是OutputWindow类的私有嵌套类,你不能用它们的类型作为参数,所以你也不能创建或者删除它们。CreatePane方法只能够创建原本不存在的pane,如果这个pane已经创建了,就只会返回它的实例。调用GetPane的时候,如果某个pane不存在,GetPane方法会创建它。
具体细节,可以参考OutputWindow.cs文件。
我在前面提到过,OutputWindowPane类实际上是IVsOutputWindowPane实例的一个包装。我只不过在设计和实现这个包装类的时候做了一些小改动。
IVsOutputWindowPane用两个单独的方法分别以线程安全和不安全的方式写消息:OutputStringThreadSafe和OutputString。我想隐藏这两个方法,这样使用者在用的时候,就不用关心该调用哪一个。在这个类里面,我加了一个布尔属性ThreadSafe,由它来决定该调用哪个方法。你还记得吧,WindowPaneDefinition类识别ThreadSafeAttribute,所以当创建了一个pane的实例之后,OutputWindowPane的ThreadSafe属性值会设置成WindowPaneDefinition的ThreadSafeAttribute指定的初始值。
另外,IVsOutputWindowPane的GetName和SetName方法被封装成Name属性。
OutputWindowPane类的成员如下:
public sealed class OutputWindowPane
{
internal OutputWindowPane(OutputPaneDefinition paneDef, IVsOutputWindowPane pane);
public bool ThreadSafe { get; set; }
public string Name { get; set; }
public bool IsVirtual { get; }
public void Activate();
public void Hide();
public void Clear();
public void Write(string output);
public void Write(string format, params object[] parameters);
public void Write(IFormatProvider provider, string format, params object[] parameters);
public void WriteLine(string output);
public void WriteLine(string format, params object[] parameters);
public void WriteLine(IFormatProvider provider, string format, params object[] parameters)
}
构造函数应该被弄成internal的,这样OutputWindow类就是OutputWindowPane的工厂类了,使用者没法自己new一个实例出来。在构造函数中,需要传入OutputPaneDefinition实例,同时也需要传入IVsOutputWindowPane的实例。IsVirtual属性可以用来设置这个pane到底是一个物理上的pane,还是一个虚拟的、安静的pane。
这个类提供了一些Write和WriteLine方法,用来代替原来的OutputString和OutputStringThreadSafe方法,并模仿System.Console中的声明方式。
上面这些方法的实现都很简单,具体你可以参考OutputWindowPane.cs文件。
编译并运行StartupToolsetRefactored项目,并点击Calculate按钮,你会发现消息输出到了一个叫“My Debug”的output pane中。如果你有时间的话,可以试着对代码做些改动(在CalculationControl类的LogCalculationToOutput方法里),并看一下相应的变化:
// --- Original code lines:
OutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane));
pane.WriteLine(message);
// --- Change 1: Writing to two panes
OutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane));
pane.WriteLine(message);
OutputWindow.General.WriteLine(message);
// --- Change 2: Changing the pane name
OutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane));
pane.Name = "My Debug (modified)";
pane.WriteLine(message);
// --- Change 3: Reflecting to an invalid pane
OutputWindowPane pane = OutputWindow.GetPane(typeof(int));
pane.WriteLine(message);
// --- Change 4: Throwing an exception (VS 2008 will stop!)
OutputWindow.OutputPaneHandling = OutputPaneHandling.ThrowException;
OutputWindowPane pane = OutputWindow.GetPane(typeof(int));
pane.WriteLine(message);
// --- Change 5: Silent exception (No output will be shown)
OutputWindow.OutputPaneHandling = OutputPaneHandling.Silent;
OutputWindowPane pane = OutputWindow.GetPane(typeof(int));
pane.WriteLine(message);
// --- Change 6: Throwing and handling exception
OutputWindow.OutputPaneHandling = OutputPaneHandling.ThrowException;
try
{
OutputWindowPane pane = OutputWindow.GetPane(typeof (int));
pane.WriteLine(message);
}
catch (WindowPaneNotFoundException ex)
{
OutputWindow.General.WriteLine(ex.Message);
}
在这篇文章里,我们修改了StartupToolsetRefactored项目,以VsxTools的形式提供helper类。这些helper类是托管的类型,减少了由VS 2008 SDK的interop类带来的“噪音”。我们为活动日志和output widow pane开发了这种可重用的类。
现在,所有的源代码(包括前几篇文章的例子)和文章可以在CodePlex(http://www.codeplex.com/LearnVSXNow)上找到。
我希望这些helper类能够对你有用。但是,我写这篇文章的本意并不是告诉你怎样去除掉代码中的“噪音”,而是希望告诉你:在VS interop类的基础上创建自己的托管类型是值得的。Microsoft在用MPF来实现这个目的,但依然还有很多地方可以使VSX的开发体验变得更有趣和更愉快!
当开始这个系列的时候,我还没有打算创建自己的VSX工具集,但现在我已经决定利用VSX社区的支持来做这些了…
原文链接:http://dotneteers.net/blogs/divedeeper/archive/2008/02/04/LearnVSXNowPart10.aspx