LearnVSXNow! #9 - 創建我們第一個工具集-重構爲服務

LearnVSXNow! #9 - 創建我們第一個工具集-重構爲服務

     在第6篇和第7篇裏,我們創建了一個名爲StartupToolset的示例package,並且手動地添加了一個菜單項和工具窗。在這篇文章裏,我們將重構這個package,提取獨立的服務模塊出來。 

     我們這個示例package有很多地方可以重構:不僅可以做提取服務之類的結構調整,也可以封裝可重用的代碼,以便供以後調用或提高代碼可讀性。在下一篇文章裏我們將封裝可重用的代碼,但在這一篇裏,我們把精力放在服務上。

複製一份StartupToolset

     爲了在重構之前保留目前的StartupToolset的版本,我把這個package複製了一份,並命名爲StartupToolsetRefactored。你可以參考第6篇和第7篇的內容自己來做一個副本:新建一個空的名爲StartupToolsRefactored的package,並且根據第6篇的內容爲它添加一個菜單項,根據第7篇的內容添加一個工具窗。 

     爲了避免和前一個package衝突,要修改一下StartupToolsRefactored裏的GUID,並且修改一下菜單命令的顯示文本,這樣就可以在界面上和舊版的package區分開來。 

創建一個全局服務(global service)

     在重構的第一步,我們將把“計算引擎”做成一個全局服務。這樣的話別的package就可以調用我們這個服務的功能了。 

     到目前爲止,“計算”的邏輯是直接嵌入到我們的工具窗的用戶控件CaculationControl類裏的。這段邏輯放在了CalculateButton_Click事件處理方法裏,這樣我們的代碼看起來就非常簡單並且容易懂。但是在這種結構下,計算邏輯和我們的package是緊耦合的:

public partial class CalculationControl : UserControl
{
  ...
  private void CalculateButton_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) { ... }
    ...
  }
}

     最適合的改進方案是把這段計算邏輯放到一個獨立的服務對象裏。如果我們把這個服務對象做成一個全局的VSX服務的話,不僅我們的CalculationControl控件可以使用它,其他的package也一樣可以使用它。OK,就這樣做!

創建服務接口

     每一個服務都必須至少提供一個接口來作爲服務的“契約”,所以,不必驚訝,我們要創建接口(譯者注:從技術上來講,服務不一定非得需要接口,這一點我在這篇譯文的後面會做些測試代碼來說明)我們可以把接口定義在我們的package程序集裏,但是,別的package要想用這個服務的話,就不得不引用我們的整個package:我們通常不想這麼做。

     所以,我們用老配方:創建一個的單獨的程序集來放置服務。這樣我們的package和其他的package都可以引用它。

     創建一個名爲StartupToolsetInterfaces的類庫項目,並在StartupToolsetRefactored項目裏引用它。刪除掉默認的Class1.cs文件,並添加一個CalculationService.cs文件。

     如果你還記得我們在前面的例子中是怎樣訪問到全局服務的話,你一定會想起來GetService方法:

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

     爲得到uiShell這個服務對象,我們用到了兩個類型:IVsUIShell是定義服務的接口;SVsUIShell是所謂的標記類型(markup type),用它來標識服務對象。你可能會問,我們爲什麼要用兩個類型?只用一個接口類型不就夠了嗎?是的,一個接口類型就夠了,但用兩個類型可以提高靈活性:一個服務對象可以實現一個或多個接口,一個接口也可以被一個或多個服務對象實現(譯者注:例如你有一堆的服務都是IXXXService類型的,但每個服務的具體實現有所不同,你就可以定義若干個標記類型來區分這些不同的服務)。用兩個類型的話,我們即可以給服務對象起名(如SVsUIShell),也可以爲服務接口起名(如IVsUIShell)。GetService的參數可以是實現了服務的類型,但也不一定非得這樣。實際上,我們可以傳任何類型給它,這個參數只是作爲一個key來標識一個服務對象。

     標記類型(markup type)不包含任何功能,它們僅僅用來標記一個類型,以區分其他類型。

     我們也按照這種模式來做,在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。另外,服務接口必須定義爲ComVisible,這樣非託管代碼就可以檢索到它。

