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

LearnVSXNow! #24 - Introducing VSXtra

If you have read my posts in the LearnVSXNow series, you can feel that I am not really satisfied with the Managed Package Framework. In a week ago I started a new project on CodePlex called VSXtra. This is an experimental project to create an improved Managed Package Framework. I make it for the VSX community to suggest ideas and receive feedbacks and I hope we can influence the VSX Team when they start to design a new Managed Package Framework.

In this article I give you an overview about the project and I hope you can imagine the opportunities for improving the development experience related to VSPackages. In the beginning and treat the main objectives of the project and give you a few sentences about how I started with the project. The majority of the post analyses a few examples to show you the new style VSXtra allows. If you download the current source code, you can find there the source code for the examples treated here.

VSXtra targets only Visual Studio 2008, I do not plan to focus on compatibility with VS 2005.

 

The VSXtra development approach

 

 

In the last few months I published many articles dealing with patterns and partial solutions to improve the VSX development experience; here is a short list of them:

—  LearnVSXNow! #23 - Coping with GUIDs

—  LVN! Sidebar #4 - Command handlers

—  LVN! Sidebar #3 - Simplifying tool window declaration

—  LVN! Sidebar #1 - Automatically loading packages

—  LearnVSXNow! #17 - Creating a simple custom editor — under pressure

I had many patters in my head like the ones used in the posts above. I always felt that the current MPF implementation is a big constraint if I want to implement my imaginations on its top. So, I decided to start from the MPF source code installed with Visual Studio 2008 SDK and rewrite the MPF in a way supporting my imagination.

No doubt, MPF is full with values! I really like the way as registration attributes are used in conjunction with the related package class and also like the tool window implementation. While analyzing the source code I could find great implementation patterns and even improvement points. I was inspired by them when creating VSXtra. Although I created many new types, also many are based on the original source code with small refactorings.

I decided to create example code first (how the code should look like) and the implementation code only after that. So I developed VSXtra in an example driven way.

In this article you can see the results. The further parts treat examples. I will not go into any implementation details (how VSXtra implements a feature). I only explain how to use VSXtra to create the examples and tell you the differences to the current MPF way.

Example #1: Automatically loaded package with output

The first sample creates a useful less package that is loaded automatically with Visual Studio and writes to the General pane of the output windows. As a result, the output looks like this:

Although the package itself does not provide any function, it is a good example to point out a few values VSXtra adds to MPF. Here is the full source code of the package excluding the resource files and package GUID constant definition:

using System;

using System.Runtime.InteropServices;

using Microsoft.VisualStudio.Shell;

using VSXtra;

 

namespace DeepDiver.OutputWindowWithAutoLoad

{

  [PackageRegistration(UseManagedResourcesOnly = true)]

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

  [InstalledProductRegistration(false, "#110", "#112", "1.0", IconResourceID = 400)]

  [ProvideLoadKey("Standard", "1.0", "OutputWithAutoLoad", "DeepDiver", 1)]

  [Guid(GuidList.guidOutputWindowWithAutoLoadPkgString)]

  [XtraProvideAutoLoad(typeof(UIContext.NoSolution))]

  public sealed class OutputWindowWithAutoLoadPackage : PackageBase

  {

    protected override void Initialize()

    {

      base.Initialize();

      Console.WriteLine();

      Console.WriteLine("This package demonstrates how to use the Output window.");

     

      Console.Write("*** Boolean values: ");

      Console.Write(true);

      Console.Write("|");

      Console.WriteLine(false);

     

      Console.Write("*** Character values: ");

      Console.Write('H');

      Console.Write("|");

      Console.Write('e');

      Console.Write("|");

      Console.Write('l');

      Console.Write("|");

      Console.Write('l');

      Console.Write("|");

      Console.WriteLine('o');

     

      Console.Write("*** Integral values: ");

      Console.Write((byte)0xab);

      Console.Write("|");

      Console.Write((short)12345);

      Console.Write("|");

      Console.Write(123456789);

      Console.Write("|");

      Console.WriteLine(1234567890123456L);

     

      Console.Write("*** String values: ");

      Console.Write("Hello");

      Console.Write("|");

      Console.WriteLine("World");

 

      Console.Write("*** Floating point values: ");

      Console.Write((Single)Math.PI);

      Console.Write("|");

      Console.WriteLine(Math.E);

 

      Console.Write("*** Char arrays: ");

      var chars = new [] { 'H', 'e', 'l', 'l', 'o' };

      Console.Write(chars);

      Console.Write("|");

      Console.WriteLine(chars);

      Console.WriteLine("End of demonstration.");

      Console.WriteLine();

    }

  }

}

