Dotneteers.net
All for .net, .net for all!

LearnVSXNow! #9 - Creating our first toolset — Refactoring to a service

 

In Part #6 and Part #7 we created StartupToolset sample package with a manually added menu command item and a Calculate tool window. In this article we are going to refactor our package to use a service as a separate architectural building block.

Not only architectural kind of refactoring can be done with our sample. We also can extract reusable code that can be used in package development later and the source gets much easier-to-read. That is what we are going to do in the next post, but this time we focus on services.

Create a copy of the StartupToolset package

In order to conserve the StartupToolset package sample in its state before refactoring, I have created a clone of this package called StartupToolsetRefactored. Having to knowledge obtained in Part #6 and #7 you can do it by yourself. Create an empty package with the name of StartupToolsRefactored and repeat the steps in Part #6 to add the menu command, steps in Part #7 to add the tool window.

I did the same and modified the GUIDs to avoid collisions with the previous project. I also modified the menu command text and the tool window caption in order to distinguish between the old and refactored UI elements.

Creating a global service

As the first refactoring step we are going to extract the “calculation engine” as a global service. We do it in order to allow external packages to leverage the functions already implemented in our calculation package.

Right now the calculation logic is embedded into our tool window’s UI represented by the CaculationControl class. This logic is coded into the CalculateButton_Click event handler that makes our sample quite easy to read and understand, but the architecture is monolithic:

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) { ... }

    ...

  }

}

The solution best suiting with our architecture requirements would be to extract this calculation logic into a separate service object. If we make this service object as a global VSX service, not only CalculationControl UI will access it but other packages also can consume the methods it provides. Ok, let’s do it!

Creating the service interface

Each service must provide at least one interface as the public “contract” for the service. This is — no surprise — implemented as an interface type. We could enclose the interface into our package’s assembly. However, in this case we must provide a reference for out package’s assembly, and generally we want to avoid it.

So, we apply the old recipe: we create a separate assembly that holds the façade for our service. We publish this assembly for external packages and also reference it from our package’s assembly.

Let’s create a class library with the name StartupToolsetInterfaces and add it to the StartupToolsetRefactored VSPackage project as a reference. Let’s delete the default Class1.cs file and add a new code file with the name of CalculationService.cs.

If you remember how we accessed global service in our previous samples, you recall to the GetService method:

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

To obtain a service object we used two types: IVsUIShell is the interface declaring the service; SVsUIShell is a so-called markup type that is used to identify the service object. You may ask why we use two types. Using only the interface type is not enough? Yes, using one type is a pattern that also could be used. Using two types provides flexibility: one service object can implement one or more interfaces, or an interface can be provided by one or more service objects. Using two types allows us to name the service object and name its service interface. The argument of GetService can be the type that implements the requested service but it is not necessary. Actually we can pass any type it will only be used as a key to provide a return a service object.

Visual Studio packages use so-called markup types that do not encapsulate functionality; they only mark a type that differs from the existing ones.

We follow this pattern and in the CalculationService.cs file we are going to create two interface types: a service interface and a markup interface:

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

  {

  }

}

By convention we prefix the service interface with “I” and the markup interface with “S”. They must be able to be identified by COM and it is done by their Guid. The service interface must be defined as COM visible in order it will be possible for query for it.

Creating the service type

We create the object type representing the calculation service in our package. Let’s create a new CalculationService.cs file in StartupToolsetRefactored project. The frame of our service class is the following:

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)

    { ... }

  }

}

Since the interface definitions are in the StartupToolsetInterface assembly we have to add a using clause for its namespace. In order out service could be created, it must implement both the service interface and the markup interface. If you try and omit the markup interface, the code will compile, but the service object instance will not be created. Before the markup interface does not have any methods, we need to implement only the Calculate method of the service interface. The implementation of the method can be copied out from the CalculationControl’s CalculateButton_Click method and should be slightly modified:

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;

}

Now we can change the CalculateButton_Click method to use or 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);

  }

  ...

}

If you run the StartupToolsetRefactored package and try the Calculate tool window, it works just like before. So we are ready? No, not yet. Now we have the service object, we can use it, but somehow we should tell the VS IDE that it exists in order other services could use it!