創建服務類

     我們把實現計算邏輯的服務實現類定義在我們的package裏(不在StartupToolsetInterfaces類庫項目裏)。在StartupToolsetRefactored項目裏,添加一個CalculationService.cs文件(此CalculationService.cs文件非彼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它們的命名空間。爲了能正常的創建我們的服務,服務類必須既實現服務接口,又實現標記接口(markup type)。如果你沒有實現標記接口,編譯是沒問題的,但這個服務對象實例是不會被創建的。由於標記類型實際上不包含任何方法,所以我們只需要實現Calculate方法就可以了。這個方法的實現可以從CalculationControl控件的CalculateButton_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;
      ...
    }
    resultText = result.ToString();
  }
  catch (SystemException)
  {
    resultText = "#Error";
    return false;
  }
  return true;
}

     現在讓我們修改一下CalculateButton_Click方法,來用這個service:

public partial class CalculationControl : UserControl
{
  ...
  private void CalculateButton_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項目,並且試一下Calculate工具窗,你會發現它能夠正常工作。這樣就夠了嗎?不,還不夠。現在我們有了服務對象,並且應用了它,但我們還需要告訴VS IDE這個服務的存在,這樣別的package才能用它!

提供服務

     在我們使我們的服務可見和可用之前,我們先來看一下VS IDE中服務體系的機制。在第5篇中,我講了一下VS IDE中服務的基本概念,這一次讓我們深入一些。

     任何一個對象如果想調用一個服務的話,它必須要和service provider“對話”。service provider實現了IServiceProvider接口,幷包含GetService方法:

public interface IServiceProvider
{
  object GetService(Type serviceType);
}

     很容易想象出來:一個service provider包含了一個預定義的服務集合。VS IDE本身就是一個service provider,然而,VS IDE可以動態的處理服務對象,因爲已安裝的package可以提供它們自己的服務給IDE。所以,還應該有一個service container,service container實現了IServiceContainer接口,該接口繼承自IServiceProvider

public interface IServiceContainer: IServiceProvider
{
  void AddService(...); // --- Overloaded
  void RemoveService(...); // --- Overloaded
}

     AddServiceRemoveService方法提供了我們所期望的service container的功能。一個VSPackage本身就是一個service container(當然也是一個service provider),因爲Package類實現了IServiceContainer

     service container並不是一個平面的東東,它可能包含parent container。當添加或移除一個服務的時候,我們可以把這個服務傳給它的parent container,VS IDE就是用這種結構來管理全局服務的。另外,VS IDE用SProfferService服務來管理全局服務,不過MPF幫我們屏蔽了SProfferService:如果我們的package繼承自Package基類的話,我們很少會用到它。

     好了,讓我們看看怎樣才能把CalculationService提供給VS IDE。我們需要做下面的幾步:

  1. 第一步:需要一個方法,該方法負責創建相應類型的服務對象。
  2. 第二步:在package上註明該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本身的container,並且只能創建SCalculationService類型的服務。

第二步:聲明能提供的服務

     就像菜單命令和工具窗那樣,我們必須在package那裏附加一個attribute,以聲明該package能提供的服務:

[ProvideService(typeof(SCalculationService))] 
public sealed class StartupToolsetRefactoredPackage : Package { ... }

      ProvideService屬性的作用是:regpkg.exe利用這個attribute去註冊我們的服務,並使我們的package能夠按需加載(在第一次調用package的服務的時候,如果package沒有加載,則加載package)。

     每個服務默認以類型的名字作爲服務名,當然也可以通過設置這個attribute的ServiceName屬性來更改服務名。

第三步:添加初始化代碼

     我們的package通過ProvideServiceAttribute使外面的事件知道它的服務的存在,但是爲了服務實例能被創建,我們還得添加一些初始化代碼纔行。這段代碼最好放在package的構造函數裏:

public sealed class StartupToolsetRefactoredPackage : Package
{
  public StartupToolsetRefactoredPackage()
  {
    IServiceContainer serviceContainer = this;
    ServiceCreatorCallback creationCallback = CreateService;
    serviceContainer.AddService(typeof(SCalculationService), 
      creationCallback, true);
  }
  ...
}

     Package類顯示地實現了IServiceContainer接口,是沒有公開的AddService方法的,所以我們必須把this轉換成IServiceContainer類型的對象。AddService方法有很多重載,我們用其中的接受3個參數的那個:要添加的服務的類型、當服務第一次調用時會被調用的回調方法、以及是否把這個服務傳遞給parent container的標記。我們把最後一個參數設成true,這樣就可以確保我們的服務可以被全局訪問。

使用服務

