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

LearnVSXNow! #27 - Multiple Tool Windows

Anyone extending Visual Studio meets with tool windows, sooner or later implements one. The extensibility architecture of Visual Studio supports multiple instances of the same tool window type. However, we rarely can see VSPackages using this feature. Even the built-in tool windows in the VS IDE seem to avoid this feature. A good example is the duplication of the Find Result tool windows:

 

These windows look like as two instances of the same tool window class. We could assume that VS IDE defines one FindResultToolWindow class and manages two instances of them. In reality, it does not. VS IDE uses two separate window types having two separate toolbar and two separate set of toolbar buttons with separate IDs. Is this just a mistake or it is by design?

You do not know Microsoft’s developers if you opt for “mistake”. This duplication is by design and the root cause can be found in the command architecture.

Tool windows—among a number of other VS objects—implement IOleCommandTarget and so receive related messages to refresh the status of a given command or execute that. If a tool window implements a command, it can register it with Visual Studio basically in two ways: either only with the OleMenuCommandService belonging to the tool window or it can even promote it to the OleMenuCommandService belonging to the package owning the tool window.

—  In the first case command messages are sent only from the tool window to itself—for example when the user clicks the related toolbar button. If the tool window loses the focus, the status query messages do not reach the tool window and so the visual states of commands are set according to their default state (according to the declaration in the VSCT file). So, if we disable a control in the tool window that was initially enabled, it gets enabled again as soon as the focus is lost.

—  In the second case command messages are sent from VS IDE to the tool window. Status query messages find the tool window even if that does not have the focus, so the visual state of the tool window remains maintained.

But what happens, when we have multiple instances of the same tool window? While we do not have any UI for the commands in the tool window, or the UI is static by means of visual state, everything works fine. However, when we must change the visual state, we have “issues”:

—  In case of local command handling our visual state returns back to the initial state as soon as the tool window loses the focus.

—  In case of promoted command handling, only one of the tool window instances can register a command handler at the package level for a given CommandID.

Therefore, if we have multiple instances of the same tool windows maintaining their separate visual state, we cannot implement them simply by multi-instantiating the tool window class. We actually must create two types of tool windows and instantiate them separately. Of course, they must use separate command sets.

This is the reason why we have two Find Results window in Visual Studio. Even if they look identical, they have separate toolbars with separate set of same-looking buttons having different command IDs.

A silly sample

I have often met with the aforementioned issue in my real-world projects, so I decided to create a pattern and build in into my VSXtra project. I have finished the first version of this pattern and also implemented it. I’d like to introduce it to you through a small example.

I’ve created a tool window called “Stack window” to push numbers to a stack and execute the four basic binary operations on the two topmost stack items. The stack window looks like this:

We can type a text into the edit box at the bottom of the tool window. When we push it to the stack, it goes to the right-side stack if it is an integer or to the left-side stack if it is not. If there is at least two items in the number stack we enable the binary operator buttons on the toolbar at the right. When an operator is applied, the operands are popped from the stack and the result is pushed back. Should any kind of exception occur the message goes to the left-side stack.

The tool window also supports Cut, Copy and Paste operations: the topmost item on the number stack can be cut or copied to the clipboard. The Paste operation pushes the clipboard text to the window just like if we have typed it into the edit box.

The sample I have created uses two Number Stack tool windows that manage their visual state separately:

As you see Stack Window #1 has the focus and its toolbar buttons are disable because we do not have two operands required for any binary operation. Stack Window #2 enables the related buttons because it has three values on the stack. On the Visual Studio toolbar you can see the Cut and Copy operations are enabled since the focused Stack Window #1 has a value on its stack.

Implementation Approach

In order our Stack Window could maintain their visual command state separately we need two sets of commands, one set for each tool window. However, the handlers behind the commands must do the same operation—using the right tool window instance.

I decided to create an abstract base class implementing the tool window behavior including the command handler implementations and two concrete derived types to define each Stack Window by a separate class:

