(翻译)LearnVSXNow! #9 创建一个工具集 重构服务

     在第6和第7部分我们创建了一个StartupToolset示例package,并手动添加了菜单命令和Calculate tool window。本文将重构package,尝试基于服务的代码结构。
     重构这个package不仅适用现在这个package,而且能提取那些在今后的package开发中可重用的代码,使代码变得可读性更强。下一篇将涉及这方面,现在我们只关注服务。


创建一个Startup Toolset包的副本

     为了保留原有的示例,我创建了一个副本叫做StartupToolRefactored。你可以参照第6和第7部分,自己创建一个:先创建一个空的Package命名为StartupToolRefactored,重复第6部分的步骤增加菜单命令,以及第7部分的步骤增加tool window。

在这基础上我修改了所有的GUID,以避免与之前的冲突。为了在UI上与上个版本区分,我还修改了菜单命令的显示文本和tool window的标题。

创建一个全局服务

     重构的第一步,我打算提取“实现计算”这部分代码,做成一个全局的服务,这样其他的Package可以在外部使用这个计算服务。

目前,计算的逻辑代码位于CaculationControl这个用户控件类中,并在Calculate_Click事件处理函数中实现,这样的结构虽然容易阅读和理解,但是过于单一:

    public partial class CalculateControl : UserControl

    {

        …

 

        private void Calculate_Click(object sender, EventArgs e)

        {

            try

            {

                int firstArg = Int32.Parse(FirstArgEdit.Text);

                int secondArg = Int32.Parse(SecondArgEdit.Text);

                int result = 0;

                switch (OperatorCombo.Text)

                {

                    case "+":

                        result = firstArg + secondArg;

                        break;
                    …

                }

                ResultEdit.Text = result.ToString();

            }

            catch (SystemException){ResultEdit.Text = "#Error";}

           

        }

    }

     优化代码结构的方案是提取这部分计算逻辑,构成一个服务对象。如果这个服务是个全局的话,无论是CaculationControl还是其他的Package都可以调用它提供的方法。

 

创建一个服务接口

     每个服务至少提供一个接口作为“契约(contract)”。不必惊讶:这被实现成接口类型。我们固然可以把这个接口写在我们package所在的程序集,然而,这样的话,外部的package必须引用我们的package(译者注:这里的意思是外部的package要使用这个接口时,必须引用整个package,才能使用),通常我们要避免这种情况。

所以,我用传统的方式实现:创建一个单独的程序集来实现这个服务,这个程序集被独立发布,在我们的package中引用这个程序集。

     创建一个类库工程(在解决方案中添加),命名为StartupToolsetInterfaces,将这个类库添加到StartupToolsetRefactored的引用中。把默认的Class1.cs改名为CalculationService.cs

     如果你还记得在前面的示例中是如何获得全局服务的,你一定会想到GetService方法:

IVsUIShell uiShell = (IVsUIShell)GetService(typeof(SVsUIShell));

     为了获取一个服务,我们要使用两个类型:IVsUIShell是声名这个服务的接口;SVsUIShell是所谓的“标记类型(markup type)”,用来标记这个服务类对象的。你可能会问:为什么要用两个类型,用一个接口不就够了吗?是的,一个接口足够了。然而,使用两个类型提供了一些灵活性:一个服务对象可以提供一个或多个接口,同一个接口也可以被一个或多个服务提供。使用两个类型使我们能够对服务对象“命名”,而在对象内部则对服务接口命名。(译者注:这里实际上主要讨论的是标记类型存在的意义:标记类型相当于给服务对象打上了一个标记,即有了一个名字)GetService方法的参数可以是实现了这个接口的类型,但这不是必须的,只要这个参数能够提供一个关键的类型,使得返回这个服务接口的对象就可以。

     Visual Studio的package使用一种叫做标记类型的类型,这种类型并不包含任何功能,仅仅用来标记一个类型,以区分其他已经存在的类型。

     我们用这样的模式在CalculationService.cs里面创建两个接口类型:一个服务接口,另一个标记接口。