The example was created with the VSPackage wizard of Visual Studio. I used the wizard to create an empty package and then changed the package code. VSXtra packages should be derived from the PackageBase class that can be found in the VSXtra namespace (in the VSXtra.dll assembly). In order our example could compile we must add VSXtra to the list of referenced assemblies.

Building, running and debugging of a VSXtra package happens on the same way as for any other VSPackages created in managed code. Registration attributes are used to register package and related object information with Visual Studio. In the sample above I used the standard registration attributes you are familiar with from VS SDK. Only exception is the XtraProvideAutoLoad attribute that functionally is the same as the ProvideAutoLoad attribute but follows a design patterns described in the “LearnVSXNow! #23 - Coping with GUIDs” blog post.

This package writes messages to the General pane of the output window using the well-known System.Console class. The PackageBase class does all the initialization magic in order the code above could work.

Example #2: Implementing the VS SDK Menus and Commands sample

VS SDK provides a reference sample to demonstrate how to use menu commands with simple handler code. I have written a detailed description about how the sample works; you can download the document from here: Menu and Commands Reference Deep Dive.

The sample implements six commands, four of them can be found in the tool menu, the two others in toolbars:

   


The “VSXtra Command Sample” and the zoom button on toolbar simply write a message to the output window. The graph button displays a message dialog. In the tool menu the “VSXtra Dynamic Visibility 1” button has a hidden pair named “VSXtra Dynamic Visibility 2”, using them alternates their visibility, so once the first then for the second menu button is visible. The “VSXtra Text Changes” button counts the clicks and indicates this number.

The package was created with the VSPackage wizard with a name of DynamicCommands. This time I configured the wizard to create a menu command. Without this step no .vsct file would have been created. I simply copied the .vsct file from the original example and made some cosmetics on it (changed GUIDs and IDs). Here I do not show the file, you can look it up in the downloaded source code.

The package has the following source code:

using System.Runtime.InteropServices;

using Microsoft.VisualStudio.Shell;

using VSXtra;

 

namespace DeepDiver.DynamicCommands

{

  [PackageRegistration(UseManagedResourcesOnly = true)]

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

  [InstalledProductRegistration(false, "#110", "#112", "1.0", IconResourceID = 400)]

  [ProvideLoadKey("Standard", "1.0", "DynamicComands", "DeepDiver", 1)]

  [ProvideMenuResource(1000, 1)]

  [Guid(GuidList.guidDynamicCommandsPkgString)]

  public sealed class DynamicCommandsPackage : PackageBase

  {

  }

}

I did not omit any code from the package: the class body is really empty! The original VS SDK sample’s package code contains about 40 lines of code to set up menu command event handlers in the Initialize event of the package. Here we do not have code at all. Where is the trick?

There is now trick: the PackageBase class is smart enough to set up command event handlers from the code automatically. It uses reflection to scan through the assembly implementing the package and recognizes event handler declaration. Here is the full code for event handles:

using System.Runtime.InteropServices;

using Microsoft.VisualStudio.Shell;

using VSXtra;

 

namespace DeepDiver.DynamicCommands

{

  [Guid(GuidList.guidDynamicCommandsCmdSetString)]

  public sealed class DynamicCommandGroup : CommandGroup<DynamicCommandsPackage>

  {

    [CommandId(CmdIDs.cmdidMyCommand)]

    [WriteMessageAction("Sample Command executed.")]

    public sealed class MyCommand : MenuCommandHandler { }

 

    [CommandId(CmdIDs.cmdidMyGraph)]

    [ShowMessageAction("Graph Command executed.")]

    public sealed class MyGraph : MenuCommandHandler { }

 

    [CommandId(CmdIDs.cmdidMyZoom)]

    [WriteMessageAction("Zoom Command executed.")]

    public sealed class MyZoom : MenuCommandHandler { }

 

    public abstract class DynamicVisibility : MenuCommandHandler

    {

      protected override void OnExecute(OleMenuCommand command)