public abstract class NumberStackWindowPane:

  ToolWindowPane<MultiToolWindowPackage, NumberStackControl>

{

  // --- Command body omitted

}

[Guid("B097B9C4-D98C-45fe-B355-DE4865E77DCF")]

[InitialCaption("Stack Window #1")]

[BitmapResourceId(300, 1)]

[ToolbarLocation(ToolbarLocation.Right)]

[Toolbar(typeof(CommandGroup.StackWindowToolbar1))]

public sealed class NumberStackWindowPane1 : NumberStackWindowPane

{

  // --- Class body is really empty!

}

 

[Guid("7B5DC947-D280-4255-B525-3AA62AAA52D4")]

[InitialCaption("Stack Window #2")]

[BitmapResourceId(300, 2)]

[ToolbarLocation(ToolbarLocation.Right)]

[Toolbar(typeof(CommandGroup.StackWindowToolbar2))]

public sealed class NumberStackWindowPane2 : NumberStackWindowPane

{

  // --- Class body is really empty!

}

The two concrete classes actually do not implement any explicit behavior, they simply use separate decorators to implement visual differences in the window caption or related bitmap resource. VSXtra tool windows can set their toolbars in a declarative fashion using the Toolbar attribute.

Handling commands

As you see, the tool windows use separate toolbar object declared in the following way:

[Guid(GuidList.guidMultiToolWindowCmdSetString)]

public sealed class CommandGroup: CommandGroup<MultiToolWindowPackage>

{

  [CommandId(CmdIDs.cmdidStackWindowToolbar1)]

  public sealed class StackWindowToolbar1 : ToolbarDefinition { }

 

  [CommandId(CmdIDs.cmdidStackWindowToolbar2)]

  public sealed class StackWindowToolbar2 : ToolbarDefinition { }

 

  // --- Other menu handlers omitted

}

The CommandId attribute of toolbar definitions bind the handler class with the corresponding toolbar object in the VSCT definition. The GUID part of the command ID is retrieved from the Guid attribute of the CommandGroup class.

The core VSCT definition contains the following information:

<?xml version="1.0" encoding="utf-8"?>