using System.Runtime.InteropServices;

namespace StartupToolsetInterfaces

{

    [Guid("D7524CAB-5029-402d-9591-FA0D59BBA0F0")]

    [ComVisible(true)]

    public interface ICalculationService

    {

        bool Calculate(string firstArgText, string secondArgText,

          string operatortext, out string resultText);

    }

    [Guid("AF7F72EF-2B54-4798-B76A-21DC02CC04B7")]

    public interface SCalculationService

    {

     }

}

     请注意:以“I”开头的是服务接口,以“S”开头的是标记接口。必须让COM能够使用,因此定义一个Guid。除此之外服务接口需要被COM对象检索,因此需要COM可见。

 

创建一个服务类型

     我们需要在package中创建一个实例类型来实现calcualtion服务。在StartupToolsetRefactored工程中新建一个CalculationService.cs,代码的框架应该是这样的:

using System;

using StartupToolsetInterfaces;

namespace MyCompany.StartupToolsetRefactored

{

    public sealed class CalculationService : ICalculationService , SCalculationService

    {

        public bool Calculate(string firstArgText, string secondArgText, string operatorText, out string resultText)

        { … }

    }

}

     由于接口定义在StartupToolsetInterface程序集,我们必须使用using关键字指定名字空间。为了在外部能够创建这个服务,必须同时实现服务接口和标记接口。如果你去掉标记接口的继承,代码能够通过编译,但是无法创建服务对象。标记接口没有任何方法需要实现,我们只要实现服务接口的Calculate方法即可。可以从CalculationControlCalculate_Click方法中复制代码过来,但需要做一些改动:

public bool Calculate(string firstArgText, string secondArgText, string operatorText, out string resultText)

{

  try

  {

    int firstArg = Int32.Parse(firstArgText);

    int secondArg = Int32.Parse(secondArgText);

    int result = 0;

    switch (operatorText)

    {

      case "+":

        result = firstArg + secondArg;

        break;

       case "-":

         result = firstArg - secondArg;

         break;

       case "*":

         result = firstArg * secondArg;

         break;

       case "/":

         result = firstArg / secondArg;

         break;

       case "%":

         result = firstArg % secondArg;

         break;

    }

    resultText = result.ToString();

  }

  catch (SystemException)

  {

    resultText = "#Error";

    return false;

  }

  return true;

}

     现在可以修改Calculate_Click方法了:

private void Calculate_Click(object sender, EventArgs e)

{

    ICalculationService calcService = new CalculationService();

    string result;

    calcService.Calculate(FirstArgEdit.Text, SecondArgEdit.Text,

                          OperatorCombo.Text, out result);

    ResultEdit.Text = result;

    LogCalculation(FirstArgEdit.Text, SecondArgEdit.Text, OperatorCombo.Text,ResultEdit.Text);

}

     如果你运行StartupToolsetRefactored,并做一些计算,它能够正常工作。这样就够了吗?不,还没有。目前,我们能够得到服务对象,并使用服务。但是我们如何让VS IDE知道这个服务的存在,以便其他服务调用呢?

公开服务(Proffering the service)

     在我们着手将服务公开以便其他package使用前,必须来看一下在VS IDE中服务的基础架构。在第5部分我介绍了VS IDE中服务的基本概念,现在我们深入一些。

