(翻译)LearnVSXNow! #10 创建我们第一个工具集重用代码

     我们在第6和第7篇创建的Calculate小工具窗还有很多可以改进的地方,所以在这篇文章里,我们不会开发新的功能,而是重构我们的代码,封装出可以重用的类和方法。

     VSX背后的对象模型是非常丰富的:有几百个类和几千个方法。但我们在开发VS add-in和package的时候,光记住类和方法的名字是不够的,我们还需要知道相应的GUID以及其他相关的常数。

     我觉得在VSX的开发中最难的是开发者必须要把.NET和COM混着用。如果VSX的编程模型(对象模型)更简洁一点话,对开发人员是非常好的事情。

     微软在interop程序集之上,开发了一些用于托管代码的层(其中一个叫做MPF,全称是Managed Package Framework)。我认为MPF里提供的类和方法是非常棒的,但它们只会涉及到VSX的某些方面,还不够。

     所以在这篇文章里,我会告诉你如何把常用的功能封装出来,供我们以后开发VSX时使用。我希望你也能够在开发过程中,逐步创建你自己需要的工具集。

     从这篇文章开始,我会创建一个叫做VsxTools的类库。这一次我仅仅出于演示目的来使用这个类库,但是既然我们是一起学习VSX的,所以我打算把这个类库弄成一个真正可用的工具。在这篇文章里我会做如下的重构:

  1. 改进活动日志的调用
  2. 简化output window的调用

CodePlex上的源码

     当你在看这篇文章的时候,我已经把所有的示例代码和文章放到了CodePlex上了(http://www.codeplex.com/LearnVSXNow)。如果下载了最新的源码,你会看到在PackageStartupSamples目录下有一个PackageStartupSamples.sln文件。它包含了这系列文章里的所有的例子。我会随着VS 2008 SDK版本的更新来相应的更新这些例子(当然如果发现了bug的话,我也会更新它们)。

创建VsxTools类库

     我们最好把可重用的代码放到一个单独的类库里。所以,让我们创建一个名为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);
}

     但是这个方法有很多“噪音”:

  1. 为了使用这个服务对象,我必须记住IVsActivityLogSVsActivityLog这两个名字。
  2. 我必须在使用它之前判断它是不是null。
  3. 我必须知道__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION这两个“魔术”常数。虽然从它们的名字上可以猜出它们代表的意思,但是很不直观,而且很难记住。我甚至还得把它们转换成System.UInt32类型。
  4. 在上面这个例子里,由于我们只需要记录日志类型、日志源和日志消息,所以我们调用了LogEntry方法。但是如果我们想记录其它信息,我们还得找另外一个方法才行。

     所以必须得想办法去掉这些“噪音”。如果能用下面这段代码岂不是很好?

string message = String.Format("Calculation executed: {0} {1} {2} = {3}", firstArg, operation, secondArg, result);
ActivityLog.Write(result == "#Error" ? ActivityLogType.Error : ActivityLogType.Information,  "Calculation", message);

     在这段代码里,我们减少了如下“噪音”:

  1. 要想使用活动日志这个service的话,我们只需要记住一个直观的名字ActivityLog就行了。
  2. 用一个很直观的枚举来代表日志类型。
  3. 写日志的时候,只需要一个Write方法就够了。当然这个方法有很多个重载版本,可以覆盖所有的参数组合。
  4. 不需要转换类型,不需要空引用检查。

     另外,这种非常简单的、带智能感知的方式可以提高我们敲代码的速度。

定义ActivityLog

     改进的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方法。

ActivityLog的内部实现

     在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);
}

     就这些就行了。通过实现这个东西,我们就拥有了一个非常简单并且容易记住的活动日志的模型。

在旧代码中使用新的ActivityLog模型

     现在可以修改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的后台结构

     在这篇文章开始的时候,我说过我要简化一下output window的使用,所以让我们开始吧。在第7篇文章中,我们已经用IVsOutputWindowIVsOutputWindowPane接口向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:

image

image

     要使用output window,用到两个简单的服务:

  1. SVsOutputWindow服务用来管理(获取、创建和删除)output window pane,但它不能用来输出消息。不过它可以获取IVsOutputWindowPaneIVsOutputWindowPane2(第二个是第一个的扩展)的实例,这些接口用来把output信息输出到相应的window pane中。
  2. SVsGeneralOutputWindowPane服务用于取得General output window pane对应的IVsOutputWindowPane实例。

     这些接口的功能如下:

服务接口 功能
IVsOutputWindow

这个接口只有3个方法,用来管理output window pane的实例,分别是:

CreatePane, DeletePane, GetPane

IVsOutputWindow2

扩展IVsOutputWindow接口,添加了一个新的方法,用于获取当前在用的pane的ID:GetActivePaneGUID

