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

LearnVSXNow! #19 - PowerCommands Deep Dive — Command Architecture

In the last week I was at MVG Global Summit 2008 held in Seattle and Redmond. I had fun there, I got many information about future directions on Microsoft development products and met with other MVPs. I also had a dinner with Ken Levy, the PM of the VSX Team and had a great chat about community and VS futures.

Due to the jet lag of nine hours I got awaken every morning—well, if we can call it morning at all—at about 4 o’clock. To spend my time till the hotel bar opens for breakfast I looked at the new release of PowerCommands made by Pablo Galiano. I found this free tool great not just because of its functionality but also due to the high quality source code that is also downloadable.

I think this tool is a good candidate for a deep dive since community members can learn a lot from the patterns and constructs used within. I have searched for deep dive articles on PowerCommands but I did not find any. So I decided to dig into the source code and tell you all important things that—as I guess—can be useful when learning VSX programming.

However, this article is intended to be a deep dive I summarize many information that can help even beginners to understand what is behind at certain concepts.

Values in PowerCommands source

There are many great patterns in the PowerCommands source code that can help to understand many aspects of VSPackage development and also patterns that offer architectural guidelines for packages. Without the need of completeness, here is a list:

—  VSPackage programming basics

—  Adding custom branding to packages

—  Declaring and consuming custom services

—  Consuming Visual Studio events

—  VSPackage patterns

—  Binding commands with menu items

—  Creating and using option pages

—  Other useful patterns

—  Using C# 3.0 syntax features (extension methods, lambda expressions, etc.)

—  Good examples of how LINQ queries make expressing intentions easy.

Overview of PowerCommands architecture and solution structure

Let us start the deep dive at the architecture of PowerCommands that is quite simple, as you can see in the following figure:

The VSPackage owns a few object types that provide the functionality. Each Command providing the functionality of PowerCommands is independent from the others, they do not interact directly with each other. A few Command objects have an associated UI. For manageability reasons commands are collected into a container named CommandSet. The package contains few Services to manage commands and related functionality. PowerCommands can be customized through options that can be set on Option Pages. A few commands require responding to VS IDE events. These events are watched and handled by Event Listeners.

The downloadable source code contains a solution with one Visual Studio Integration Package project. The source files within this project are very well structured; the following table gives you an overview about what can be found in a specific source folder:

Source folder Content
Commands

Each command is represented by a separate class. This folder contains these classes.

Commands\Base

The folder has only one file, DynamicCommand.cs that holds the common root class of command classes.

Commands\UI

This folder holds the user interfaces associated with commands. These UIs are implemented as WPF forms.

Common

A few general helper classes.

Extensions

Helper classes containing extension methods.

Linq

PowerCommands intensively uses LINQ. This folder contains helper classes handling the project hierarchy with iterators and LINQ queries.

Listeners

Classes in this folder implement listener functions.

Mvp

The UI of PowerComands follows the Model-View-Presenter pattern. This folder provides basic implementation types representing that design pattern.

OptionPages

The folder holds the types representing the option pages.

Resources

This folder contains the bitmap and icon files used in the package.

Services

Services used within this VSPackage

Shell

Helper classes for interacting with a few shell-owned object types.

ToolWindows

A folder for tool windows used within PowerCommands.

The PowerCommands VSPackage

To understand how PowerCommands work the best start is to have a look at the structure of its package file named PowerCommandsPackage.cs. The decorating attributes tell us a lot about what the package provides when integrating into Visual Studio:

[PackageRegistration(UseManagedResourcesOnly = true)]

[ProvideLoadKey("Standard", "1.0", "PowerCommands for Visual Studio 2008",

  "Microsoft PLK", 1)]

[InstalledProductRegistration(true, "#9394", "#25288", "1.0",

  IconResourceID = 57077,

  LanguageIndependentName = "PowerCommands for Visual Studio 2008")]

[DefaultRegistryRoot(@"Software\Microsoft\VisualStudio\9.0")]

[ProvideMenuResource(1000, 1)]

[ProvideAutoLoad("{ADFC4E64-0397-11D1-9F4E-00A0C911004F}")]

[ProvideService(typeof(SCommandManagerService),

  ServiceName = "CommandManagerService")]

[ProvideService(typeof(SUndoCloseManagerService),

  ServiceName = "UndoCloseManagerService")]

[ProvideProfileAttribute(typeof(CommandsPage), "PowerCommands", "Commands", 15600,

  1912, true, DescriptionResourceID = 197)]