如果任何一个对象想要获得服务的实例,它必须向服务提供者“对话”。实现了IServiceProvider接口的类便是一个服务提供者;其中便包含了一个GetService方法:

        public interface IServiceProvider

        {

            object GetService(Type serviceType);

        }

     如果一个服务提供者预先静态地实现了一些服务,那么这些服务能够被服务提供者方便的定位和返回。VS IDE本身就是一个服务提供者。不仅如此,当package安装并向IDE公开他们的某些服务时,VS IDE能够动态的增加服务对象。这要依赖于一个叫做“服务容器(service container)”的接口IServiceContainer,而这个接口本身继承自IServiceProvider

        public interface IServiceContainer: IServiceProvider

        {

            void AddService(...); // --- Overloaded

            void RemoveService(...); // --- Overloaded

        }

     AddServiceRemoveService方法提供了我们期望的容器的功能。每个VSPackage本身都是服务容器(当然也是服务提供者),原因是Package类实现了IServiceContainer接口。

     一个服务容器并非平面的结构,它也可以有父容器。当我们向一个容器添加一个服务时,可以指定这个服务的父容器。这便是VS IDE用来检索全局服务的机制基础。有一个叫做SProfferService的VS IDE服务负责检索公开的全局服务。MPF没有公开SProfferService。不过我们一般需要继承Package,所以很少涉及它。

     现在,我们该如何向IDE公开我们的CalculationService服务!我们必须按照如下步骤进行:

     1.需要一个创建服务对象的方法

     2.声明我们的Package公开了这个服务类型

     3.添加初始化代码创建服务对象

第一步:用于创建服务对象的方法

     服务对象只被创建一次,这个实例为所有的客户服务。我们可以在package初始化的时候实例化这个服务,或者在第一次需要使用这个服务的时候实例化。

这里我们选择第二种方式,因此我们需要一个创建服务实例的回调函数。在package类中添加一个CreateService

        private object CreateService(IServiceContainer container, Type serviceType)

        {

            if (container != this)

            {

                return null;

            }

            if (typeof(SCalculationService) == serviceType)

            {

                return new CalculationService();

            }

            return null;

        }

     这个回调函数有两个参数:container是需要实例化这个服务的容器对象,serviceType是服务类型。这个函数必须返回服务的实例,如果不能成功创建则需要返回null。在这个实现中我们只允许package本身这个容器来创建SCalculationService服务的实例。

 

第二步:声明这个服务是公开的

     就像声明菜单项和tool window一样,我们需要用一个属性来声明公开这个服务:

    [ProvideService(typeof(SCalculationService ))]

    public sealed class StartupToolsetPackage : Package

     ProvideService属性为我们完成这个任务。这个属性使regpkg.exe注册我们的服务,使得package得以按需加载(package只会在公开的服务第一次被调用的加载)。

     每个服务都可以将服务的名字传递给这个属性的ServiceName属性值。

第三步:初始化代码

     我们的package通过ProvideService属性公开了自己的服务,但为了是服务能被创建,也需要一些初始化的代码。最好的放置这些代码的地方便是构造函数:

        public StartupToolsetRefactoredPackage()

        {

            IServiceContainer serviceContainer = this;

            ServiceCreatorCallback creationCallback = CreateService;

            serviceContainer.AddService(typeof(SCalculationService), creationCallback, true);

        }

     Package类已经显示实现了IServiceContainer接口,所以我们不得不将package转化成IServiceContainer接口以调用AddService方法。这个方法有多个重载形式。我们用其中一个带3个参数的形式,这3个参数分别是:想要被添加的服务的类型;一个只在第一次调用服务才会执行创建工作的回调函数;一个标记值指示是否将服务添加给父容器,这里我们设置为true,使得我们的服务能被全局使用。

调用(Consuming)这个服务

     现在所有其他的package都可以以松耦合的方式调用我们的服务。目前我们是直接在Calculate_Click方法中使用这个服务的:

ICalculationService calcService = new CalculationService();

string result;

calcService.Calculate(FirstArgEdit.Text, SecondArgEdit.Text,

                                  OperatorCombo.Text, out result);

     修改一下代码,从IDE中获取这个服务:

       private void Calculate_Click(object sender, EventArgs e)

        {

            ICalculationService calcService = Package.GetGlobalService(typeof(SCalculationService)) as ICalculationService;

            if (calcService != null)

            {

                string result;

                calcService.Calculate(FirstArgEdit.Text, SecondArgEdit.Text,

                                      OperatorCombo.Text, out result);

                ResultEdit.Text = result;

            }

        }

 

