• (翻译)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

  • 相关阅读:
    TDSCDMA手机(WM系统)信号的采集?
    vc2008 + libpq + postgresql 8.4 配置
    code complete 2阅读笔记(第二章)
    Python 学习笔记(一)语句,变量,函数
    CS通用模型设计,socket,tcp实现()
    VS2005,VS2008编辑器设置
    设计模式之个人理解单例模式
    请教:C#网络编程相关的知识,建立socket服务器时向客户端连接,就建立不了了?
    服务器开发
    年终总结
  • 原文地址:https://www.cnblogs.com/P_Chou/p/1680530.html
Copyright © 2020-2023  润新知