Proffering the service

Before we can make our service visible and usable for other packages, we have to take a look at the service infrastructure in VS IDE. In Part #5 I treated the basic idea of a service in VS IDE, this time we take a deeper dive.

If any object wants to obtain a service in order to leverage its methods, it must “talk” with a service provider. A provider is represented by the IServiceProvider interface; that is where GetService comes from:

public interface IServiceProvider

{

  object GetService(Type serviceType);

}

It is quite easy to image that an object can be a service provider for a set of predefined services. The VS IDE itself is a service provider. However, the VS IDE can dynamically handle service objects, since packages installed can proffer their services to the IDE. This is an extended role called “service container” and is represented by the IServiceContainer interface that itself derives from IServiceProvider:

public interface IServiceContainer: IServiceProvider

{

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

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

}

The AddService and RemoveService methods provide the functionality we expect from a service container. A VSPackage itself is a service container (and so a service provider), since the Package class implements IServiceContainer.

The service container is not a flat “creature” it may have a parent container. When adding or removing a service we can promote the service to the parent of the container. This is the mechanism VS IDE uses to handle global services. There is a VS IDE service called SProfferService that is responsible for handling globally proffered services. The MPF hides SProfferService from us. While we derive our packages from the Package class we rarely need to use it.

Now, let’s see how we can proffer our CalculationService to VS IDE! We have to do the following steps:

—  Step 1: We need a method to create the service object from its service type.

—  Step 2: We decorate the package to proffer the service type.

—  Step 3: We add initialization code for service object creation.

Step 1: Service object creation method

Service objects are created once and the same instance serves all the clients. We can instantiate this service object at package initialization time or postpone it to the time when the first client requests for the service instance.

In this case we are going to use the second approach and so we need a service object creation callback method. We need in our package class, name it to CreateService:

private object CreateService(IServiceContainer container, Type serviceType)

{

  if (container != this)

  {

    return null;

  }

  if (typeof(SCalculationService) == serviceType)

  {

    return new CalculationService();

  }

  return null;

}

This callback method takes two input arguments: container is a reference to the container requesting the service creation and serviceType is type of the requested service. This method must return the service instance if it can be created; otherwise it must return a null reference. In this implementation we accept only the package itself as the service container and we can instantiate only the SCalculationService type.

Step 2: Declaring the service to proffer

Just as in case of menu commands and tool windows we must decorate our package with an attribute to declare proffered services:

[ProvideService(typeof(SCalculationService))]

public sealed class StartupToolsetRefactoredPackage : Package { ... }

It is the role of ProvideService attribute to make it for us. This attribute causes regpkg.exe to register our service supporting the on-demand loading of our package (the package will be loaded only the first time when the proffered service is to be consumed).

Each service has a name that is the type name by default; it can be set with the ServiceName property of the attribute.

Step 3: Initialization code

Our package lets the external world know about its service by the ProvideService attribute, but we also must set up some initialization code in order the service instance to be created. This code’s better location is the constructor of our package:

public sealed class StartupToolsetRefactoredPackage : Package

{

  public StartupToolsetRefactoredPackage()

  {

    IServiceContainer serviceContainer = this;

    ServiceCreatorCallback creationCallback = CreateService;

    serviceContainer.AddService(typeof(SCalculationService),

      creationCallback, true);

  }

  ...

}

The package class implements the IServiceContainer explicitly and so we have to cast this package to access the AddService method. This has several overloads. We use the one that accepts three parameters:  the type of service to be added to the container, a callback method that is called first time when the service is requested; and a flag marking the service is to be promoted to the parent container. We set the last parameter to true and that ensures our service can be accessed globally.

Consuming the service

Now all other packages are able to use our calculation service in a loosely-coupled way. Right now we use it by directly instantiating the service class in the CalculationButton_Click method:

ICalculationService calcService = new CalculationService();

string result;

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

                      OperatorCombo.Text, out result);

We’ better change it to let the service instance obtained from the 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;

  }

}

A few checks