一些调试

     目前,我们重构的package使用了我们创建的服务。我建议你对代码做一些临时的改动,看看改动过后会发生什么。

     为了查看调试的结果,我建议在Calculate_Click的最后加上LogCalculation方法,以便调试信息输出到output window中:

       private void Calculate_Click(object sender, EventArgs e)

        {

            …

 

            LogCalculation(FirstArgEdit.Text, SecondArgEdit.Text, OperatorCombo.Text,ResultEdit.Text);

        }

     接下来修改一下代码,这些改动将使得服务不可用:当需要返回服务对象时,我们只能得到null,不会有任何错误信息,但是观察output窗口,我们可以察觉到服务没有正常工作,因为没有完成计算。例如,如果我们看到output窗口中输出的是“1 + 2 =”,而不是期望的“1 + 2 = 3”,那么可以断定服务使用失败。

     我将介绍一些在开发package工程中很常见的疏忽,你可以尝试这样做,看看结果。

     请做下面这些调试(最后请恢复):

调试1:删除CalculationService类对SCalculationService标记接口的继承

public sealed class CalculationService : ICalculationService

        //, SCalculationService

{

}

     编译可以通得过,但是服务对象不会被创建。因为GetService参数接受的类型不能转化成期望的类型。在这个例子中GetService(typeof(SCalculationService))调用了,但是CalculationService不能转化成SCalculationService

调试2:将AddService方法的最后一个参数从true改成false:

        public StartupToolsetPackage()

        {

            IServiceContainer serviceContainer = this;

            ServiceCreatorCallback creationCallback = CreateService;

            serviceContainer.AddService(typeof(SCalculationService), creationCallback, false);

        }

     我们无法获得这个服务的实例,因为Package.GetGlobalService方法试图找一个向VS IDE公开的服务时失败了。定义成false将不能把CalculationService服务向VS IDE公开。

在本地调用服务

     到目前为止,我们将这个服务视为外部的公开的服务,通过全局的方法Package.GetGlobalService来调用的。事实上,我们可以使用GetService方法:

ICalculationService calcService = GetService(typeof(SCalculationService)) as ICalculationService;

     代码不仅通过了编译,运行也完全正常!这里的GetService是哪里来的?CalculateControl和package并没有直接的联系,只是继承自UserControl,而UserControl继承自System.ComponentModel.Component,这个类实现了IServiceProvider接口。别忘了,这个接口定义了GetService方法!但是属于UserControl的GetService怎么知道我们的package提供了这个服务呢?CalculateControl和package没有直接的联系啊!

     这是由于VS IDE的挂载(siting)机制。我们的package被载入到IDE的同时,被挂载并获得了父容器的IServiceProvider(由siting对象提供)。当我们的CalculateControl连带tool window被载入到内存的时候,这个Control也通过tool window被挂载,于是也获得了父容器的IServiceProvider。在UserControl中也实现了GetService,于是开始遍历它父容器IServiceProvider的服务对象,寻找所要的服务,最终找到了我们package的GetService方法,并成功返回了这个服务。这样的方式是一种本地的服务调用。你可以尝试注释掉ProvideService属性对package的声明的代码,当你编译运行的时候,依旧正常,不过这样便不能全局飞访问这个服务。

总结

     在之前的StartupToolset包中,计算的逻辑是直接写在与tool window对应的用户控件里面的。本文我们创建了一个全局的服务,将业务逻辑提独立成一个服务。

     我们创建了两个接口类型,放在独立的程序集中:

     1.服务接口定义了服务的契约。

     2.标记接口用于GetService方法的参数。

     在重构的package中我们添加了一个实现服务接口和标记接口的服务类型。详细描述了如何用现有的机制公开这个服务,使得服务能够被其他的package全局的访问。我们的服务是在第一次使用的时候创建的。

     我们还探索了如何以全局方式和本地方式访问服务。

     在下一篇文章中,为了使我们的代码能够被复用,我们将再次重构我们的package。

原文地址:http://dotneteers.net/blogs/divedeeper/archive/2008/01/31/LearnVSXNow9.aspx

原文地址:https://www.cnblogs.com/P_Chou/p/1680530.html