在第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方法即可。可以从CalculationControl的 Calculate_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
}
AddService和RemoveService方法提供了我们期望的容器的功能。每个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