<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <Extern href="stdidcmd.h"/>

  <Extern href="vsshlids.h"/>

  <Extern href="msobtnid.h"/>

 

  <Commands package="guidMultiToolWindowPkg">

    <Menus>

      <!-- Toolbar definition for the first toolbar -->

      <Menu guid="guidMultiToolWindowCmdSet" id="StackWindowToolbar1"

        priority="0x0000" type="ToolWindowToolbar">

        <Parent guid="guidMultiToolWindowCmdSet" id="StackWindowToolbar1"/>

        <Strings>

          <ButtonText>StackWindowToolBar</ButtonText>

          <CommandName>Stack Window Toolbar</CommandName>

        </Strings>

      </Menu>

      <!-- Second toolbar definition omitted -->

    </Menus>

 

    <Groups>

      <!-- Button group in the first toolbar -->

      <Group guid="guidMultiToolWindowCmdSet" id="StackWindowToolbarGroup1"

        priority="0x0500">

        <Parent guid="guidMultiToolWindowCmdSet" id="StackWindowToolbar1"/>

      </Group>

      <!-- Second button group omitted -->

    </Groups>

   

    <Buttons>

      <!-- Buttons displaying the tool windows are omitted -->

      <!-- Toolbar buttons for window #1 -->

      <Button guid="guidMultiToolWindowCmdSet" id="cmdidAdd1" priority="0x100"

        type="Button">

        <Parent guid="guidMultiToolWindowCmdSet" id="StackWindowToolbarGroup1"/>

        <Icon guid="guidOfficeIcon" id="msotcidPlus"/>

        <CommandFlag>DynamicVisibility</CommandFlag>

        <Strings>

          <ButtonText>Add</ButtonText>

        </Strings>

      </Button>

      <Button guid="guidMultiToolWindowCmdSet" id="cmdidSubtract1"  

        priority="0x100" type="Button">

        <Parent guid="guidMultiToolWindowCmdSet" id="StackWindowToolbarGroup1"/>

        <Icon guid="guidOfficeIcon" id="msotcidMinus"/>

        <CommandFlag>DynamicVisibility</CommandFlag>

        <Strings>

          <ButtonText>Subtract</ButtonText>

        </Strings>

      </Button>

      <!-- Other buttons for the first toolbar omitted -->

      <!-- Toolbar buttons for window #2 -->

      <Button guid="guidMultiToolWindowCmdSet" id="cmdidAdd2" priority="0x100"

        type="Button">

        <Parent guid="guidMultiToolWindowCmdSet" id="StackWindowToolbarGroup2"/>

        <Icon guid="guidOfficeIcon" id="msotcidPlus"/>

        <CommandFlag>DynamicVisibility</CommandFlag>

        <Strings>

          <ButtonText>Add2</ButtonText>

        </Strings>

      </Button>

      <!-- Other buttons for the second toolbar omitted -->

    </Buttons>

  

    <Bitmaps>

      <Bitmap guid="guidImages" href="Resources\Images_32bit.bmp"

        usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows"/>

    </Bitmaps>

 

  </Commands>

 

  <Symbols>

    <GuidSymbol name="guidMultiToolWindowPkg"

      value="{c2eee522-7897-4f66-967b-d8f226771914}" />

   

    <GuidSymbol name="guidMultiToolWindowCmdSet"

      value="{18fe7969-c755-450a-9067-a9098f179b48}">

      <IDSymbol name="MultiWindowGroup" value="0x1020" />

      <IDSymbol name="StackWindowToolbarGroup1" value="0x1021" />

      <IDSymbol name="StackWindowToolbarGroup2" value="0x1022" />

      <IDSymbol name="StackWindowToolbar1" value="0x2000" />

      <IDSymbol name="StackWindowToolbar2" value="0x2001" />

      <IDSymbol name="cmdidShowFirstWindow" value="0x0100" />

      <IDSymbol name="cmdidShowSecondWindow" value="0x0101" />

      <IDSymbol name="cmdidAdd1" value="0x0102" />

      <IDSymbol name="cmdidSubtract1" value="0x0103" />

      <IDSymbol name="cmdidMultiply1" value="0x0104" />

      <IDSymbol name="cmdidDivide1" value="0x0105" />

      <IDSymbol name="cmdidAdd2" value="0x0112" />

      <IDSymbol name="cmdidSubtract2" value="0x0113" />

      <IDSymbol name="cmdidMultiply2" value="0x0114" />

      <IDSymbol name="cmdidDivide2" value="0x0115" />

    </GuidSymbol>

    <!-- Bitmap symbols omitted -->

  </Symbols>

 

</CommandTable>

As you see from the VSCT file, all buttons are duplicated with their own command ID and they are placed on the toolbar belonging to the appropriate tool window instances.

The menu commands displaying the tool windows are also defined in the VSCT file (even if I omitted from the definition above) and they use the following command handler definitions:

[Guid(GuidList.guidMultiToolWindowCmdSetString)]

public sealed class CommandGroup: CommandGroup<MultiToolWindowPackage>

{

  [CommandId(CmdIDs.cmdidShowFirstWindow)]

  [ShowToolWindowAction(typeof(NumberStackWindowPane1))]

  public sealed class ShowStackWindow1: MenuCommandHandler {}

 

  [CommandId(CmdIDs.cmdidShowSecondWindow)]

  [ShowToolWindowAction(typeof(NumberStackWindowPane2))]

  public sealed class ShowStackWindow2 : MenuCommandHandler { }

}

CommandId attributes bind handlers with the VSCT command definitions. ShowToolWindowAction attributes automatically display the tool window types set in their parameters—without the need to write any code for this action.

Defining command handlers