IVSOutputWindowPane

这个接口用于管理对应的pane的内容和可见性。

可以调用Activate 方法显示一个pane,调用Hide方法来隐藏一个pane。每个pane都有一个名字,可以通过GetNameSetName 方法来获取名字或设置名字。pane里面的内容可以通过ClearOutputStringOutputStringThreadSafe方法来管理。

发送到window pane里的信息也可以通过调用OutputTaskItemString、 OutputTaskItemStringExFlushToTaskList方法来放到任务列表中。

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的话,我们也可以用IVSOutputWindow2GetActivePaneGUID来得到这个GUID。

负责管理pane的方法

     通过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个额外的参数:

  1. pszPaneName表示pane的初始名字(可以在创建后改变这个名字)
  2. fInitVisible用于设置pane的初始可见性。如果设成了true(即非0值),这个pane在创建后会立刻显示。当然,这里是说这个pane会显示在output window里,但output window是可以隐藏的。不过你可以通过视图|输出(View|Output)菜单来显示output window。
  3. fClearWithSolution参数如果设成true的话,pane里面的内容就会随着解决方案的关闭而自动清空。

     你也许认为,如果我们对VS内置的output pane调用CreatePaneDeletePane的话,VS会报错。但是不是这样的,这两个方法也可以删除和重新创建原本已经内置的pane。所以在用的时候你必须意识到这一点。

     最常用的方法是GetPane,它可以获取一个IVsOutputWindowPane的实例,从而向相应的pane中写消息。

把消息发送到pane中

     IVsOutputWindowPane接口提供了往pane中写消息的功能。你可以把文本消息输出到pane中,也可以输出到任务列表中,但是在这篇文章中,我仅仅把消息直接输出到pane中(处理任务列表是以后的文章的主题)。通过调用OutputString或OutputStringThreadSafe这两个方法,你可以用线程安全或线程不安全的形式把消息输出到pane中。什么时候需要用线程安全的方法,什么时候不需要用,这个要搞清楚。如果你搞不清楚的话,那就用OutputStringThreadSafe吧。

简化output window的调用

     正如你看到的那样,为了管理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。

OutputWindowOutputWindowPane用这个类的属性去获取这些attribute的值。

OutputWindow

这个静态类负责管理output window pane,就像IVsOutputWindow接口那样。这个类也提供了静态属性,用这些属性可以直接访问到VS内置的pane。同时,这个类提供了一个异常处理机制,可以把消息转发到“Genernal”或“Debug” pane中,甚至转发到一个虚拟的pane中(Silent pane)。

OutputWindowPane

这个类负责把消息输出到它对应的pane中,和IVsOutputWindowPane接口一样(不过它不支持任务列表的处理)。它提供WriteWriteLine方法,类似System.Console类。

你可以把这个类看成IVsOutputWindowPane的包装类(Wrapper class)。

     当用OutputPaneDefinition来定义一个pane时,我们可以把这个pane弄成默认线程安全的。这样的话,就会以线程安全的方式当向pane中输出消息。

定义一个pane类

     如果我们需要用VS的标准pane,只需要用OutputWindow类中的GeneralDebugBuild静态属性就行了。

     不过如果我们创建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文件。

利用OutputWindow管理pane

     我参考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); 
}

     CreatePaneGetPaneDeletePane方法接受一个Type类型的参数,这个类型必须继承自WindowPaneDefinition类。代表内置的pane的类是OutputWindow类的私有嵌套类,你不能用它们的类型作为参数,所以你也不能创建或者删除它们。CreatePane方法只能够创建原本不存在的pane,如果这个pane已经创建了,就只会返回它的实例。调用GetPane的时候,如果某个pane不存在,GetPane方法会创建它。

     具体细节,可以参考OutputWindow.cs文件。

向pane中写消息

     我在前面提到过,OutputWindowPane类实际上是IVsOutputWindowPane实例的一个包装。我只不过在设计和实现这个包装类的时候做了一些小改动。

     IVsOutputWindowPane用两个单独的方法分别以线程安全和不安全的方式写消息:OutputStringThreadSafeOutputString。我想隐藏这两个方法,这样使用者在用的时候,就不用关心该调用哪一个。在这个类里面,我加了一个布尔属性ThreadSafe,由它来决定该调用哪个方法。你还记得吧,WindowPaneDefinition类识别ThreadSafeAttribute,所以当创建了一个pane的实例之后,OutputWindowPaneThreadSafe属性值会设置成WindowPaneDefinitionThreadSafeAttribute指定的初始值。

     另外,IVsOutputWindowPaneGetNameSetName方法被封装成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。

     这个类提供了一些WriteWriteLine方法,用来代替原来的OutputStringOutputStringThreadSafe方法,并模仿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

原文地址:https://www.cnblogs.com/default/p/1699127.html