[ProvideOptionPageAttribute(typeof(CommandsPage), "PowerCommands", "Commands",

  15600, 1912, true)]

[ProvideProfileAttribute(typeof(GeneralPage), "PowerCommands", "General", 15600,

  4606, true, DescriptionResourceID = 2891)]

[ProvideOptionPageAttribute(typeof(GeneralPage), "PowerCommands", "General", 

  15600, 4606, true)]

[ProvideToolWindowVisibility(typeof(UndoCloseToolWindow),

  "{F1536EF8-92EC-443C-9ED7-FDADF150DA82}")]

[ProvideToolWindow(typeof(UndoCloseToolWindow), MultiInstances = false,

  Style = VsDockStyle.Tabbed,

  Orientation = ToolWindowOrientation.Top,

  Transient = true,

  Window = "{D78612C7-9962-4B83-95D9-268046DAD23A}")]

[Guid("24E33DBF-CADF-4DA8-ACFE-566366FC8468")]

public sealed class PowerCommandsPackage : Package, IVsInstalledProduct

{

  // --- Package body

}

PowerCommandsPackage implements the IVsInstalledProduct interface to add branding information about the package to the splash screen and to the Help|About dialog. The class is decorated with many package attributes. I treat the highlighted attributes in a few details.

Attribute Description
PackageRegistration

The regpkg.exe utility scans types for this attribute to recognize that the type should be registered as a package. Adding this attribute to our class, regpkg.exe will handle it as a package and looks for other attributes to register the class according to our intention. In our example this attribute sets the UseManagedResourcesOnly flag to tell that all resources used by our package are described in the managed package and not in a satellite .dll.

ProvideLoadKey

This attribute defines the Package Load Key and the base information used to generate the PLK. The first four parameter of the attribute defines the following elements:

—  Minimum VS edition expected

—  Version of the product (package)

—  Name of the product (package)

—  Company name (owner/developer).

The fifth parameter is the ID of the resource holding the PLK.

InstalledProductRegistration

This attribute is responsible to provide information to be displayed by the Help|About function in the VS IDE. The constructor of this attribute requires four parameters with the following meanings:

—  The first false indicate that out package does not provide its own UI to display information about the package.

—  The second and third strings provide the name and the description of the package. The “#” characters indicate that these values should be looked up in the package resources with the IDs following the # character.

—  The fourth “1.0” parameter is the product ID (version number).

—  IconResourceID tells which icon to use for the package.

—  LanguageIndependentName holds the non-localized name of this package.

The resources (name, description and icon) should be defined in the VSPackage.resx file.

DefaultRegistryRoot

To provide an easy way to develop and debug Visual Studio components, VS provides a quite simple method: it allows naming a registry root in the startup parameters of the devenv.exe (that is the VS IDE). When we run a VS component in debug mode, our component actually runs in an IDE using the so-called experimental hive. This hive uses different settings for our debug environment.

When we use the “Start Debugging” function of VS, it uses regpkg.exe with a command line parameter to register out package for the experimental hive.

The DefaultRegistryRoot attribute names the registry root to be used for the package registration if the corresponding regpkg.exe command line parameter is not used. In our example this is the registry root of the “normal” VS 2008 IDE.

ProvideMenuResource

This attribute has two parameters. The first is the so-called resourceID. This value should be set to 1000 as the VSCT compiler uses the value of 1000 by default when adding the binary representation of the .vsct file to the VSPackage resource.  The second parameter is called versionID and it plays important role in the caching mechanism of resources. Instead going into details right now, please keep in mind that ProvideMenuResource attribute allows the package to register its menu resources.

ProvideAutoLoad

VSPackages arte on-demand loaded: VS loads them when first the package is about to be used. This attribute explicitly tells the VS Shell to load the package automatically when it enters into a specific context. The GUID parameter of the attribute is the so-called NoSolution context; this is the default context after starting VS.

This attribute is required since PowerCommands listens to solution events. The listeners can only be created if the package is initialized. If we did not use this attribute, the events related to the first solution opened might not be caught by the package. You can read more about this mechanism in LVN! Sidebar #1.

ProvideService

This attribute is used to proffer services implemented by this package to be consumed by other VSPackages. PowerCommands proffers two services. The attribute specifies the type that can be used to request the service and also sets the name of the service.

ProvideOptionPage

This attribute is used by regpkg.exe to register an option page for the package. The parameters are the followings:

—  Class representing the option page UI

—  Non-localized option page category name

—  Non-localized option page name under the category

—  Resource ID for the category name

—  Resource ID for the page name

—  Flag indicating that the option page can be accessed through the VS automation mechanism.

In the package we use two option pages to customize the behavior of commands.

ProvideProfile

The parameters set on the option pages can be saved to VS settings. This attribute sets up which settings should be saved. The first five parameters have the same meaning as in case of ProvideOptionPage attribute. The closing true parameter value describes that a corresponding option page belongs to the setting. The DescriptionResourceID sets the resource ID used for the setting group description.

Both option pages used by PowerCommands have a corresponding ProvideProfile attribute.

ProvideToolWindow

This attribute is used by regpkg.exe to register a tool window for the package. The first parameter defines the type implementing the tool window. Multinstances is set to false, so we have only a singleton instance of this tool window. The Style parameter sets that the tool window is to be tabbed by default. Orientation sets that the tool window is tabbed at the top of the window identified by the Window parameter.

The Transient property is set to true telling the IDE it should not show tool window if VS is opened (even if it was displayed before closing the IDE).

The GUID used for the Window parameter refers to the Error List window.

ProvideToolWindowVisibility

This attribute sets the context in which the specified tool window is visible. The first parameter is the type of the tool window; the second is the GUID of the context. In this case the specified GUID refers to the so-called SolutionExists context.

It means that our tool window is automatically hidden when the current solution is closed.

The internal package structure is simple as it can be seen from its prototype:

public sealed class PowerCommandsPackage : Package, IVsInstalledProduct

{

  // --- Properties

  public CommandsPage CommandsPage { get; }

  public GeneralPage GeneralPage { get; }

  public IUndoCloseManagerService UndoCloseManager { get; }

  public DTE Dte { get; }

  public UndoCloseToolWindow UndoCloseToolWindow { get; }

 

  // --- IVsInstalledProduct implementation

  public int IdBmpSplash(out uint pIdBmp);

  public int IdIcoLogoForAboutbox(out uint pIdIco);

  public int OfficialName(out string pbstrName);

  public int ProductDetails(out string pbstrProductDetails);

  public int ProductID(out string pbstrPID);

 

  // --- Lifecycle methods

  protected override void Initialize();

  protected override void Dispose(bool disposing);

 

  // --- Event Handlers

  int RDTListener_AfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame);

  int RDTListener_BeforeSave(uint docCookie);

  void documentListener_DocumentClosing(Document Document);

  int solutionListener_AfterOpenSolution(object pUnkReserved, int fNewSolution);

  int solutionListener_AfterCloseSolution(object pUnkReserved);

 

  // --- Private implementation methods

  private Document GetDocumentToBeSaved(uint docCookie);

  private object CreateCommandManagerService(IServiceContainer container,

    Type serviceType);

  private object CreateUndoCloseManagerService(IServiceContainer container,

    Type serviceType);

  private string GetResourceString(string resourceName);

  private string GetViewKind(Document document);

}

Here I do not go into the implementation details of all members above, I will treat them later at the corresponding topic. For example, when I detail how the Undo Close File command works I describe the related event handler methods. However, to understand how the package works, I show the code for the Initialize method:

protected override void Initialize()

{

  base.Initialize();

 

  // --- Initialize services defined within the package

  (this as IServiceContainer).AddService(

      typeof(SCommandManagerService),

      new ServiceCreatorCallback(CreateCommandManagerService),

      true);

 

  (this as IServiceContainer).AddService(

      typeof(SUndoCloseManagerService),

      new ServiceCreatorCallback(CreateUndoCloseManagerService),

      true);

 

  // --- Init the set of commands available within PowerCommands

  CommandSet commandSet = new CommandSet(this);

  commandSet.Initialize();

 

  // --- Initialize an RTDListener

  RDTListener = new RDTListener(this);

  RDTListener.Initialize();

  RDTListener.BeforeSave +=

    new RDTListener.OnBeforeSaveHandler(RDTListener_BeforeSave);

  RDTListener.AfterDocumentWindowHide +=

    new DTListener.OnAfterDocumentWindowHideEventHandler(

    RDTListener_AfterDocumentWindowHide);

 

  // --- Initialize an SolutionListener

  solutionListener = new SolutionListener(this);

  solutionListener.Initialize();

  solutionListener.AfterCloseSolution +=

    new SolutionListener.OnAfterCloseSolutionHandler(

    solutionListener_AfterCloseSolution);

  solutionListener.AfterOpenSolution +=

    new SolutionListener.OnAfterOpenSolutionHandler(

    solutionListener_AfterOpenSolution);

 

  // --- DocumentListener is responsible for handling document related events.

  documentListener = new DocumentListener(this);

}