      {

        var dynCommand1 = GetHandler<DynamicVisibility1>();

        var dynCommand2 = GetHandler<DynamicVisibility2>();

        if (dynCommand1 == null || dynCommand2 == null) return;

        dynCommand1.MenuCommand.Visible = command.CommandID.ID == CmdIDs.cmdidDynVisibility2;

        dynCommand2.MenuCommand.Visible = !dynCommand1.MenuCommand.Visible;

      }

    }

 

    [CommandId(CmdIDs.cmdidDynVisibility1)]

    [CommandVisible(true)]

    public sealed class DynamicVisibility1 : DynamicVisibility { }

 

    [CommandId(CmdIDs.cmdidDynVisibility2)]

    [CommandVisible(false)]

    public sealed class DynamicVisibility2 : DynamicVisibility { }

 

    [CommandId(CmdIDs.cmdidDynamicTxt)]

    public sealed class DynamicText : MenuCommandHandler

    {

      private int _ClickCount;

      protected override void OnExecute(OleMenuCommand command)

      {

        command.Text = "VSXtra Text Changed: " + ++_ClickCount;

      }

    }

  }

}

Each menu command handler is represented by a class derived from MenuCommandHandler. These classes have decorating attributes like CommandId and the others. The handler classes are nested into a class derived from the generic CommandGroup<> class having the DynamicCommandsPackage class as its type parameter. The meaning of the resulted DynamicCommandGroup class is a logical container for commands forming a group. Each command handlers in this group share the same GUID represented by the GuidList.guidDynamicCommandsCmdSetString value. The MenuCommandHandler derived classes do not need to specify this GUID in their CommandId attribute (however, they could do it, or even to specify a different GUID). The containment of the command handler classes in the logical command group helps to guess out the GUID part of the command ID if it is not set explicitly.

Let’s have a look at the command handlers one-by-one. The “VSXtra Sample Command” has the following handler code:

[CommandId(CmdIDs.cmdidMyCommand)]

[WriteMessageAction("Sample Command executed.")]

public sealed class MyCommand : MenuCommandHandler { }

The CommandId attribute sets the ID part of the command within the logical container set by the GUID part of the command identifier. VSXtra provides predefined actions (and also allows developers to create their own custom actions). These actions can be bound to menu handler classes to eliminate the need to write explicit action code. In the code above the WriteMessageAction attribute sends output message to the output window when the command is invoked. The MyZoom command handler works on the same way sending a different message to the output window.

The MyGraph command handler is also simple. Its command action is ShowMessageAction displaying a message box with the specified string as the following code indicates:

[CommandId(CmdIDs.cmdidMyGraph)]

[ShowMessageAction("Graph Command executed.")]

public sealed class MyGraph : MenuCommandHandler { }

There are two buttons, DynamicVisibility1 and DynamicVisibility2 that share their command handler using inheritance: the handler code is defined in an abstract class and concrete classes inherit the behavior:

public abstract class DynamicVisibility : MenuCommandHandler

{

  protected override void OnExecute(OleMenuCommand command)

  {

    var dynCommand1 = GetHandler<DynamicVisibility1>();

    var dynCommand2 = GetHandler<DynamicVisibility2>();

    if (dynCommand1 == null || dynCommand2 == null) return;

    dynCommand1.MenuCommand.Visible = command.CommandID.ID ==

      CmdIDs.cmdidDynVisibility2;

    dynCommand2.MenuCommand.Visible = !dynCommand1.MenuCommand.Visible;

  }

}

 

[CommandId(CmdIDs.cmdidDynVisibility1)]

[CommandVisible(true)]

public sealed class DynamicVisibility1 : DynamicVisibility { }

 

[CommandId(CmdIDs.cmdidDynVisibility2)]

[CommandVisible(false)]

public sealed class DynamicVisibility2 : DynamicVisibility { }

Inside the OnExecute method we can access the command handler instances through the GetHandler method. Through this instance we access the MenuCommand and can set the properties of the menu item. In this case we use it to set the visibility.

The abstract DynamicVisibility class has no attributes; those are attached to the concrete handler classes. The CommandVisible attribute sets the initial state of command items behind the handler class.

The last “tricky” button counts and displays the number of clicks. It uses the following code:

[CommandId(CmdIDs.cmdidDynamicTxt)]

public sealed class DynamicText : MenuCommandHandler

{

  private int _ClickCount;