    現在,所有其他package都可以用松耦合的方式來使用我們的計算服務了。但是我們在CalculationButton_Click方法裏是直接實例化它的:

ICalculationService calcService = new CalculationService();
string result;
calcService.Calculate(FirstArgEdit.Text, SecondArgEdit.Text,
                      OperatorCombo.Text, out result);

    我們最好修改一下它,以便從IDE裏得到服務實例:

private void CalculateButton_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已經使用了我們創建的服務了。接下來,我建議你對代碼做些臨時的改動,並看看我們的package會有什麼變化。

     爲了能夠清楚地看到這些變化,我建議你在CalculateButton_Click方法的最下面調用LogCalculationToOutput方法,這樣就可以看到我們的package在執行的時候輸出來的調試信息:

private void CalculateButton_Click(object sender, EventArgs e)
{
  ...
  LogCalculationToOutput(FirstArgEdit.Text, SecondArgEdit.Text, 
    OperatorCombo.Text, ResultEdit.Text);
}

     我們將對代碼做些小的改動,並且每次改動都會使我們的服務不可用:當我們需要得到這個服務的實例的時候,我們只能得到空引用。在這個過程中不會有任何錯誤提示,但是在output窗口裏,我們可以發現這個服務不會正常工作。例如,如果我們想計算“1+2”,我們期待在output窗口中能看到“1 + 2 = 3”,但是我們只能看到“1 + 2 = ”。

     我強烈建議你做這一下這些改動,並檢查改動後的結果,因爲服務開發者經常會犯類似的錯誤,並且不知道錯在哪了。所以,求你了,做一下下面的試驗(每次試驗完要記得“undo”這一次的修改)。

試驗1:在CalculationService類聲明那裏,註釋掉對SCalculationService接口的實現

public sealed class CalculationService: ICalculationService 
  // , SCalculationService
{
  public bool Calculate(string firstArgText, string secondArgText, 
    string operatorText, out string resultText)
  { ... }
}

     package照樣可以編譯通過,但是這個服務對象是沒法被創建的。因爲當我們調用GetService方法的時候,這個方法認爲返回的服務對象能夠轉換成參數裏指定的類型。在我們的例子中我們是通過GetService(typeof(SCalculationService))調用的,但返回的CalculationService類的實例是不能夠轉換成SCalculationService類型的,因爲它並沒有實現SCalculationService接口。

試驗2:在調用AddService方法時,把最後一個參數從true改成false

public StartupToolsetRefactoredPackage()
{
  IServiceContainer serviceContainer = this;
  ServiceCreatorCallback creationCallback = CreateService;
  serviceContainer.AddService(typeof(SCalculationService), creationCallback,
    false);
}

     這樣改後,我們也得不到服務的實例了,這是因爲Package.GetGlobalService方法找的是所有公開給VS IDE的服務,但是我們把AddService的最後一個參數改成false之後,我們的服務就不是公開的了。

用本地的方式使用服務

     到目前爲止我們都是通過調用Package.GetGlobalService方法來得到服務實例的,看起來像是這個服務是別的package而不是我們的package提供的。其實,我們可以用GetService方法:

private void CalculateButton_Click(object sender, EventArgs e)
{
  ICalculationService calcService = 
    GetService(typeof (SCalculationService)) as ICalculationService;
  ...
}

     這樣改動後,我們的package照樣運行正常!但是這個GetService方法是從哪裏來的呢?CalculateControl用戶控件和我們的package沒有直接的聯繫,它繼承自UserControl類,UserControl又繼承自System.ComponentModel.Component,而這個類實現了IServiceProvider接口,還記得不,這個接口定義了GetService方法!但是,屬於用戶控件的GetService方法是怎麼知道我們的package會提供這個服務的?我們並沒有在這個用戶控件裏直接引用package啊。