First the services are initialized later when I treat them, I explain, how initialization works. The initialization of commands is done by the Initialize method of the CommandSet class that uses the SCommandManagerService. Before the service can be used, its initialization should be done.

Then the listener types are initialized. Due to the ProvideAutoLoad attribute, the package is loaded into the memory after Visual Studio is started and then the Initialize method is called. It is important in order the SolutionListener can work correctly, since it subscribes to the event representing the solution is opened. If PowerCommands were not loaded into the memory by Visual Studio startup, the Initialize method would not run unless one of the available commands or object within the package is requested. In this case we could “lose” a few AfterOpenSolution events and that would prevent a few commands work appropriately.

Command handling

The individual commands within PowerCommands are independent. PowerCommands provides a simple infrastructure to keep track of available commands. This helps in a few common tasks, like enabling or disabling each command separately, enumerating commands, etc. The architecture of command handling is summarized in the following figure:

 

The heart of the command handling architecture is the CommandManagerService class that provides the ICommandManagerService to keep track of registered commands. Through this service commands can be registered. The PowerCommandsPackage class uses the CommandSet type to interact with the CommandManagerService. For each command an appropriate instance class is created. When the CommandSet class registers a command instance, it also binds that instance with ID of the commands used by the VS Shell. Through this ID the VS Shell can invoke the command.

The CommandManagerService

The implementation of this service follows the service design pattern recommended by the VS SDK. The elements of this pattern can be found in the files of the Service folder:

// --- ICommandManagerService.cs

[Guid("31A6E058-A531-41CC-B385-B6FAB536066A")]

[ComVisible(true)]

public interface ICommandManagerService

{

  void RegisterCommand(OleMenuCommand command);

  void UnRegisterCommand(OleMenuCommand command);

  IEnumerable<OleMenuCommand> GetRegisteredCommands();

}

 

// --- SCommandManagerService.cs

[Guid("357C77BD-7F09-47E6-82E7-2E847D73204C")]

public interface SCommandManagerService

{

}

 

// --- CommandManagerService.cs

internal class CommandManagerService :

  ICommandManagerService,

  SCommandManagerService

{

  // --- Implementation details

}

In this pattern ICommandManagerService is the interface defining the service contract. This interface is COM visible in order to be available by external packages. The SCommandManagerService type is actually the “address” of the service. We can use this interface type as the parameter of the GetService method when requesting a service object.

The service contract is implemented in the CommandManagerService class. It also must implement the SCommandManagerService since without it the GetService method will not retrieve the service instance.

In order the service can be used the package must initialize it:

[ProvideService(typeof(SCommandManagerService),

  ServiceName = "CommandManagerService")]

// --- Other attributes omitted

public sealed class PowerCommandsPackage : Package, IVsInstalledProduct

{

  // --- Only relevant members are listed

  protected override void Initialize()

  {

    base.Initialize();

 

    // --- Initialize services defined within the package

    (this as IServiceContainer).AddService(

        typeof(SCommandManagerService),

        new ServiceCreatorCallback(CreateCommandManagerService),

        true);

   // --- Other initialization tasks

  }         

 

  // --- This method creates the service instance

  private object CreateCommandManagerService(IServiceContainer container,

    Type serviceType)

  {

    if (container != this)

    {

      return null;

    }

    if (typeof(SCommandManagerService) == serviceType)

    {

      return new CommandManagerService();

    }

    return null;

  }

}

The ProvideService attribute ensures that our service class is registered. The Initialize method adds our service to the service container of the package using the AddService method. The service object is instantiated only when the service is first requested. This is done by the private CreateCommandManagerService method that we also pass to AddService.

The CreateCommandManagerService implements simple operations:

using System.Collections.Generic;

using System.Collections.ObjectModel;

using System.Linq;

using Microsoft.VisualStudio.Shell;

 

namespace Microsoft.PowerCommands.Services

{

  internal class CommandManagerService :

    ICommandManagerService,

    SCommandManagerService

  {

    private IList<OleMenuCommand> registeredCommands;

 

    public CommandManagerService()