At this point our refactored package is working using the service we created. I suggest you to make some temporary changes and try how our package responds to those changes.

To easily visualize the effect of changes, I suggest you to put back the LogCalculationToOutput method call to the end of the CalculateButton_Click method to provide a diagnostic message about the calculation:

private void CalculateButton_Click(object sender, EventArgs e)

{

  ...

  LogCalculationToOutput(FirstArgEdit.Text, SecondArgEdit.Text,

    OperatorCombo.Text, ResultEdit.Text);

}

We are make small modifications to our code. Any of them makes our service unavailable: when we are requesting the service object, we obtain no (null) references. We do not get any error message, but in the output window we can recognize our service is not working, because it does not execute calculation. For example, if we check the output window for “1+2” we expect the “1 + 2 = 3” message but instead we get “1 + 2 =”.

I recommend you to try these changes and examine their result, because these are common mistakes service developers can make and wondering why their services do not work.

Please, make the following checks (and “undo” the changes after each check):

Check 1: Omit the SCalculationService markup interface from the base interface list of the CalculationService class:

  public sealed class CalculationService: ICalculationService

    // , SCalculationService

  {

    public bool Calculate(string firstArgText, string secondArgText,

      string operatorText, out string resultText)

    { ... }

  }

Our package will compile but the service object will not be created. If we use GetService it expects that the resulting service object can be casted to the argument service type. In our example we use the GetService(typeof(SCalculationService)) call but the resulting CalculationService instance cannot be casted to SCalculationService due to our modification.

Check 2: Change the last parameter of the AddService call in the package initialization code from true to false:

public StartupToolsetRefactoredPackage()

{

  IServiceContainer serviceContainer = this;

  ServiceCreatorCallback creationCallback = CreateService;

  serviceContainer.AddService(typeof(SCalculationService), creationCallback,

    false);

}

We cannot get the service instance because the Package.GetGlobalService call tries to find a service proffered to the VS IDE. Using the false flag in the AddService call prevents the CalculationService to be promoted to VS IDE.

Consuming the service locally

Till this time we obtained the service object by the Package.GetGlobalService method as if it were proffered by another package and not our own. However, we could use the GetService method:

private void CalculateButton_Click(object sender, EventArgs e)

{

  ICalculationService calcService =

    GetService(typeof (SCalculationService)) as ICalculationService;

  ...

}

Our code not just simply compiles but also runs as we expect! Where does this GetService method come from? Our CalculateControl has no direct relation with our package. It derives from UserControl which indirectly derives from System.ComponentModel.Component and that class implements the IServiceProvider interface. Remember, that interface defines the GetService method! But how the GetService method that belongs to the user control knows our package that provides the service? We did not have any direct reference in the user control to our package!

The solution is the siting mechanism of VS IDE. When our package is loaded into the IDE, it is sited and it gets a reference to the parent IServiceProvider (provided by the siting object) When the user control representing our tool windows user interface is loaded into the memory, this control is also sites through the tool window and so has a parent IServiceProvider. The GetService method of our user control is implemented so that it looks for service object traversing this IServiceProvider chain. In this chain it reaches our package’s GetService method and so gets the requested service object. This kind of service access is a local service access. You can check the “locality” by commenting out the ProvideService attribute of the package. When you compile and run the project, our code will work as expected. However, in this case the service cannot be accessed globally.

Where we are?

In the original StartupToolset package the calculation logic was the part of the user control representing the tool window’s user interface. In this article we created a global service and extracted the “business logic” behind the tool window into a separate service type.

We created two interface types and put them into a separate assembly:

—  The service interface defining the responsibilities (contract) of the service.

—  A markup interface (no members) to be used as the argument of the GetService call.

In the refactored package we developed a service type implementing both the service interface and the markup interface.

We treated the mechanism used to proffer services and detailed the steps to make a service global and so accessible for other packages. Our service implementation allowed on-first-load service object creation.

We looked how to consume this service globally and also locally.

In the next article we are going to refactor our package in order to create reusable code.


Posted Jan 31 2008, 06:42 PM by inovak
Filed under:

Add a Comment

(required)  
(optional)
(required)  
Remember Me?