The ToolWindowPane<,> class of VSXtra is an IOleCommandTarget just like the ToolWindowPane defined by MPF. However, VSXtra makes it much easier to define IOleCommandTarget behavior then MPF. Instead of writing switch statements for the Exec and QueryStatus methods or even to override these methods, developers can define command handler methods as illustrated here:

public abstract class NumberStackWindowPane:

  ToolWindowPane<MultiToolWindowPackage, NumberStackControl>

{

  [CommandStatusMethod]

  [Promote]

  [CommandId(CmdIDs.cmdidAdd1)]

  [CommandId(CmdIDs.cmdidSubtract1)]

  [CommandId(CmdIDs.cmdidMultiply1)]

  [CommandId(CmdIDs.cmdidDivide1)]

  protected void OperationStatus(OleMenuCommand command)

  {

    command.Enabled = UIControl.HasAtLeastTwoOperands;

  }

 

  [CommandExecMethod]

  [CommandId(CmdIDs.cmdidAdd1)]

  protected void AddExec()

  {

    UIControl.Operation((x, y) => { checked { return x + y; } });

  }

 

  [CommandExecMethod]

  [CommandId(CmdIDs.cmdidSubtract1)]

  protected void SubtractExec()

  {

    UIControl.Operation((x, y) => { checked { return x - y; } });

  }

 

  // --- Other operations and command handlers omitted

}

By marking a method with the CommandStatusMethod attribute VSXtra knows that the method should be executed when the IOleCommandTarget.QueryStatus method is called. Similarly, CommandExecMethod attribute marks a method to be called for IOleCommandTarget.Exec. The CommandId attribute is used to bind the handler with the appropriate command, a handler method can respond to one or more commands. For example all the four commands representing the binary operations set their visual state according to the OperationStatus method checking if there are at least two values on the stack.

The Promote attribute signs that commands having any handler methods with this attribute should be promoted to the parent (in this case to the package) OleMenuCommandService.

You may wonder where the GUID parts of command identifiers are defined. You do not need to explicitly use them, VSXtra infers these GUIDs from the toolbar definition of the related pane. Handlers having CommandExecMethod attribute can be used with two method signatures: without parameters and with one OleMenuCommand parameter representing the actual menu command (in the code above I use only the parameterless signature).

Using this approach we can write command handlers also for the VS IDE commands:

[CommandStatusMethod]

[VsCommandId(VSConstants.VSStd97CmdID.Cut)]

[VsCommandId(VSConstants.VSStd97CmdID.Copy)]

protected void CutCopyStatus(OleMenuCommand command)

{

  command.Enabled = UIControl.HasAtLeastOneOperand;

}

 

[CommandStatusMethod]

[VsCommandId(VSConstants.VSStd97CmdID.Paste)]

protected void PasteStatus(OleMenuCommand command)

{

  command.Enabled = Clipboard.ContainsText();

}

 

[CommandExecMethod]

[VsCommandId(VSConstants.VSStd97CmdID.Cut)]

protected void CutExec()

{

  Clipboard.SetText(UIControl.PopValue().ToString());

}

 

[CommandExecMethod]

[VsCommandId(VSConstants.VSStd97CmdID.Copy)]

protected void CopyExec()

{

  Clipboard.SetText(UIControl.PeekValue().ToString());

}

 

[CommandExecMethod]

[VsCommandId(VSConstants.VSStd97CmdID.Paste)]

protected void PasteExec()

{

  UIControl.PushText(Clipboard.GetText());

}

VsCommandId attributes binds the handler with the built-in VS IDE command set in its argument.  I suppose, it is quite easy to understand what the handler methods above do...

The same handler code for two command sets

The solution for the main issue of multiple tool window instances has not been answered. How to use the same command handlers for two set of commands? VSXtra answers this question by the command mapping mechanism. Command mapping means that we can change how the CommandId attributes are used when physically binding handler methods with the real commands:

// --- Ottributes omitted

public sealed class NumberStackWindowPane1 : NumberStackWindowPane

{

}

 

// --- Other attributes omitted