  protected override void OnExecute(OleMenuCommand command)

  {

    command.Text = "VSXtra Text Changed: " + ++_ClickCount;

  }

}

The code is straightforward. The interesting point is that the owner package always uses the same instance of the command handler class while it is loaded into a Visual Studio instance. So, we can store _ClickCount in an instance field.

Example #3: Dynamic Tool Window

The VS SDK 2008 contains a reference sample called “ToolWindows” sample. That sample implements two kind of tool windows: a “dynamic” tool window demonstrating how to display tool windows and respond to window frame events; a “persisted” tool window to demonstrate tool window persistence and toolbar handling. This VSXtra example demonstrates the dynamic tool window, the next (Example #4) implements the persistent tool window.

The dynamic tool window handles the so-called window frame events: when the tool window is moved, sized, docked, undocked or tabbed its window frame raises events. Our tool window catches the events and writes out a related message into a custom output window pane and also displays the tool window coordinates within the window itself:

 

 

You may not surprise how simple our VSPackage code is:

using System.Runtime.InteropServices;

using Microsoft.VisualStudio.Shell;

using VSXtra;

 

namespace DeepDiver.DynamicToolWindow

{

  [PackageRegistration(UseManagedResourcesOnly = true)]

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

  [InstalledProductRegistration(false, "#110", "#112", "1.0",

    IconResourceID = 400)]

  [ProvideLoadKey("Standard", "1.0", "DynamicToolWindow", "DeepDiver", 1)]

  [ProvideMenuResource(1000, 1)]

  [ProvideToolWindow(typeof(DynamicWindowPane))]

  [Guid(GuidList.guidDynamicToolWindowPkgString)]

  public sealed class DynamicToolWindowPackage : PackageBase

  {

  }

}

Our tool window has a related menu item in the View|Other windows menu to display the tool window. The related menu handler code uses the same pattern as command items in Example #2:

using System.Runtime.InteropServices;

using VSXtra;

 

namespace DeepDiver.DynamicToolWindow

{

  [Guid(GuidList.guidDynamicToolWindowCmdSetString)]

  public sealed class DynamicToolWindowCommandGroup:

    CommandGroup<DynamicToolWindowPackage>

  {

    [CommandId(CmdIDs.cmdidMyTool)]

    [ShowToolWindowAction(typeof(DynamicWindowPane))]

    public sealed class ShowToolCommand : MenuCommandHandler { }

  }

}

We do not need to write imperative code to display the tool window: the ShowToolWindowAction does it for us. Its parameter declares which tool window should be displayed.

The user interface (code defining the UI behavior) of our dynamic tool window is simple:

using System;

using System.Drawing;

using System.Windows.Forms;

using VSXtra;

 

namespace DeepDiver.DynamicToolWindow

{

  public partial class DynamicWindowControl : UserControl

  {

    public DynamicWindowControl()

    {

      InitializeComponent();

    }

 

    public void RefreshValues(object sender, EventArgs arguments)

    {

      var frame = sender as WindowFrame;

      if (frame == null) return;

      Rectangle rect;

      var framePos = frame.GetWindowPosition(out rect);

      xText.Text = rect.Left.ToString();

      yText.Text = rect.Top.ToString();

      widthText.Text = rect.Width.ToString();

      heightText.Text = rect.Height.ToString();

      dockedCheckBox.Checked = framePos == FramePosition.Docked;

      Invalidate();

    }

  }

}

The RefreshValues method subscribes to window frame events (so the sender argument can be casted to a WindowFrame) and displays the window position in the controls of the user interface. As you know there is no managed type for a window frame in MPF, we have only the IVsWindowFrame and IVsWindoFrameNotify3 interop types. VSXtra defines the WindowFrame type to wrap the functionality of those interfaces into a managed type. WindowFrame plays a crucial role in defining the functionality of the window pane:

using System.ComponentModel;

using System.Drawing;

using System.Runtime.InteropServices;

using VSXtra;

 

namespace DeepDiver.DynamicToolWindow

{

  [Guid("F0E1E9A1-9860-484d-AD5D-367D79AABF55")]

  [InitialCaption("Dynamic Tool Window")]

  [BitmapResourceId(301)]

  class DynamicWindowPane : ToolWindowPane<DynamicToolWindowPackage,

    DynamicWindowControl>

  {

    private OutputWindowPane _OutputPane;

 

    public override void OnToolWindowCreated()

    {

      base.OnToolWindowCreated();

     

      // --- Set up the window pane

      _OutputPane = OutputWindow.GetPane<EventsPane>();

      VsDebug.Assert(_OutputPane != null, "Output pane creation failed.");

     

      // --- Set up window frame events

      Frame.OnShow += OnFrameShow;

      Frame.OnClose += OnFrameClose;

      Frame.OnResize += OnFrameResize;

      Frame.OnMove += OnFrameMove;

      Frame.OnDockChange += OnFrameDockChange;

      Frame.OnStatusChange += UIControl.RefreshValues;

    }

   

    // --- Event handler methods omitted

  }

 

  [AutoActivate(true)]

  [DisplayName("Dynamic window events")]

  class EventsPane : OutputPaneDefinition

  {

  }

}

The window pane class is inherited from the ToolWindowPane<,> generic class that accepts two type parameters. The first is the owner package type (must be a subclass of PackageBase), the second is a control representing the package user interface. The initial appearance of the tool window is set up through attributes like InitialCaption and BitmapResourceID.

After the tool window has been created and sited in its window frame, the OnToolWindowCreated method is called. In this method we set up the output frame where event handler codes write out messages and then subscribe for window frame events. As you see, we can access the hosting window frame through the Frame property of the pane. This example demonstrates all the events the window frame has (see source code comments for detailed description about each event). The OnStatusChange event is raised in parallel with all the other events.

The EventsPane class is a good example how easy is to create and use an output window panel with VSXtra. Using the _OutputPane member field frame event handler methods write directly to this output pane just as windows console applications to the system console:

void OnFrameDockChange(object sender, WindowFrameDockChangedEventArgs e)

{

  _OutputPane.WriteLine("Dock state changed.");

  _OutputPane.WriteLine("  Docked: {0}", e.Docked);

  DisplayPosition(e.Position);

}

 

void DisplayPosition(Rectangle rect)

{

  _OutputPane.WriteLine("  New position: {0}", rect);

}

Example #4: Persisted Tool Window

This example demonstrates the second tool window of the VS SDK’s ToolWindows reference sample called Persisted tool window. This tool window has a tool bar with a refresh button and displays the list of tool windows instantiated within Visual Studio:


This sample uses the same simple package definition as all the examples before:

using System.Runtime.InteropServices;

using Microsoft.VisualStudio.Shell;

using VSXtra;

 

namespace DeepDiver.PersistedToolWindow

{

  [PackageRegistration(UseManagedResourcesOnly = true)]

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

  [InstalledProductRegistration(false, "#110", "#112", "1.0",

    IconResourceID = 400)]

  [ProvideLoadKey("Standard", "1.0", "PersistedToolWindow", "DeepDiver", 1)]

  [ProvideMenuResource(1000, 1)]

  [ProvideToolWindow(typeof(PersistedWindowPane), Style = VsDockStyle.Tabbed,

    Window = "3ae79031-e1bc-11d0-8f78-00a0c9110057")]

  [Guid(GuidList.guidPersistedToolWindowPkgString)]

  public sealed class PersistedToolWindowPackage : PackageBase

  {

  }

}

As you see, the ProvideToolWindow still uses the “old” form with a literally specified GUID. Soon, it will be changed to the style like XtraProvideAutoLoad attribute in Example #1. The declaration of the tool window pane follows the pattern described in Example #3:

using System.Runtime.InteropServices;

using Microsoft.VisualStudio.Shell;

using Microsoft.VisualStudio.Shell.Interop;

using VSXtra;

 

namespace DeepDiver.PersistedToolWindow

{

  [Guid("0A6F8EDC-5DDB-4aaa-A6B3-2AC1E319693E")]

  [InitialCaption("Persisted Tool Window")]

  [BitmapResourceId(301)]

  [Toolbar(typeof(DynamicToolWindowCommandGroup.PersistedWindowToolbar))]

  class PersistedWindowPane : ToolWindowPane<PersistedToolWindowPackage,

    PersistedWindowControl>

  {

    public override void OnToolWindowCreated()

    {

      base.OnToolWindowCreated();

      UIControl.TrackSelection = GetService<STrackSelection, ITrackSelection>();

      RefreshList(null);

    }

 

    [CommandExecMethod]

    [