     原因就是VS IDE的Siting機制。當我們的package加載到IDE的時候,它被site了,並且得到了一個parent IServiceProvider;當我們的工具窗裏的用戶控件加載到內存的時候,這個控件也被site到工具窗中,所以也會有一個parent IServiceProvider,這兩個service provider是同一個對象。用戶控件的GetService方法在執行的時候,會查找整個IServiceProvider鏈。在這個鏈中,它會調用到我們的package的GetService方法並最終得到這個服務對象。這是一種本地訪問服務的方式。如果註釋掉package上附加的ProvideService屬性的話(譯者注:僅註釋掉是不夠的,要卸載package然後再註冊),我們的package也可以正常運行,但是這個服務就不再是一個全局服務了,別的package不一定能夠再使用它。(譯者注:在別的package請求這個服務時,無法知道這個服務在哪個package內,所以也就無法使用這個服務,但是如果我們的package已經加載了,那麼別的package依然可以得到這個服務,因爲在我們package的構造函數裏,我們把這個服務加到了parent service container裏)

總結

     原來的StartupToolset裏的計算邏輯是耦合在工具窗的用戶控件裏的,在這篇文章裏,我們把這段邏輯抽了出來做成了一個全局服務。爲創建這個服務,我們在一個單獨的程序集裏添加了兩個接口:

  1. 服務接口聲明瞭服務的功能(契約)。
  2. 標記類型(無成員的接口)被用作GetService的參數。

     在package項目中,我們添加了一個服務實現類,實現了服務接口和標記接口,並探討了服務的機制和使服務能被全局訪問的步驟。我們的服務實例在第一次被請求時纔會創建。另外,我們還知道了怎樣以全局和本地的方法來訪問服務。

     在下一篇裏,我們繼續重構這個package,並創建可重用的代碼。

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

 

     到這裏這篇譯文就已經結束了,但我還想再多說明一些東西:

1。服務一定需要定義成接口嗎?

     如果單單從技術上來看,服務不一定非得需要接口。爲了說明這一點,我們在StartupToolsetInterfaces項目裏添加一個MyServiceClass.cs文件,並添加如下代碼:

public class MyServiceClass
{
    public int Caculate(int i)
    {
        return i*i;
    }
}

     然後用這篇譯文裏的方法添加ProvideService、回調方法,並在package的構造函數裏調用AddService。然後,新建一個帶菜單項的package,並添加對StartupToolsetInterfaces的引用,然後在菜單項的事件處理方法裏,添加如下代碼:

MyServiceClass myService = GetService(typeof(MyServiceClass)) as MyServiceClass;
if (myService != null)
{
    MessageBox.Show(myService.Caculate(3).ToString());
}
     運行起來後,點擊這個package的菜單,是不是彈出了一個消息框,並顯示9?所以,服務不一定非得用接口,但用接口會更好,可以使結構更好,又或者可以使非託管代碼可以訪問這個服務(我並沒有驗證過)。

2。服務的GUID是幹什麼用的?

     在上面這個示例服務MyService裏,我們並沒有給他加GUID,但原文作者給出的例子卻加了GUID,那麼這個GUID是幹嘛用的呢?其實,GUID無非就是標識這個服務而已。然後打開註冊表,在HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\9.0Exp\Configuration\Services下面就可以找到這個GUID,是ProvideService這個attribute指定的SCalculationService接口的GUID。如果沒有給它加GUID,regpkg在註冊的時候,會自動產生一個GUID,所以,一般情況下也不用給服務指定GUID。

     但在某些情況下,這個GUID還是有用的。比如由於某種原因,我們的package不能夠引用StartupToolsetInterfaces項目,但是在package裏又想用它的service,我們就可以在package項目里加一個接口或類(該接口或類可以是空的),然後給他一個GUID,GUID的值是StartupToolsetInterfaces裏的SCalculationService的GUID:

[Guid("AF7F72EF-2B54-4798-B76A-21DC02CC04B7")]
class MyService
{
 //空的   
}

     然後把自己定義的這個接口類型傳給GetService方法,這樣就照樣可以得到這個服務的實例(是一個object類型的),然後通過反射來調用它的方法了。

object service = GetService(typeof(MyService));
if(service != null)
{
    //反射調用其方法
}

     當然,通過反射來調用看起來很怪,應該有其他方式可以用“強類型”的方式使用這個服務,例如像使用COM對象那樣,定義interop類型,但我缺少這方面的知識,所以沒有去驗證怎樣使用。

作者:明年我18
出處:http://www.cnblogs.com/default
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,否則保留追究法律責任的權利。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章