[CommandMap(CmdIDs.cmdidAdd1, CmdIDs.cmdidDivide1,

  (int)(CmdIDs.cmdidAdd2 - CmdIDs.cmdidAdd1))]

public sealed class NumberStackWindowPane2 : NumberStackWindowPane

{

}

IOleCommandTarget classes in VSXtra (of course, including window panes) can have zero, one or more CommandMap attributes assigned to the type definition. One CommandMap attribute defines a range of command IDs and an offset value. When the physical mapping of command handlers happens, the IDs in the defined range are mapped to a new ID (the original ID plus the offset value).

In our concrete case, all command handlers are defined in the abstract NumberStackWindowPane class (we use the cmdidAdd1 to cmdIdDivide1 range in the command handler methods). The concrete NumberStackWindowPane1 class does not have any mappings, so the command handlers will be bound to the original command IDs. NumberStackWindowFrame2 has a CommandMap attribute that shifts the [cmdidAdd1, cmdIdDivide1] range to the [cmdidAdd2, cmdIdDivide2] range, so these commands will be bound to the handler methods.

That’s all! We put all the behavior into an abstract class and derived two concrete classes with empty bodies (no change in functional behavior).

The user interface implementing the functionality is quite simple, so I do not treat it here. Download the current source code of VSXtra and look for the MultiToolWindow example in the Samples\ToolWindows folder.

Where we are?

However Visual Studio supports multiple instances of a tool window, due to the command handling mechanism, it is a challenge to create multiple instances handling their own visual command state. VSXtra provides a way that makes it simple to cope with this challenge. The two cornerstones are the declarative command dispatching and command ID mapping mechanisms.


Posted Jul 25 2008, 02:29 PM by inovak
Filed under:

Comments

Visual Studio Hacks wrote Visual Studio Links #58
on Wed, Jul 30 2008 3:09

My latest in a series of the weekly, or more often, summary of interesting links I come across related to Visual Studio. DiveDeeper blog continues LearnVSXNow! with Multiple Tool Windows . Bruce Kyle (US ISV Developer Evangelism Team blog) announced the

sean wrote re: LearnVSXNow! #27 - Multiple Tool Windows
on Wed, Jul 30 2008 19:12

One of the weakness I've found with multi-instance toolwindows is that when the IDE switches layouts (design to debug for example), the toolwindows go away.   When the IDE switches back (in this example, from debug back to design), the multi-instance toolwindows are not restored.

Do you account for that?  If so, how do you manage that?

inovak wrote re: LearnVSXNow! #27 - Multiple Tool Windows
on Wed, Jul 30 2008 20:21

Hey Sean, the toolwindows I handle in this post seem to be multi-instance windows, but in reality they are single instances of multiple tool window classes. They work correctly with IDE context switching, they are restored.

However, I have never tried to use the multiple instances of the same window class. Let me some time to guess it out how thhey work...

sean wrote re: LearnVSXNow! #27 - Multiple Tool Windows
on Wed, Jul 30 2008 22:02

Sorry, I see I misread.  If you do play around with multi-instance toolwindows, it would be interesting to get your take on the differences between multi-instance toolwindows and single instances of multiple tool window classes.

On the other hand, the VS SDK documentation doesn't seem to mention them anymore.  Compare the vs2005 docs:

msdn.microsoft.com/.../bb165920(VS.80).aspx

to the vs2008 docs:

msdn.microsoft.com/.../bb165920.aspx

kion wrote re: LearnVSXNow! #27 - Multiple Tool Windows
on Mon, Aug 25 2008 3:15

As SO said above, this article referred to multi classes of single instance of tool window. I'd like to have an article about creating tool window dynamically. It means that a tool window will be created at runtime whenever user need.

Friday links 139 « A Programmer with Microsoft tools wrote Friday links 139 &laquo; A Programmer with Microsoft tools
on Fri, Nov 6 2015 3:21

Pingback from  Friday links 139 « A Programmer with Microsoft tools