Chapter 9. Programming the Visual Studio User Interface

Microsoft® Visual Studio® is made up of many different windows that show data to the user, including the Task List, Solution Explorer, and the Microsoft Windows® Forms designer. You can manipulate these windows not only by using the mouse and keyboard but also through the object model by using a macro or an add-in. In this chapter, we'll discuss the many objects you can program in the user interface of Visual Studio.

Window Basics

The user interface for each window in Visual Studio is different from that of other windows, but they all share a few basic methods and properties. Let's look at the common parts of the object model.

The Windows Collection

Visual Studio contains a number of tool and document windows that you can access through the automation model. Each of these windows is represented in the object model by a Window object and can be found in the Windows collection, which is accessible through the DTE.Windows property.

You can retrieve a Window object from the Windows collection in a number of ways. One way is to use the enumerator to walk the list of all available windows, as shown here:

Sub EnumWindows()
    Dim window As EnvDTE.Window
    For Each window In DTE.Windows
        MsgBox(window.Caption)
    Next
End Sub

Or you can use the numerical indexing method:

Sub EnumWindows2()
    Dim window As EnvDTE.Window
    Dim i As Integer
    For i = 1 To DTE.Windows.Count
        MsgBox(DTE.Windows.Item(1).Caption)
    Next
End Sub

However, using these formats for finding a window isn't optimal because you usually want to find one specific window, and looking at all the windows to find it is a waste of CPU cycles. The numerical indexing method isn't always best because the position of a window from one instance of Visual Studio to the next might change, so you can't rely on using an index to return a specific Window object. In fact, you have no guarantee that calling the Item method twice in a row by using a numerical index will return the same EnvDTE.Window object because new windows might be created in between calls to this method. In addition, the numerical indexing method might not find all the available windows. For example, creating a tool window can be an expensive operation. To increase performance, Visual Studio won't create a tool window until one is specifically asked for, and, because the numerical indexing method looks only for windows that have been created, a particular tool window might not be found.

A simple experiment shows how iterating through the list of all tool windows slows down your code if all tool windows haven't been created. By default, the Server Explorer tool window is docked and hidden on the left side of the Visual Studio main window. If you move the mouse pointer over the icon for this window, the Server Explorer window appears. If this window hasn't yet been shown for that instance of Visual Studio, you'll see a delay of a couple seconds while the window is created before being shown for the first time. If you run the EnumWindows2 macro and some of the Window objects need to be created, creating those windows will consume a lot of processor time, causing the macro to run very slowly.

Another way to find a window is to index the Windows collection by using the name of the window. The following macro demonstrates this approach; it uses the name of the Task List tool window to find the Window object for the Task List.

Sub FindTaskListWindow()
    Dim objWindow As EnvDTE.Window
    objWindow = DTE.Windows.Item("Task List")
End Sub

This is also not the best way of finding a particular Window object, as this example clearly shows. During a search for a window, the string passed to the Windows.Item method is compared with the title of each window until a window with a matching title is found. If you right-click on the Task List and choose Show Tasks | Comment, the title of this window becomes "Task List – X Comment tasks shown (filtered)," where X is a number. Because the string Task List passed to the Item method doesn't exactly match the title of the Task List window, the code Windows.Item("Task List") won't find the Window object. This isn't to say that you can't use the title indexing method in some situations. Some windows, such as the Properties window or Object Browser window, have names that don't change (unless the user is using a different language), and you can find such windows by using the window title as the index. Another reason why passing the title of a window isn't the best choice for the Item method is because, just as in the case of a numerical index, if the tool window hasn't been created, the Window object won't be found.

The best way to find a Window object is to use an index that is unique and independent of both the position within the Windows collection and the title of the window. Each tool window has a constant globally unique identifier (GUID) assigned to it; you can pass this GUID to the Item method to find the window you need. Because a GUID might be hard to remember, most of the tool windows that Visual Studio can create have constants defined that are easier to remember and recognize. These constants all start with the prefix vsWindowKind and are static (shared if you're using the Visual Basic language) members of either the EnvDTE.Constants class or the EnvDTE80.WindowKinds class. The following macro finds the Task List tool window:

Sub FindTaskListWindow2()
    Dim objWindow As EnvDTE.Window
    objWindow = DTE.Windows.Item(EnvDTE.Constants.vsWindowKindTaskList)
End Sub

Because the GUID is unique to a specific tool window and doesn't change over time, you don't need to worry about either the caption of a window or its position within the EnvDTE. Windows collection changing. One other benefit of using the GUID is that even if the window you're searching for hasn't yet been created, Visual Studio is advanced enough to create the tool window when it's requested.

You might occasionally run across a window that doesn't have a constant GUID defined for it. The Source Control Explorer window is an example. When you need to find such a window, you can use the GUID in the form of a string in place of one of the predefined constants, as shown in the following example, which retrieves the Window object for the Source Control Explorer window:

Sub FindTheSourceControlExplorerWindow()
    Dim window As EnvDTE.Window
    window = DTE.Windows.Item("{99B8FA2F-AB90-4F57-9C32-949F146F1914}")
End Sub

You can find the GUID that can be passed to the Item method by using the ObjectKind property. The following macro takes this approach to display the GUID for the Favorites window:

Sub FindTheSourceControlExplorerWindow2()
    Dim window As EnvDTE.Window
    'You should show the Source Control Explorer window
    ' before calling this code!
    window = DTE.Windows.Item("Source Control Explorer")
    MsgBox(window.ObjectKind)
End Sub

When you run this macro, the GUID for the Source Control Explorer window is displayed in a message box. You can then define a constant set to this GUID, and use this constant in any code that needs to find this window. This is how we found the GUID for the FindTheSourceControlExplorerWindow macro.

Using the Object Property

Many windows in Visual Studio have an object model that you can use to manipulate the data contained in that window. You can find these window-specific objects by using the Object property of the Window object. For example, calling the Object property of the Window object for the Task List window returns the TaskList object, which allows you to enumerate, add, remove, and change properties of task items in the Task List window. The following macro retrieves the TaskList object:

Sub GetTaskListObject()
    Dim window As EnvDTE.Window
    Dim taskList As EnvDTE.TaskList
    window = DTE.Windows.Item(EnvDTE.Constants.vsWindowKindTaskList)
    taskList = CType(window.Object, EnvDTE.TaskList)
End Sub

A number of types are available as the programmable object for the different windows, not just the TaskList object, as shown in the macro. Table 9-1 lists the GUID constant you pass to the Item method to find a Window object, as well as the programmable object for that window.

Table 9-1. Windows and Their Programmable Objects

Window

GUID Constant

Object Type

Command Window

vsWindowKindCommandWindow

EnvDTE.CommandWindow

Macro Explorer

vsWindowKindMacroExplorer

EnvDTE.UIHierarchy

Output window

vsWindowKindOutput

EnvDTE.OutputWindow

Server Explorer

vsWindowKindServerExplorer

EnvDTE.UIHierarchy

Solution Explorer

vsWindowKindSolutionExplorer

EnvDTE.UIHierarchy

Error List

vsWindowKindErrorList

EnvDTE80.ErrorList

Task List

vsWindowKindTaskList

EnvDTE.TaskList

Toolbox

vsWindowKindToolbox

EnvDTE.ToolBox

Web browser window

vsWindowKindWebBrowser

SHDocVw.WebBrowser

Text editor

<None>

EnvDTE.TextWindow

Forms designer

<None>

System.ComponentModel.Design.IDesignerHost

HTML designer

<None>

EnvDTE.HTMLWindow

Not only do some of the tool windows in Visual Studio have an object model, but a couple of the document windows have an object model, as well. The Window.Object property of the text editor, .NET Forms designer, and HTML designer windows returns an object appropriate for programming that window object. The object for programming the .NET Forms designer windows is discussed later in this chapter; the objects for programming the text editor and HTML editor windows are discussed in Chapter 10.

Shortcuts to Common Tool Windows

Although acquiring the specific object behind a tool window is not too terribly complicated, Visual Studio makes it easy to get to some of the most common tool window objects by using the ToolWindows object. This object gives you direct access to the object behind the Command Window, the Output window, Solution Explorer, Error List, Task List, and Toolbox. The ToolWindows object can be found on the DTE2 object with code such as this very simple macro:

Sub FindTaskList()
    Dim taskList As TaskList

    taskList = CType(DTE, DTE2).ToolWindows.TaskList
End Sub

The ToolWindows object also gives you quick access to the specific object of other tool windows with the GetToolWindow property. This property accepts as a parameter the GUID for a tool window—the same GUID you would pass to the Windows.Item method, and returns the same value returned from a window's Object property, meaning that this line of code:

taskList = DTE.Windows.Item(EnvDTE.Constants.vsWindowKindTaskList).Object

is equivalent to this line of code:

taskList = CType(DTE, DTE2).ToolWindows.GetToolWindow _
(EnvDTE.Constants.vsWindowKindTaskList)

The Main Window

Each tool and document window in Visual Studio has a Window object available. However, Visual Studio is also a window, so it's only fair that a Window object be available for that window, as well. Rather than indexing the EnvDTE.Windows collection to find this Window object, you use the MainWindow property of the DTE object:

Sub FindTheMainWindow()
    Dim mainWindow As  EnvDTE.Window
    mainWindow = DTE.MainWindow
End Sub

When you work with the Window object for the Visual Studio main window, a few methods and properties don't work as they do when you work with tool or document Window objects. The differences between tool and document Window objects and the Window object for the main window are as follows:

  • The Document, Selection, Object, ProjectItem, and Project properties return null if you're using Microsoft Visual C#® or Visual J#®, and they return Nothing if you're using Microsoft Visual Basic®.

    The set versions of the Caption and Linkable properties generate an exception if called.

  • IsFloating and AutoHides generate an exception if you call the get or set versions of these properties.

  • The Close method generates an exception if called.

Whereas a number of methods and properties don't work on the Window object for the main window, one property is available only for the main window. If an add-in or a macro needs to display a dialog box, you should supply a parent window when the dialog box is shown to correctly manage focus and set the "modalness" of the new window. You can use the main Visual Studio window as the parent window by calling the Window.HWnd property. This property returns a handle to a window—a Windows platform SDK HWND data type. This property is hidden, so when you develop your add-in or macro, it doesn't appear within statement completion. Because the .NET Framework can't use HWND values as a parent, this handle must first be wrapped by a class that implements an interface that the .NET library can accept as a parent. You can implement this interface, System.Windows.Forms.IWin32Window, on your add-in class or on a separate class within a macro project. The IWin32Window interface has one property named Handle; this property returns a System.IntPtr, which contains the handle to a parent window and, in this case, is the value returned from the Window.HWnd property. When it's time to show a form by using the Form.ShowDialog method, you can pass the class that implements the IWin32Window as an argument to this method.

To implement IWin32Window for an add-in, you must first add it to the interface list for your add-in, as shown here:

public class Connect : Object, Extensibility.IDTExtensibility2,
System.Windows.Forms.IWin32Window

Next, you add the implementation of the Handle property:

//Implementation of the IWin32Window.Handle property:
public System.IntPtr Handle
{
    get
    {
        return new System.IntPtr (applicationObject.MainWindow.HWnd);
    }
}

Finally, you can display a form (assuming that a form class named Form1 exists within an add-in project) by using code such as this:

Form1 form1 = new Form1();
form1.ShowDialog(this);

Implementing this interface within a macro is even easier; the macro samples project that is installed with Visual Studio already contains the code for a class that implements this interface. Located in the Utilities module of the Samples project, this class, named WinWrapper, can be instantiated and passed to any code that requires a parent window, such as the standard Open File dialog box:

Sub ShowFileOpenDialog()
    Dim openFile As New OpenFileDialog
    openFile.ShowDialog(New WinWrapper)
End Sub

All you do is copy the WinWrapper class into your macro project, and it's ready to use.

Explorer Windows and the UIHierarchy Object

User interface hierarchy (or UI hierarchy) windows are tool windows that use a tree-like structure to display their data. Examples include the Solution Explorer, Server Explorer, and Macro Explorer windows. The UIHierarchy object and its associated objects, UIHierarchyItems and UIHierarchyItem, are so named because they represent a hierarchy of objects displayed in a tool window. The UIHierarchy object is used extensively by the macro recorder, allowing it to record the correct code to modify the selection within a UI hierarchy window; you can also use the UIHierarchy object as a valuable source of information about what is contained within these tool windows.

The UIHierarchy Object Tree

The UIHierarchy, UIHierarchyItems, and UIHierarchyItem objects work recursively. The UIHierarchy object is used to find the UIHierarchyItems collection, which contains all the root items of the tree within a UI hierarchy window. Each root tree item is represented by a UIHierarchyItem object within the UIHierarchyItems collection, and, because all of these tree items can themselves contain subitems, the UIHierarchyItem.UIHierarchyItems property returns a UIHierarchyItems collection. This pattern of tree nodes returning a collection of other nodes continues until that branch of the tree ends. The following macro uses the UIHierarchy object to find and display the name of the top-level node of Macro Explorer:

Sub GetTopLevelUIHierItems()
    Dim macroExplWin As Window
    Dim uiHierarchy As EnvDTE.UIHierarchy
    Dim uiHierarchyItems As EnvDTE.UIHierarchyItems
    'Find the macro explorer window, and the UIHierarchy
    ' object for this window:
    macroExplWin = DTE.Windows.Item(Constants.vsWindowKindMacroExplorer)
    uiHierarchy = macroExplWin.Object
    'Get the top level collection of items:
    uiHierarchyItems = uiHierarchy.UIHierarchyItems
    'Display the name of the first node in this collection:
    MsgBox(uiHierarchyItems.Item(1).Name)
End Sub

Here, Macro Explorer's UIHierarchy object is found and the collection of UIHierarchyItems is retrieved. The name displayed is that of the first item in the collection, which in this case is Macros because the top-level node in Macro Explorer is always the Macros node.

Continuing with our example, the Macros node in the Macro Explorer window contains a number of macro projects. Because this node can have subitems, it is a container of UIHierarchyItem objects, so the UIHierarchyItem.UIHierarchyItems property returns a collection object. This UIHierarchyItems collection contains a list of all the macro projects, and if we modify the earlier macro, we can walk the list of the macro projects:

Sub WalkMacroProjects()
    Dim macroExplWin As Window
    Dim uiHierarchy As EnvDTE.UIHierarchy
    Dim uiHierarchyItems As EnvDTE.UIHierarchyItems
    Dim uiHierarchyItem As EnvDTE.UIHierarchyItem
    Dim uiHierarchyItem2 As EnvDTE.UIHierarchyItem
    'Find the Macro Explorer window, and the UIHierarchy
    ' object for this window:
    macroExplWin = DTE.Windows.Item(Constants.vsWindowKindMacroExplorer)
    uiHierarchy = macroExplWin.Object
    'Get the first node in this collection, the Macros node:
    uiHierarchyItem = uiHierarchy.UIHierarchyItems.Item(1)
    'Walk all the items in this collection, which is
    ' the list of macro projects:
    For Each uiHierarchyItem2 In uiHierarchyItem.UIHierarchyItems
        MsgBox(uiHierarchyItem2.Name)
    Next
End Sub

These sample macros show how to walk the hierarchy shown in the Macro Explorer window. To use this code to look at what is contained in the Solution Explorer and Server Explorer windows, you can simply change the value passed to the Windows.Item method to Constants.vsWindowKindSolutionExplorer or Constants.vsWindowKindServerExplorer.

Note

Do the UIHierarchy objects seem familiar? Walking the UIHierarchy, UIHierarchyItems, and UIHierarchyItem objects to find an item in a UI hierarchy window is similar to using ProjectItems and ProjectItem to walk a project to find a project item. The reason for this similarity is that the UIHierarchy objects were designed to reflect how you would use the ProjectItem and ProjectItems objects.

The UIHierarchy Object

Finding a specific node within a UI hierarchy window can involve a great deal of code, especially if the desired node is nested more than two levels deep. Using the UIHierarchy. GetItem method, you can directly find a UIHierarchyItem object of a node rather than writing a lot of code to traverse the tree of nodes. For example, if you want to get to the UIHierarchyItem object of the InsertDate macro located in the VSEditor module of the Samples macro project, you can write code such as this:

Sub FindUIHierItemForInsertDateMacro()
    Dim macroExplWin As Window
    Dim uiHierarchy As EnvDTE.UIHierarchy
    Dim uiHierarchyItem As EnvDTE.UIHierarchyItem
    Dim uiHierarchyItems As EnvDTE.UIHierarchyItems
    macroExplWin = DTE.Windows.Item(Constants.vsWindowKindMacroExplorer)
    uiHierarchy = macroExplWin.Object
    uiHierarchyItems = uiHierarchy.UIHierarchyItems
    uiHierarchyItem = uiHierarchyItems.Item("Macros")
    uiHierarchyItems = uiHierarchyItem.UIHierarchyItems
    uiHierarchyItem = uiHierarchyItems.Item("Samples")
    uiHierarchyItems = uiHierarchyItem.UIHierarchyItems
    uiHierarchyItem = uiHierarchyItems.Item("VSEditor")
    uiHierarchyItems = uiHierarchyItem.UIHierarchyItems
    uiHierarchyItem = uiHierarchyItems.Item("InsertDate")
    MsgBox(uiHierarchyItem.Name)
End Sub

This bit of code is quite verbose, however, and we can shorten it by using the UIHierarchy.GetItem method:

Sub FindUIHierItemForInsertDateMacro2()
    Dim macroExplWin As Window
    Dim uiHierarchy As EnvDTE.UIHierarchy
    Dim uiHierarchyItem As EnvDTE.UIHierarchyItem
    macroExplWin = DTE.Windows.Item(Constants.vsWindowKindMacroExplorer)
    uiHierarchy = macroExplWin.Object
    uiHierarchyItem = _
        uiHierarchy.GetItem("MacrosSamplesVSEditorInsertDate")
    MsgBox(uiHierarchyItem.Name)
End Sub

UIHierarchy.GetItem accepts a string, which is the path to an item that pinpoints a node within the hierarchy. This path is calculated by taking the names of each node in the branch to the tree node that you want to find, separated by the forward slash () character.

The UIHierarchy.SelectedItems property returns an array of UIHierarchyItem objects for items that are selected within the UI hierarchy tree. As do other arrays returned by the object model when you're using a language supported by .NET, this property returns an array of untyped objects—an array of System.Object.

Sub GetUIHierSelectedItems()
    Dim macroExplWin As Window
    Dim uiHierarchy As EnvDTE.UIHierarchy
    Dim selectedItems As Object()
    Dim uiHierarchyItem As EnvDTE.UIHierarchyItem
    macroExplWin = DTE.Windows.Item(Constants.vsWindowKindMacroExplorer)
    uiHierarchy = macroExplWin.Object
    selectedItems = uiHierarchy.SelectedItems
    For Each uiHierarchyItem In selectedItems
        MsgBox(uiHierarchyItem.Name)
    Next
End Sub

To help the macro recorder record the movement of selections in a UI hierarchy window, the UIHierarchy object has two methods, SelectUp and SelectDown, that simulate the user selecting nodes within the tree. Both methods take two parameters as arguments. The first parameter is of type EnvDTE.vsUISelectionType, which denotes how nodes should be selected and closely reflects how the keyboard and mouse can be used to select particular nodes. EnvDTE.vsUISelectionTypeSelect selects a single node within the tree, causing any other selected node or nodes to lose their selection state. EnvDTE.vsUISelectionTypeExtend selects from the last selected node to the chosen node, much as if the user had clicked a node while holding down the Shift key. EnvDTE.vsUISelectionTypeSetCaret doesn't select a node—it moves the caret within the tree to the specified node. Lastly, EnvDTE.vsUISelectionTypeToggle swaps the selection state of a node, setting the selection if the node isn't selected or clearing the selection if it is selected. The second parameter of the SelectUp and SelectDown methods is a count parameter. By default, only one item is selected in either the up or down direction, but you can supply a different value so more than one node can be selected at one time.

The UIHierarchy object also has a method named DoDefaultAction. This method simulates the user pressing the Enter key with one or more nodes selected in the tree. For example, if a macro node is selected in Macro Explorer and the UIHierarchy.DoDefaultAction method is called, that macro runs.

The UIHierarchyItems Object

The EnvDTE.UIHierarchyItems object is a collection of EnvDTE.UIHierarchyItem objects and works as any other collection object in the Visual Studio object model. This object supports one property that is not part of the standard set of methods and properties of other collection objects: the Expanded property. This property is of type Boolean and returns true if the nodes underneath the UIHierarchyItem collection are shown in the user interface, and false otherwise. Setting this property to True has the same effect as the user clicking the plus symbol next to a tree view item; setting it to False is the same as the user clicking the minus symbol.

The UIHierarchyItem Object

The EnvDTE.UIHierarchyItem, being a collection item, supports the standard collection item methods and properties, such as Collection and Name. It also supports a method named Select. This method is similar to the UIHierarchy.SelectUp and UIHierarchy.SelectDown methods, except that it works on only one node at a time—the UIHierarchyItem that the Select method was called on. Because the Select method modifies only the current UIHierarchyItem, it doesn't accept a number of items to select.

Calling the UIHierarchyItem.Object property returns the extensibility object, if one is available, for that node. For example, when you're using Solution Explorer, you can retrieve an EnvDTE.Project or EnvDTE.ProjectItem object behind that node by using the Object property. The following code finds the UIHierarchyItem for the first project and second item within that project (the second item is searched for because the first item, when a .NET project is loaded, is the References node) and gets the EnvDTE.Project and EnvDTE. ProjectItem objects for those nodes:

Sub GetUIHierItemObject()
    Dim uihier As EnvDTE.UIHierarchy
    Dim uihierProj As EnvDTE.UIHierarchyItem
    Dim uihierProjItem As EnvDTE.UIHierarchyItem
    Dim project As EnvDTE.Project
    Dim projItem As EnvDTE.ProjectItem
    uihier = DTE.Windows.Item( _
        Constants.vsWindowKindSolutionExplorer).Object
    uihierProj = uihier.UIHierarchyItems.Item(1).UIHierarchyItems.Item(1)
    project = uihierProj.Object
    uihierProjItem = uihierProj.UIHierarchyItems.Item(2)
    projItem = uihierProjItem.Object
End Sub

The Toolbox Window

The Toolbox stores controls and code snippets that you can drag onto the Forms Designer window, text editor windows, and nearly anything else that can be a drag-and-drop target. The Toolbox is made up of a set of pages, or tabs, where items can be stored and grouped into related categories.

Tabs and Items

To find the Toolbox window, you can pass the constant vsWindowKindToolbox to the Windows.Item method, which returns a Window object. The ToolBox object is then found by calling the returned object's Window.Object property, as shown here:

Sub FindTheToolBox()
    Dim toolBoxWindow As EnvDTE.Window
    Dim toolBox As EnvDTE.ToolBox
    toolBoxWindow = DTE.Windows.Item(Constants.vsWindowKindToolbox)
    toolBox = toolBoxWindow.Object
End Sub

Because the Toolbox can contain more than one tab, a collection is available to enumerate all these tabs. You find this collection, the ToolBoxTabs object, by calling the ToolBox. ToolBoxTabs property. Using the ToolBoxTabs collection, you can enumerate each ToolBoxTab object in the Toolbox and even create new tabs to house components or text fragments of your choosing. To create a new tab, you use the ToolBoxTabs.Add method, which takes as an argument the name of the new tab to create and returns a ToolBoxTab object for the newly created tab. The following macro adds a new Toolbox tab:

Sub AddNewToolBoxTab()
    Dim toolBoxWindow As EnvDTE.Window
    Dim toolBox As EnvDTE.ToolBox
    toolBoxWindow = DTE.Windows.Item(Constants.vsWindowKindToolbox)
    toolBox = toolBoxWindow.Object
    toolBox.ToolBoxTabs.Add("My commonly used items").Activate()
End Sub

This code creates a new tab called My Commonly Used Items, and the Activate method of the ToolBoxTab object makes sure it's the selected tab.

Not only is the Toolbox a collection of tabs, but each tab is also a collection of items. Each collection item is represented in the object model by a ToolBoxItem object and can be enumerated by using the ToolBoxItems object, which is found by calling the ToolBoxTab. ToolBoxItems property. You can walk the entire contents of the Toolbox by using the EnumerateToolBoxContents macro, shown here:

Sub EnumerateToolBoxContents()
    Dim toolBoxWindow As EnvDTE.Window
    Dim toolBox As EnvDTE.ToolBox
    Dim toolBoxTab As ToolBoxTab
    Dim outputWindow As New _
        InsideVSNET.Utilities.OutputWindowPaneEx(DTE, "Toolbox contents")
    toolBoxWindow = DTE.Windows.Item(Constants.vsWindowKindToolbox)
    toolBox = toolBoxWindow.Object
    For Each toolBoxTab In toolBox.ToolBoxTabs
        Dim toolBoxItem As ToolBoxItem
        outputWindow.WriteLine(toolBoxTab.Name)
        For Each toolBoxItem In toolBoxTab.ToolBoxItems
            outputWindow.WriteLine(vbTab + toolBoxItem.Name)
        Next
    Next
End Sub

Once you find a ToolBoxItem object, you'll see that you can't do much with it. You can call the Select method to make sure it's the active item in the Toolbox, you can remove the item by using the Delete method, and you can find the label that's displayed in the user interface by using the Name property. Although the object model of a ToolBoxItem is a functional dead end, the real power that the Toolbox object model offers you is the ability to create new items.

Adding Items to the Toolbox

The Toolbox can hold different types of objects, such as text, HTML, COM components, and .NET components. You can add your own items by using the ToolBoxTab.Add method, which takes three parameters. The first parameter, Name, is the display name of the item added; this string is the text that will be displayed within the Toolbox user interface. The second parameter, Data, defines the information stored in the Toolbox for the item. How this data is formatted depends on the third parameter, Format, which is of type vsToolBoxItemFormat.

The simplest data type that can be stored is raw text. The string passed to the Data parameter is copied verbatim into the Toolbox item, and when the text is dragged onto a window that supports drag-and-drop with a Clipboard format of type text (such as a text editor window), it is copied into that window. To add a text fragment, you can use code like this:

Sub AddTextToTheToolBox()
    Dim toolBoxWindow As EnvDTE.Window
    Dim toolBox As EnvDTE.ToolBox
    Dim toolBoxTab As EnvDTE.ToolBoxTab
    Dim toolBoxItems As EnvDTE.ToolBoxItems
    toolBoxWindow = DTE.Windows.Item(Constants.vsWindowKindToolbox)
    toolBox = toolBoxWindow.Object
    toolBoxTab = toolBox.ToolBoxTabs.Item("General")
    toolBoxItems = toolBoxTab.ToolBoxItems
    toolBoxItems.Add("My Text", "This is some text", _
        vsToolBoxItemFormat.vsToolBoxItemFormatText)
End Sub

This code starts by walking the object model and finding the General tab of the Toolbox. It ends by calling the ToolBoxItems.Add method and adding an item labeled My Text with the text This is some text that has the Clipboard format of type text.

Adding text in the HTML format is similar to adding plain text—the differences are that rather than passing raw text, you need to pass a fragment of HTML code, and the format of the data is marked as HTML by using vsToolBoxItemFormatHTML:

Sub AddHTMLToTheToolBox()
    Dim toolBoxWindow As EnvDTE.Window
    Dim toolBox As EnvDTE.ToolBox
    Dim toolBoxTab As EnvDTE.ToolBoxTab
    Dim toolBoxItems As EnvDTE.ToolBoxItems
    toolBoxWindow = DTE.Windows.Item(Constants.vsWindowKindToolbox)
    toolBox = toolBoxWindow.Object
    toolBoxTab = toolBox.ToolBoxTabs.Item("General")
    toolBoxItems = toolBoxTab.ToolBoxItems
    toolBoxItems.Add("My HTML", "<b>This is bold HTML</b>", _
        vsToolBoxItemFormat.vsToolBoxItemFormatHTML)
End Sub

After you run this code, a fragment of HTML is placed onto the Toolbox; if you drag that Toolbox item into the HTML designer, text will appear in bold style.

Note

Remember that HTML is really just an application of XML that follows a particular schema. Because HTML is XML, you can also store XML fragments as HTML on the Toolbox. Visual Studio not only lets you drag-and-drop these HTML/XML fragments into an HTML document, but it also allows you to drag them into an XML document. In fact, a better name for the vsToolBoxItemFormatHTML value would have been vsToolBoxItemFormatXML.

Along with these two text formats, the Toolbox can also store ActiveX® controls, which can be dragged into HTML-designer or Win32® applications (such as a Microsoft Foundation Classes [MFC] dialog box) that support hosting ActiveX controls. To add an ActiveX control, supply the vsToolBoxItemFormatGUID data type. The format of the Data argument is the Class Identifier (CLSID) GUID of the ActiveX control or (despite the name of the format type) the ProgID of the control. The following macro adds two copies of the Windows Media® Player control to the Toolbox. The first one is added by using the CLSID of the control, and the second is added based on its ProgID:

Sub AddCOMObjectToTheToolBox()
    Dim toolBoxWindow As EnvDTE.Window
    Dim toolBox As EnvDTE.ToolBox
    Dim toolBoxTab As EnvDTE.ToolBoxTab
    Dim toolBoxItems As EnvDTE.ToolBoxItems
    toolBoxWindow = DTE.Windows.Item(Constants.vsWindowKindToolbox)
    toolBox = toolBoxWindow.Object
    toolBoxTab = toolBox.ToolBoxTabs.Item("General")
    toolBoxItems = toolBoxTab.ToolBoxItems
    toolBoxItems.Add("Name", "{22D6F312-B0F6-11D0-94AB- 0080C74C7E95}", _
        vsToolBoxItemFormat.vsToolBoxItemFormatGUID)
    toolBoxItems.Add("Name", "MediaPlayer.MediaPlayer.1", _
        vsToolBoxItemFormat.vsToolBoxItemFormatGUID)
End Sub

When you run this code, you'll notice that the Name parameter is ignored. This is because the Toolbox extracts the name from the control.

Note

.NET User Controls can be added to the toolbox by using the vsToolBoxItemFormat. vsToolBoxItemFormatDotNETComponent enumerated value. However, this method has been deprecated in favor of using the Content Installer to install controls, as we discussed in Chapter 4. The Content Installer gives you much better control over where items are installed into the toolbox and makes installing and uninstalling controls much easier.

The Task List Window

As you saw earlier, the programmable object behind the Task List window is the EnvDTE.TaskList object. Using the TaskList object, you can add new task items to provide information to the user about work that needs to be performed, as well as examine tasks added by a compiler or other tool.

Task List Items

The EnvDTE.TaskList object lets you get to the items in the Task List window by calling the TaskItems property, which returns a TaskItems collection containing one item for each task item in the Task List window. You can view subsets of the items in the Task List window by filtering out items that don't belong to a particular grouping, or category, but items that are hidden because of this filtering will still have an item in the EnvDTE.TaskItems collection.

As with any other collection, you can index EnvDTE.TaskItems by its numerical position, which returns an EnvDTE.TaskItem object. You can use a number as an index to this collection, but it doesn't have a string format as an index.

Adding New Tasks

You can add new items to the Task List window to build a wide range of new tools. Examples of tools you can build that use the Task List window include:

  • Code analysis tools that find common programming errors, letting you find a bug before your customer does. You can place details about these errors in the Task List window alongside compiler errors. These tools are sometimes called Lint tools.

  • Scheduling tools that pull information from other software such as Microsoft Project and create task items to let programmers know when a specific portion of their work is due. When the check box next to a task item is selected, the corresponding item in Project is marked as completed.

  • An add-in that searches through compiler errors and fixes as many as it can. Remember the last time you compiled a C# project, only to have errors generated because you forgot a semicolon? Wouldn't it be great to have a tool to fix this automatically?

  • A macro that synchronizes your calendar in Microsoft Outlook® with the Visual Studio Task List window, reminding you, among other things, to pick up a gift on your way home from work for an anniversary or a birthday. (Such a tool can save you a lot of grief.)

You can build such tools because you can insert new task items into the Task List window by using the TaskItems.Add method. TaskItems.Add offers a great deal of flexibility in the elements that are displayed for new task items and how they're displayed. As a result, this method has one of the most complex argument signatures of all the methods in Visual Studio:

public EnvDTE.TaskItem Add(string Category,
    string SubCategory,
    string Description,
    EnvDTE.vsTaskPriority Priority = vsTaskPriorityMedium,
    object Icon,
    bool Checkable = false,
    string File = "",
    int Line = -1,
    bool CanUserDelete = true,
    bool FlushItem = true)

You can use the sample add-in AddTaskListItems to see the output generated by the many combinations of these parameters. We'll look at each parameter in turn over the next few sections.

Category and SubCategory

All tasks, whether they're created by the automation object model or by Visual Studio 2005 itself, belong to a category. Categories are used simply to group tasks and relate them to one another. Common category types are compile errors, user tasks, and shortcuts. You can create new category groups by using the Category parameter of the Add method. When you call the method, the list of currently known categories is searched for a category with a name that matches this argument. If one is not found, a new category is added and the new task item is added to this category. If a category with a matching name is found, the new task is added to that existing category group.

Visual Studio doesn't currently use the SubCategory argument of the Add method; your add-in or macro can leave it blank, or use it for internal bookkeeping.

Description

The description of a task appears in the Description column of the Task List window, and the Description argument of the Add method sets this column. This parameter of the Add method and the Category and SubCategory parameters are the only required parameters. Ignoring the optional parameters for now, we'll create our first Task List item by using the following macro code:

Sub TLAddItems()
    Dim taskList As EnvDTE.TaskList
    taskList = DTE.Windows.Item(Constants.vsWindowKindTaskList).Object
    taskList.TaskItems.Add("Category", "", "Description2")
    taskList.TaskItems.Add("Category", "", "Description1")
End Sub

Priority

The next argument you can pass to the Add method is the Priority argument. This argument is optional when you use the Visual Basic programming language, but if it's supplied, an icon appears in the first column of the Task List—the priority column—to remind the user of the importance of completing that task. A high-priority task has a red exclamation point next to it, a low-priority task has a blue downward-pointing arrow, and a medium-priority task has no priority icon. The following macro adds new task items to the Task List, each with a different priority.

Sub TLAddItemsPriority()
    Dim taskList As EnvDTE.TaskList
    taskList = DTE.Windows.Item(Constants.vsWindowKindTaskList).Object
    taskList.TaskItems.Add("Category", "", _
        Description1", vsTaskPriority.vsTaskPriorityHigh)
    taskList.TaskItems.Add("Category", "", _
        "Description2", vsTaskPriority.vsTaskPriorityLow)
    taskList.TaskItems.Add("Category", "", _
        "Description3", vsTaskPriority.vsTaskPriorityMedium)
End Sub

Icon

The Icon parameter allows you to place an icon next to a newly added task item to identify that task item. The five predefined icons are described in Table 9-2.

Table 9-2. Predefined Icons for Task List Items

Icon Image

Constant

vsTaskIcon.vsTaskIconComment

vsTaskIcon.vsTaskIconUser

vsTaskIcon.vsTaskIconSquiggle

vsTaskIcon.vsTaskIconShortcut

vsTaskIcon.vsTaskIconCompile

Left out of this table is the default icon, vsTaskIconNone—a blank icon—which appears (or, in this case, does not appear) if this parameter is not specified.

Note

If you call the TaskItems.Add method and supply the value vsTaskIconShortcut as the icon, a shortcut in a file isn't created. The icon is used for display purposes only. This applies to the other values that can be passed as the Icon parameter; using vsTaskIconCompile doesn't create a compiler error, vsTaskIconComment doesn't add a comment to a source file, and so forth.

If these predefined images don't suit the task item you're creating, you can create your own image to display next to the task. You need a 16-by-16-pixel bitmap image with a color depth of either 16 colors or 16,777,215 (24-bit) colors. Any pixel in the image that has a background RGB color of 0,254,0 will bleed through the image, showing the color of the Task List window. The Icon paramater can be set to one of three formats in addition to the previously listed constants. You can pass the handle of a System.Drawing.Bitmap object, which is retrieved from the GetHbitmap method. You can also pass the path as a string to a bitmap file on disk. The final way is to load the bitmap into an IPictureDisp instance and then pass it as the Icon parameter. An IPictureDisp interface is the COM way of passing around a bitmap object. To create an IPictureDisp object in a .NET add-in, you must write a small amount of P/Invoke code to create this object type. (P/Invoke is the technology the .NET Framework uses to call unmanaged code from .NET programs.) The system DLL, oleaut32.dll, exports a method called OleLoadPictureFile that takes a path to a bitmap file, which can be the bitmap to show in the Task List, and returns the necessary IPictureDisp object. Before you call the OleLoadPictureFile method, you must add some code that might seem magical to the class that implements your add-in:

[DllImport("oleaut32.dll",
    CharSet=System.Runtime.InteropServices.CharSet.Auto,
    SetLastError=true)]
internal extern static int OleLoadPictureFile(object fileName,
[MarshalAsAttribute(UnmanagedType.IDispatch)] ref object ipictrueDisp);

This code defines the method signature for code that's exported from the COM DLL OleAut32.dll, and, with this P/Invoke method declared, you can call the OleLoadPictureFile method with the file name of the custom bitmap:

object objIPictureDisp = null;
string filename = "C:SomeImage.bmp";
int nret = OleLoadPictureFile(fileName, ref objIPictureDisp);

When this method call returns, the objIPictureDisp variable is set to an IPictureDisp object that can be passed as the Icon parameter of the TaskItems.Add method.

If you try to call the Add method from within a macro and pass as the Icon parameter an IPictureDisp object or the handle to a bitmap, an exception is generated. This happens because your macros run in a separate process. When a method or property is called on the Visual Studio object model, all data must be marshaled, or translated from the memory being used by the Macros editor program to the memory used by the Visual Studio program, across the process boundaries. However, objects such as IPicture, IPictureDisp, and HBITMAP can't be marshaled across processes, so if you try to create an IPicture, IPictureDisp, or HBITMAP and pass it to the TaskItems.Add method from a macro, an exception will be generated. This limits you to passing only a file path or one of the constants from a macro, but you can create and use a custom bitmap from within an add-in by using IPictureDisp or a handle because add-ins are loaded into the same process as Visual Studio.

Checkable

The Checkable parameter of TaskListItems.Add controls whether the check box appears next to a task item. If it's set to true, the check box is available; if it's set to false, the check box does not appear.

File and Line

File and Line are a string and an integer, respectively, that fill out the File and Line columns of the Task List. These can contain any values you want—they're not used in any way other than for information to display within the Task List. If the user later performs the default action on the task (either double-clicking or pressing the Enter key when the task item is selected), the file won't open and the caret won't be placed on the line specified in the Line argument unless you write a little extra code, which will be discussed shortly.

CanUserDelete

The CanUserDelete parameter controls whether the user can delete the task item by pressing the Delete key when the task is selected in the user interface. If this value is set to false, the user cannot delete the item, but you can still delete it through the object model by calling the TaskItem.Delete method.

FlushItem

The last parameter of TaskListItems.Add is a Boolean value called FlushItem. As each new item is inserted into the Task List, the Task List must be updated to show the new task. If you add a large number of tasks, redrawing the Task List each time an item is added will slow down your application's performance. If you pass a false value as the FlushItem argument, no updates are made to the Task List until either another task item is added that does an update or the method TaskItems.ForceItemsToTaskList is called.

The TaskItem Object

Once an item has been added to the Task List—whether it was created by using the TaskItems. Add object or created by Visual Studio itself and obtained by the TaskItems collection—you can use the TaskItem object's methods and properties to examine and modify the data displayed for that task item. The properties Category, SubCategory, Checked, Description, FileName, Line, and Priority each can be read programmatically to see what data is stored for those columns of the Task List. You can also set these properties as long as they're not read-only. Some task items that Visual Studio creates have their columns marked as read-only so they can't be modified. To test whether a particular column can be set, you can make a call to the IsSettable property, which accepts as a parameter the column within the Task List (a vsTaskListColumn enumeration value), and if the column can be modified, the IsSettable property returns true; otherwise, it returns false. For example, to change a task item's description value, you can write code such as this, which first verifies that the description can be changed:

Sub ModifyTaskDescription()
    Dim taskList As EnvDTE.TaskList
    Dim task As EnvDTE.TaskItem
    taskList = DTE.Windows.Item(Constants.vsWindowKindTaskList).Object
    task = taskList.TaskItems.Item(1)
    If (task.IsSettable(vsTaskListColumn.vsTaskListColumnDescription)) Then
        task.Description = "A new description"
    End If
End Sub

Delete deletes the item, if deletion is possible, from the list of items. As mentioned earlier, all items added through the object model can be deleted whether the CanUserDelete parameter is true or false when you call the TaskItems.Add method. Other task items can be deleted depending on who created them. For example, if the task item was added by the user clicking on the Create User Task button on the command bar of the Task List when the User Tasks category is selected, it can be deleted by using the object model. If the item was created by IntelliSense®, an exception is generated when this method is called because the only way to remove the task item is to modify the source code that's causing the task item to appear.

AutoNavigate

The TaskItem.Navigate method simulates the user double-clicking or pressing the Enter key when the task has the focus. If the task was added by Visual Studio or by a compiler, or if the task is a shortcut task, this opens the target file and places the caret on the line specified by the task. By default, if the task was added through the automation model, no action is taken unless you write code to manually navigate to the proper file and line location by using the TaskNavigated event. However, you can also let Visual Studio handle opening the referenced document and line number through the AutoNavigate parameter of the TaskItems2.Add2 method. TaskItems2.Add2 looks very similar to the TaskItems.Add method, except it takes one additional parameter, AutoNavigate. If this parameter is set to true, when the user double-clicks the task item or calls Navigate on the TaskItem object, then the document path given in the File parameter is opened—or, if the document is already open, it is made active, and then the caret is placed at the beginning of the line indicated by the Line parameter. You should make sure that if you specify true to AutoNavigate, the file is a text document and not a binary file; otherwise, unpredictable results may occur.

Task List Events

As the user interacts with the Task List, events are fired to allow your add-in or macro to respond to those user interactions. Possibly the most important event of your add-in or macro that adds task list items is the TaskNavigate event. This event is fired when the user double-clicks on a Task List item, presses the Enter key when a task has the focus, or chooses Next Task or Previous Task from the Task List's shortcut menu. To capture this event, you can connect to the TaskListEvents.TaskNavigated event. This event is passed the TaskItem object of the item that the user wants to navigate to, plus a reference to a Boolean value called NavigateHandled that you can use to tell Visual Studio whether your code has handled the navigation of the task item. If the value false is passed back through the NavigateHandled argument and no one else handles the navigation of the task, Visual Studio plays a bell sound for the user.

Connecting to this event within a macro project is as simple as opening the EnvironmentEvents macro module, selecting the TaskListEvents item from the drop-down list at the top left of the editor window for this module, and then selecting the TaskNavigated event from the top-right drop-down list. Using this event and the arguments that are passed to it, you can write a macro event handler for the NavigateHandled event that opens the file (if specified) that the task item refers to and select the line in the source file that the task item points to. The code for this event handler would look like this:

Private Sub TaskListEvents_ TaskNavigated(ByVal TaskItem As EnvDTE.TaskItem, _
    ByRef NavigateHandled As Boolean) Handles TaskListEvents.TaskNavigated
    'If the file argument has been specified for this task...
     If (TaskItem.FileName <> "") Then
         Dim fileWindow As EnvDTE.Window
         Dim textWindow As EnvDTE.TextWindow
         Dim textPane As EnvDTE.TextPane

        'Then open the file, find the TextWindow and TextPane objects...
        fileWindow = DTE.ItemOperations.OpenFile(TaskItem.FileName, _
            EnvDTE.Constants.vsViewKindTextView)
        textWindow = CType(fileWindow.Object, EnvDTE.TextWindow)
        textPane = CType(textWindow.ActivePane, EnvDTE.TextPane)

        'Then move the caret to the correct line:
        textPane.Selection.MoveTo(TaskItem.Line, 1, False)
        textPane.Selection.SelectLine()
        NavigateHandled = True
    End If
End Sub

Connecting to this event within an add-in is almost as simple as connecting to it within a macro, but a little more code is needed. The first step is to declare a variable to connect the event handler to. In this case, we're connecting to Task List events, so we'll use the EnvDTE. TaskListEvents class:

private EnvDTE.TaskListEvents taskListEvents;

Next, you declare the event handler method, which must follow the method signature as declared in the Object Browser. You can also convert the macro code shown earlier into C# for an add-in:

public void TaskNavigated(EnvDTE.TaskItem taskItem,
     ref bool navigateHandled)
{
     //If the file argument has been specified for this task...
     if(taskItem.FileName != "")
     {
         EnvDTE.Window fileWindow;
         EnvDTE.TextWindow textWindow;
         EnvDTE.TextPane textPane;

         // Then open the file, find the TextWindow and TextPane objects...
         fileWindow = applicationObject.ItemOperations.OpenFile(
             taskItem.FileName, EnvDTE.Constants.vsViewKindTextView);
         textWindow = (EnvDTE.TextWindow)fileWindow.Object;
         textPane = (EnvDTE.TextPane)textWindow.ActivePane;

         //Then move the caret to the correct line:
         textPane.Selection.MoveTo(taskItem.Line, 1, false);
         textPane.Selection.SelectLine();
         navigateHandled = true;
     }
}

Finally, you must set the taskListEvents variable to an instance of a TaskListEvents object, which you find by calling the Events.TaskListEvents property. This property takes one argument—a category that's used as a filter. If you pass the empty string as an argument, your event handler is called when any task item generates an event—whether the item was added by an add-in or macro or by Visual Studio itself. But if you specify a category for this argument—the same category string you can pass as the first argument to the TaskItems. Add method—only events for a task item that have this same category are sent to your event handler. This filtering mechanism can help cut down on the number of events that are fired, thereby increasing the performance of your code. Because we want our code to handle events for all task items, we'll pass the empty string to the Events.TaskListEvents property:

EnvDTE.Events events = applicationObject.Events;
taskListEvents = (EnvDTE.TaskListEvents)events.get_TaskListEvents("");

The last step is to associate the event object with the event handler. You do this by creating a new EnvDTE._dispTaskListEvents_TaskNavigatedEventHandler object and adding it to the taskListEvents.TaskNavigated collection of event handlers:

taskListEvents.TaskNavigated += new
    _dispTaskListEvents_TaskNavigatedEventHandler(this.TaskNavigated);

TaskNavigated isn't the only Task List event your code can capture. TaskAdded and TaskRemoved events are fired when a new task item is added or just before it's removed, respectively. The last event, TaskModified, is fired when one of the columns of the Task List is modified. For instance, the user can check or uncheck an item or change the priority or descriptive text for a task item. To let your code know when these tasks are changed, the TaskModified event is fired, passing the task item and the column that was modified.

Comment Tokens

Developers commonly leave portions of their code incomplete as they work, with the intention of finishing it later. This omitted code might include error-checking, some parameter validation, or notes to themselves to handle a few additional code paths. Of course, unless you specifically search through the code for these tokens, either by visually inspecting it or by using the text editor search tools, you might never revisit these notes and make the corrections. However, if you use a special notation, the Task List can find and report these notes for you automatically. When you open a source file, the file is scanned for these special tokens, and, if any are found, an entry is made in the Task List. The tokens in the source file have the format of a language comment marker followed by the comment token, the colon (:) character, and finally the note that is to appear within the Task List. For example, the comment //TODO: Fix this later creates a Task List item for Microsoft Visual C++ and C# with the description Fix this later, and the comment 'TODO: Fix this later' does the same for a Visual Basic file.

The special tokens that the Task List searches for are defined in the Options dialog box, where you can add new tokens and remove or modify existing tokens. The default comment tokens are HACK, TODO, UNDONE, and UnresolvedMergeConflict.

You can add a new token by typing a token name in the Name box, selecting a priority, and then clicking the Add button. You can also add, remove, and modify these tokens by using the object model. To program these tokens, use the Properties collection. You'll find more details about the Properties collection later in this chapter—for now, we'll overlook the details of how to use the Properties collection and look only at how to change the tokens by using this object. The first step is to find the CommentTokens property by using code such as the following:

Sub GetTokensArray()
    Dim tokens As Object()
    Dim prop As EnvDTE.Property
    Dim props As EnvDTE.Properties
    props = DTE.Properties("Environment", "TaskList")
    prop = props.Item("CommentTokens")
    tokens = prop.Value
End Sub

The CommentTokens property returns an array of strings that have a special format, and when this macro is run, it finds all the available tokens in the format TokenName: Priority, where TokenName is what should appear after the comment notation for the given language and Priority is the numerical value of an item in the EnvDTE.vsTaskPriority enumeration. In the preceding macro, the string for the TODO token is TODO:2 because the string to search for in the text editor is TODO and the priority that appears in the Task List for this token is vsTaskPriorityMedium (whose numerical value is 2).

Adding your own token to the list of tokens is a three-step process. Setting the list of tokens clears the current list (at least the tokens that are not read-only), so you need to preserve the known tokens so you don't overwrite the known tokens that the user might have created. First, you need to retrieve the list of current Task List tokens. Add your own token to the array of existing tokens, and then set the property with the expanded array. You can see this in the following macro, which adds a high-priority SECURITY token to the list of comment tokens:

Sub AddSecurityToken()
    Dim tokens As Object()
    Dim token As String
    Dim prop As EnvDTE.Property
    Dim props As EnvDTE.Properties
    'Find the property holding the known tokens
    props = DTE.Properties("Environment", "TaskList")
    prop = props.Item("CommentTokens")
    tokens = prop.Value
    Add one to the list of known tokens to hold
    ' the new SECURITY token
    ReDim Preserve tokens(tokens.Length)
    'Add the new token
    tokens(tokens.Length - 1) = "SECURITY:3"
    'Set the list of known tokens
    prop.Value = tokens
End Sub

To delete a token, you run similar code, but instead of adding an element to the array, you remove an element:

Sub RemoveSecurityToken()
    Dim tokens As Object()
    Dim newTokens As Object()
    Dim token As String
    Dim i As Integer = 0
    Dim found As Boolean = False
    Dim prop As EnvDTE.Property
    Dim props As EnvDTE.Properties
    props = DTE.Properties("Environment", "TaskList")
    prop = props.Item("CommentTokens")
    tokens = prop.Value
    'Don't want to shrink the array if
    '  the token is not available
    For Each token In tokens
        If token = "SECURITY:3" Then
            found = True
            Exit For
        End If
    Next
    'If the SECURITY token was not found, then
    ' there is nothing to remove so we can exit
    If found = False Then
        Exit Sub
    End If
    'Resize the newTokens array
    ReDim newTokens(tokens.Length - 2)
    'Copy the list of tokens into the newTokens array
    ' skipping the SECURITY token
    For Each token In tokens
        If token <> "SECURITY:3" Then
            newTokens(i) = token
            i = i + 1
        End If
    Next
    'Set the list of tokens
    prop.Value = newTokens
End Sub

If your add-in generates code to place in the text buffer and you want to insert a comment token that gives the user additional information about how to modify the code, you can use the TaskList.DefaultCommentToken property to find which token to insert. The following code creates a string containing a class, with the default comment token directing the user to where to insert code:

Sub InsertTLTokenCode()
    Dim classString As String
    Dim taskList As EnvDTE.TaskList
    taskList = DTE.Windows.Item(Constants.vsWindowKindTaskList).Object
    classString = "Public Class AClass" + Chr(13)
    classString = classString + Chr(9) + "'" + taskList.DefaultCommentToken
    classString = classString + ": Insert your code here" + Chr(13)
    classString = classString + "End Class"
End Sub

The Error List Window

The error list window looks like and acts very much like the task list, except this window is read-only, meaning you cannot add your own items to it—adding items is reserved only for compilers. When the code is compiled within Visual Studio, the output of the compilers are scanned, and one item is inserted into the error list for each error, warning, or message found. If you were to browse the objects, methods, and properties of the error list, you will find that many of the methods and properties on the ErrorList, ErrorItems, and ErrorItem objects work much like the methods and properties of the TaskList, TaskItems, and TaskItem objects.

The ErrorList object, the object representing the error list tool window in the user interface, can be found either by using the ToolWindows.ErrorList property or the Windows.Item method, passing in the EnvDTE80.WindowKinds.vsWindowKindErrorList constant, and then casting the Window.Object property into an ErrorList type:

    Sub FindErrorList()
        Dim errorList As ErrorList

        'Find the ErrorList object using the Windows.Item(...).Object style
        errorList = DTE.Windows.Item _
(EnvDTE80.WindowKinds.vsWindowKindErrorList).Object
 
        'Find the ErrorList object using the ToolWindows style
        errorList = CType(DTE, DTE2).ToolWindows.ErrorList
    End Sub

Once you have the ErrorList object, you can get to the ErrorItems object, and then to an ErrorItem object. The differences between these objects and their counterparts, TaskList, TaskItems, and TaskItem, are that, obviously, the name Task has been replaced with Error; in objects, methods, and properties, the Add method has been removed from the collection object; and the properties ShowErrors, ShowWarnings, and ShowMessages have been added to the item object. These properties, which take and return a Boolean value, allow you to control and inspect to determine whether errors, warnings, and messages are visible within the error list.

The Output Window

The Output window is where Visual Studio displays text information generated by tools such as compilers or the debugger. The Output window is also a perfect place for any tools you create that generate text information that might be useful to the user. In fact, throughout this book, the sample macros and add-ins use the class library OutputWindowPaneEx to display text in the Output window as these samples do their work.

The object behind the Output window is called OutputWindow, and you can find this object by using code such as this:

Sub FindOutputWindow()
    Dim window As EnvDTE.Window
    Dim outputWindow As EnvDTE.OutputWindow
    window = DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput)
    outputWindow = CType(window.Object, EnvDTE.OutputWindow)
End Sub

Output Window Panes

The user interface of the Output window consists of a number of view ports, or panes, each of which displays text. You can switch between these panes by selecting a pane by name from the drop-down list at the top of the Output window. You can enumerate the panes by using the OutputWindowPanes object, as shown here:

 Sub EnumOutputWindowPanes()
     Dim window As EnvDTE.Window
     Dim outputWindow As EnvDTE.OutputWindow
     Dim outputWindowPanes As EnvDTE.OutputWindowPanes
     Dim outputWindowPane As EnvDTE.OutputWindowPane
     'Find the OutputWindow object
     window = DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput)
     outputWindow = CType(window.Object, EnvDTE.OutputWindow)
     'Retrieve the OutputWindowPanes object
     outputWindowPanes = outputWindow.OutputWindowPanes
     'Enumerate each OutputWindowPane
     For Each outputWindowPane In outputWindowPanes
         MsgBox(outputWindowPane.Name)
     Next
 End Sub

You can also use the OutputWindowPanes object to create new panes. The method Add takes as its only argument the name of the new pane to create:

 Sub CreateOutputWindowPane()
     Dim window As EnvDTE.Window
     Dim outputWindow As EnvDTE.OutputWindow
     Dim outputWindowPanes As EnvDTE.OutputWindowPanes
     Dim outputWindowPane As EnvDTE.OutputWindowPane
     'Find the OutputWindow object
     window = DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput)
     outputWindow = CType(window.Object, EnvDTE.OutputWindow)
     'Retrieve the OutputWindowPanes object
     outputWindowPanes = outputWindow.OutputWindowPanes
     'Add a new pane:
     outputWindowPane = outputWindowPanes.Add("My New Pane")
 End Sub

This macro creates a new output window pane named My New Pane that's ready to be filled with the text output of your add-in or macro code. You can inject text into this window by using the OutputWindowPane.OutputString method, which takes a string that's appended to the end of other text in the appropriate pane. As strings are placed into the Output window pane, they're injected without a line break between them; this means that if a new line character needs to be placed between each string, you must write the code to do this. The following macro sample displays the contents of the folder containing the solution file that's currently open; as each file path is displayed in the Output window pane, a line break (or ASCII value 13) is inserted:

Sub DisplaySolutionDirectory()
    Dim files As String()
    Dim file As String
    Dim directoryOutputWindowPane As OutputWindowPane
    Dim fullName As String
    Dim outputWindow As OutputWindow
    outputWindow = DTE.Windows.Item(Constants.vsWindowKindOutput).Object

    'Find the folder the solution is in, as well as the files that are
    ' in that folder:
    fullName = System.IO.Path.GetDirectoryName(DTE.Solution.FullName)
    files = System.IO.Directory.GetFiles(fullName)

    'Try to find a "Solution Directory" pane, if one does not exist,
    ' create it:
    With outputWindow.OutputWindowPanes
        Try
            directoryOutputWindowPane = .Item("Solution Directory")
            'Show the pane:
            directoryOutputWindowPane.Activate()
        Catch
            directoryOutputWindowPane = .Add("Solution Directory")
        End Try
    End With
    'Clear the pane:
    directoryOutputWindowPane.Clear()
    For Each file In files
        'Display the file path, with a line break between each line
        directoryOutputWindowPane.OutputString(file + Chr(13))
    Next
End Sub

This macro demonstrates the use of a few methods and properties of the OutputWindowPane object. The Activate method makes sure the pane corresponding to the instance of the OutputWindowPane that it's being called on is the same pane displayed to the user; it simulates the selection of that pane from the drop-down list in the Output window. OutputString dumps a string into the pane, and Clear removes all text from that pane. Another property, TextDocument, which isn't shown in this macro, deserves special note. It returns an EnvDTE.TextDocument object for the pane that's read-only—you can retrieve the contents of this window, but not change it. (You can use OutputString only to modify the contents.) We'll discuss this object in further detail in the next chapter.

The Forms Designer Window

You visually create the user interface for your .NET Framework program within the Forms designer. By simply dragging and dropping controls from the Toolbox onto a form and setting a few properties in the Properties window, you can quickly build the user interface for your program. The Forms designer was built with .NET components and uses the System.Windows.Forms assembly to display and create the form. Because the System.Windows. Forms assembly is used, programming a form in the designer is similar to programming a form as it executes at run time.

The IDesignerHost Interface

A Forms designer window exposes an object model, as many of the other windows do. The object hierarchy returned from calling the Window.Object property of a Forms designer window is of type System.ComponentModel.Design.IDesignerHost. To examine and modify a form within the designer, you must find the System.Windows.Forms.Control object for that form. You can do this by calling the IDesignerHost.RootComponent property and casting the object returned into a Forms.Control object, as shown in this code snippet:

System.Windows.Forms.Control control;
System.ComponentModel.Design.IDesignerHost designerHost;
designerHost =(System.ComponentModel.Design.IDesignerHost)
    applicationObject.ActiveWindow.Object;
control = (System.Windows.Forms.Control)designerHost.RootComponent;

Note

The System.Windows.Forms.Form class derives from the System.Windows.Forms.Control class. The code shown here demonstrates how to manipulate a user control, but you can use the same unmodified code to program a Windows Form.

Using the System.Windows.Forms.Control object, you can connect events to determine when the form was modified; to find and modify properties such as the dimensions of the form; and to place, modify, and remove controls from the form.

Marshaling

If you try to use the IDesignerHost interface from within a macro, a System.Runtime.Remoting.RemotingException exception is thrown. This is because user interface elements, such as the System.Windows.Control object, cannot be remoted across process boundaries. Remember that the Macros IDE runs in a process separate from the Visual Studio process. Because of this restriction, the designer object model can be used only within an add-in and not from a macro.

Adding Controls to a Form

Once you find the IDesignerHost interface for a Forms designer, you can easily add new controls to the form. To add a control, you need the System.Type object that describes the control. You can find this object by using the Type.GetType static method, which is passed the full class name of a control. For example, to add a list view control to a form, you can use code such as this:

System.Type type;
type = System.Type.GetType("System.Windows.Forms.ListView");

You can then pass this Type object to the IDesignerHost.CreateComponent method to create the control. This method has two overloads, the first of which takes two parameters. The first parameter is the Type object we just found, and the second parameter is the variable name of the control we want to create. This variable name must be unique among variables contained within the form's class; otherwise, a name collision will occur and an exception will be generated. The second overload of this method takes as an argument only the Type object; the Forms designer examines the form code to find a unique variable name to use. Both of these overloads emit the appropriate code to instantiate a control of the specified type. The following code creates a list view control with the variable name listViewControl:

System.ComponentModel.IComponent component;
component = designerHost.CreateComponent(type, "listViewControl");

If you were to add this code to an add-in and execute it, you wouldn't see the control appear on the form. This is because the control, while instantiated, hasn't been parented to the form and added to the form's Controls collection. To add the control to the form's Controls collection, you must set the Parent property of the control to the form that should contain the control. You can set the Parent property (or any property of a control, for that matter) by using the System.ComponentModel.PropertyDescriptorCollection object. This object contains a collection of properties available for a control; as values are set for the properties they contain, code is generated within the form's class that corresponds to the property you set. You can set the Parent property as follows:

System.ComponentModel.PropertyDescriptorCollection props;
System.ComponentModel.PropertyDescriptor propParent;
//Find the properties for the listViewControl control:
props = System.ComponentModel.TypeDescriptor.GetProperties(component);
//Get the Parent property
propParent = props["Parent"];
//Set the Parent property to the form:
propParent.SetValue(newControl, designerHost.RootComponent);

Finding Existing Controls

You now know how to create controls and place them on a form. But how do you find existing controls on a form? As mentioned earlier, the IDesignerHost.RootComponent property returns an object that can be cast into a System.Windows.Forms.Control object. Using this object, you can call methods and properties just as you would at run time to find information about a form. For example, the following code walks the list of controls contained in a System.Windows.Forms.Control object:

System.ComponentModel.Design.IDesignerHost designer;
System.Windows.Forms.Control rootControl;

//Set the designer variable here from the Window.Object property

rootControl = (System.Windows.Forms.Control)designer.RootComponent
foreach (System.Windows.Forms.Control control in rootControl.Controls)
{
    //Retrieve desired control information
}

You can use the PropertyDescriptorCollection object to find properties of a control much as you would to set properties on the form, except you use the PropertyDescriptor.GetValue method:

System.ComponentModel.Design.IDesignerHost designer;
System.ComponentModel.PropertyDescriptor propControls;
System.ComponentModel.PropertyDescriptorCollection props;
System.ComponentModel.IComponent component;
System.Windows.Forms.Form form;
System.Drawing.Size size;
designer = (System.ComponentModel.Design.IDesignerHost) _
    applicationObject.ActiveWindow.Object;
component = designer.RootComponent;

//Get the Size property using the forms designer:
props = System.ComponentModel.TypeDescriptor.GetProperties(component);
propControls = props["Size"];
size = (System.Drawing.Size)propControls.GetValue(component);

//Get the Size property directly from the form:
form = (System.Windows.Forms.Form)component;
size = form.Size;

A Form Layout Sample

Microsoft Visual Basic versions 6 and earlier have a tool window called Form Layout that shows the size of a form being designed as it would appear on the desktop of your computer. Visual Studio doesn't have this feature, but you can easily create one by using the automation model of the Forms designer. You can find the source code for this sample, called FormView, among the book's sample files.

When the add-in starts, it creates a tool window that draws a virtual monitor representing your computer monitor. The screen of the virtual monitor matches the display resolution of your monitor. (If your computer uses multiple monitors, the resolution of the primary monitor is used.) After connecting to the WindowActivated event, it waits for a Forms designer window to become active, and then it looks at the available controls in the form and draws the form and its controls on the virtual screen. For example, if you create a form that has the calendar and button controls on it, as shown in Figure 9-1 the Forms designer window, shown in Figure 9-2 appears. This sample also demonstrates our next topic, creating your own tool windows that are hosted by Visual Studio.

A Windows Form with a calendar control and a button control

Figure 9-1. A Windows Form with a calendar control and a button control

The Form Layout window showing the form from Figure 9-1

Figure 9-2. The Form Layout window showing the form from Figure 9-1

Creating Custom Tool Windows

As you know, most of the windows in Visual Studio have an object model that you can use to program the contents and present data that your code generates. However, at times you might need to display data in a way that the existing tool windows cannot handle. To allow you to display data in a way that is most suitable for your add-in, the Visual Studio object model allows creation of custom tool windows.

There are two ways to create a tool window—one way is with an ActiveX control, and the other is with a .NET User Control. To create a tool window, all you need is an ActiveX control and an add-in that makes a call to the Windows.CreateToolWindow method. CreateToolWindow has the following method signature:

public EnvDTE.Window CreateToolWindow(EnvDTE.AddIn AddInInst,
    string  ProgID,
    string Caption,
    string GuidPosition,
    ref object DocObj)

This method returns a Window object that behaves as any tool window that Visual Studio creates. Here are the arguments for this method:

  • AddInInst An add-in object that's the sponsor of the tool window. When the sponsor add-in is unloaded, all tool windows associated with that add-in are automatically closed and the ActiveX control is unloaded.

  • ProgID The ProgID of the ActiveX control that's hosted on the newly created tool window.

  • Caption The text to show in the title bar of the new tool window.

  • GuidPosition A GUID in string format. As you'll recall, the Windows.Item method can be indexed by a GUID, and that GUID uniquely identifies a specific window. The GUID assigned to your tool window and the GUID passed to the Windows.Item method are set by using this parameter. This GUID must be different from the GUID used by other tool windows; if you call CreateToolWindow multiple times, you must use a different GUID for each window.

  • DocObject Most ActiveX controls have a programmable object in the form of a COM IDispatch interface, which is mapped to a System.Object when you're using the .NET Framework. The programmable object of the ActiveX control is passed back to the caller through this parameter, which allows you to program the control as you would any other tool window. You can also retrieve the programmable object of the ActiveX control by calling the Object property of the Window object for the tool window that's created by using the CreateToolWindow method.

To demonstrate the use of the CreateToolWindow method, the sample files that accompany this book include an add-in project called VSMediaPlayer. This sample creates a tool window hosting the ActiveX control for Windows Media Player and then, by using the programmable object of the control, plays an audio file. The code that does the work of creating the tool window looks like this:

void CreateMediaPlayerToolWindow()
{
    EnvDTE.Windows windows;
    EnvDTE.Window mediaPlayerWindow;
    object controlObject =  null;
    string mediaPlayerProgID =  "MediaPlayer.MediaPlayer";
    string toolWindowCaption =  "Windows Media  Player";
    string toolWindowGuid =  "{AB5E549E-F823-44BB-8161-BE2BD5D698D8}";

    //Create  and  show a tool   window that  hosts  the
    // Windows Media Player  control:

    windows = applicationObject.Windows;
    mediaPlayerWindow = windows.CreateToolWindow(addInInstance,
                                                    mediaPlayerProgID,
                                                    toolWindowCaption,
                                                    toolWindowGuid,
                                                    ref controlObject);
    mediaPlayerWindow.Visible = true;

    //Play the Windows "Tada" sound:
    //Can get only the system directory (Eg: C:windowssystem32),
    // need to change this to the Windows install dir
    string mediaFile = System.Environment.GetFolderPath(
        System.Environment.SpecialFolder.System);
    mediaFile += "\..\media\tada.wav";
    MediaPlayer.IMediaPlayer2 mediaPlayer =
         (MediaPlayer.IMediaPlayer2)controlObject;
    mediaPlayer.AutoStart = true;
    mediaPlayer.FileName = mediaFile;
}

The CreateMediaPlayerToolWindow method is called in two places in the sample add-in—once in the OnConnection method and once in the OnStartupComplete method. It must be called twice because of the way add-ins are loaded by Visual Studio. If an add-in is set to load on startup, when Visual Studio starts, the add-in starts loading. This loading process includes calling the OnConnection method. But the OnConnection method is called just before the Visual Studio main window is created and shown. If you call the CreateToolWindow method within OnConnection before the main window is shown, creating the tool window will fail because creating an ActiveX control requires its parent window to be visible. You can check to make sure that the main window has been created by examining the connectMode argument passed to the OnConnection method. If this is set to ext_cm_AfterStartup, the add-in was loaded through the Add-in Manager or by means other than the load on startup flag being set and Visual Studio being started. Therefore, the tool window can be shown when an add-in is loaded by using an OnConnection implementation such as this:

public void OnConnection(object application,
    Extensibility.ext_ConnectMode connectMode, object addInInst,
    ref System.Array custom)
{
    applicationObject = (_DTE)application;
    addInInstance = (AddIn)addInInst;

    //If the add-in is loaded from the Add-in Manager dialog, then
    // create and show the tool window:
    if(connectMode == Extensibility.ext_ConnectMode.ext_cm_AfterStartup)
    {
       CreateMediaPlayerToolWindow();
    }
}

If the load on startup flag is set and you want to show the tool window when an add-in is loaded, you can create the window in the OnStartupComplete method. This method is called when initialization of Visual Studio is complete, which includes creating and showing the main window. It's as simple as this code snippet:

public void OnStartupComplete(ref System.Array custom)
{
    //If the add-in is loaded at startup, then
    // create and show the tool window:
    CreateMediaPlayerToolWindow();
}

To create a tool window by using a .NET user control, you will need to call the Windows2.CreateToolWindow2 method. This method has a signature that is similar to the Windows.CreateToolWindow method, but rather than taking a COM ProgID to identify a control, CreateToolWindow2 takes both a name of a .NET assembly and the full name of a class within that assembly that derives from the .NET Framework's System. Windows.Forms.UserControl class. The signature of the CreateToolWindow2 method is:

public  EnvDTE.Window CreateToolWindow2(EnvDTE.AddIn AddInInst,
     string Assembly,
     string Class,
     string Caption,
     string GuidPosition,
     ref object DocObj)

The arguments that differ from the CreateToolWindow method are:

  • Assembly The name of an assembly. This can be either a full path to an assembly, such as C:MyControlAssembly.dll, or a strong name for an assembly, such as Microsoft.VisualStudio.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL, which is the name of the assembly implementing forms within the .NET Frameworks. The assembly name can also be a URL, meaning the prefix file:/// and even http:// can be used to locate the assembly.

  • Class The fully qualified name of a class implementing System.Windows.Forms. UserControl. The namespace as well as the class name must be given using the dotted format, such as System.Windows.Forms.PropertyGrid, which is the name for the property grid, the same grid used within the Properties window inside of Visual Studio.

Note

When using the CreateToolWindow or CreateToolWindow2 method, you should not use code such as Guid.NewGuid().ToString("B") to pass the GUID string. This code will create a new GUID each time the method is called. Rather, you should declare a variable at class scope such as string toolWindowGuid = "{6b74173d-c3e0-4d95-a6bc-877e660c319d}"; to create one GUID for each window type. If you ever need to find the Window object for the tool window again, you can locate that window directly with the DTE.Windows.Item(toolWindowGuid) method. Using the same GUID each time also will allow Visual Studio to save the position of the tool window wherever the user has placed the window, so the next time you create the tool window it can be placed back in the same position. You should use this GUID only for a tool window of the same type, and you should never use the same GUID to create multiple or different tool windows.

The Form Layout sample uses the CreateToolWindow2 method to create its user interface. Because the user control implementing the user interface is contained within the same assembly as the add-in code, the assembly name can be found by using the Location property of the System.Reflection.Assembly class. This Assembly class is found with the GetExecutingAssembly static method, and Location returns the full path, with the file:/// protocol. Lastly, because the user control to be hosted is named FormLayoutCtl, and is contained within the FormLayout assembly, the class name passed to CreateToolWindow2 is FormLayout.FormLayoutCtl.

You can also create a programmable object for a tool window created from a .NET User Control. This will allow the users of your add-in to program your tool window, just as they program the Task List window or Toolbox. To expose a programmable object, all you need to do is to define an interface, and then implement that interface on the class that derives from System.Windows.Forms.UserControl. This bit of code shows an interface named IProgrammableObject, providing a programmable object on a control named UserControl1.

public interface IProgrammableObject
{
    void Method();
}

public partial class UserControl1 : UserControl, IProgrammableObject
{
    public UserControl1()
    {
        InitializeComponent();
    }

    public void Method()
    {
        System.Windows.Forms.MessageBox.Show("Method");
    }
}

With this code to implement a programmable object, you can create the tool window and call Method like this:

object obj = null;
EnvDTE80.Windows2 window2 = (EnvDTE80.Windows2)applicationObject.Windows;
string thisAssembly = System.Reflection.Assembly.GetExecutingAssembly().Location;
EnvDTE.Window win = window2.CreateToolWindow2
(addInInstance, thisAssembly, "MyAddin.UserControl1",
"Window Caption", "{0d3619e3-b0b4-4af3-9053-95a29222159b}",
ref obj);
win.Visible = true;
IProgrammableObject programmableObj = (IProgrammableObject)obj;
programmableObj.Method();

Setting the Tab Picture of a Custom Tool Window

When two or more tool windows are tab-linked together, an image is displayed so the user can quickly recognize the tool windows that are linked together. To set the tab picture for a tool window that's created by an add-in, you use the Window.SetTabPicture method. SetTabPicture takes as its argument a COM IPictureDisp type, a bitmap handle, or a path to a bitmap file (a file with the extension .bmp) such as C:somebitmap.bmp. To create an IPictureDisp object, you can use the same technique described earlier of calling the OleLoadPictureFile method and then passing the returned IPictureDisp object to the SetTabPicture method. Bitmap handles can be retrieved by loading a bitmap file by using any of the various ways that an image can be loaded into an instance of the .NET Framework's Bitmap class (such as from a resource embedded within the add-in's assembly), then the Bitmap.Handle property is called to retrieve the handle.

The bitmap to place onto a tool window tab must have one of two specific formats, and any deviation from these formats can cause the bitmap to appear with incorrect colors or not appear at all. The first format is for a 16-color bitmap, and it must be 16 by 16 pixels. If any portion of the bitmap is to appear transparent, the transparent pixels must have the RGB value 0,254,0. The format for this bitmap is the same format used for displaying custom pictures on command bar buttons; a bitmap can be shared for these two uses. The other format is for high-color bitmaps; it must use 24-bit color, and it, too, needs to be 16 by 16 pixels. If any portion of this bitmap is to appear transparent, the color to use is to have the RGB value 255,0,255.

You can call the Window.SetTabPicture method only on a tool window created by using the Windows.CreateToolWindow method. Windows defined by Visual Studio already have their bitmaps set; if you try to change them, an exception will be generated. If you want to set the bitmap for your own tool window, you should set it before setting the Visible property of your window to true; otherwise, the picture might not be displayed immediately. Lastly, if a custom picture is not set, Visual Studio uses a default picture—the Visual Studio logo.

Setting the Selection Object

As you select different windows in Visual Studio, you see the Properties window update itself with properties available for those windows. For example, if you select a file in Solution Explorer, a set of properties is made available—such as the file path, when the file was modified, or how the file should be built. When you create a tool window, you might also want to have properties for your tool window appear in the Properties window. You set items to appear in the Properties window by using the Window.SetSelectionContainer method, which takes as a parameter an array of type System.Object. These items are displayed in the Properties window when the window that has this method called on it becomes the active window. The sample VSMediaPlayerAdv, an extension to the VSMediaPlayer sample, displays a property set in the Properties window by calling the SetSelectionContainer method with the programmable object of Windows Media Player, which was returned through the DocObj parameter of the CreateToolWindow method. This portion of code shows how this is done:

object []propertiesWindowObjects = {mediaPlayer};
mediaPlayerWindow.SetSelectionContainer(ref propertiesWindowObjects);

You can call the SetSelectionContainer method only on tool windows that you create. If you call this method on a Window object for, say, the Solution Explorer tool window, an exception will be generated.

The Options Dialog Box

Developers can be a finicky bunch—they want Visual Studio to work the way they want down to the finest detail; if even one option is set up in a way they didn't expect, they can become quite unproductive. The Options dialog box is full of options that you can configure—everything from how many spaces are inserted when the Tab key is pressed in the text editor to whether the status bar is shown along the bottom of the main window of Visual Studio.

Changing Existing Settings

Many settings in the Options dialog box can be controlled through the automation model by using the Properties and Property objects. To find a Properties collection, you must first calculate the category and subcategory of the settings you want to modify. On the left side of the dialog box is a tree view control that's rarely more than two levels deep. The top-level nodes in this tree, such as Environment, Source Control, and Text Editor, are the categories of options you can manipulate. Each category contains a group of related Options pages, and each page contains a number of controls you can manipulate to customize your programming environment. The subitem nodes are the subcategories of the Options dialog box; if you select one of these nodes, the right side of the Options dialog box changes to show the options for that category and subcategory. The category and subcategory used to find a Properties collection are based on the category and subcategory displayed in the Options dialog box user interface, but their names might be slightly different from the category and subcategory names. To find the list of categories and subcategories, you must use the Registry Editor. First, you find the item in the Options dialog box that you want to edit. For our example, we'll modify the tab indent size of the Visual Basic source code editor, which is found on the page of the Text Editor category and Basic subcategory.

Note

The Text Editor category is a bit different from other categories in the Options dialog box in that it has three levels, with the third level being a sub-subcategory. However, in the automation model, the General and Tabs sub-subcategories are combined into one and have the same name as they do in the programming language.

After running regedit.exe, navigate to the key HKEY_LOCAL_MACHINESOFTWAREMicrosoftVisualStudio8.0AutomationProperties. Underneath this key is a list of all the property categories accessible to a macro or an add-in. We're looking for the Text Editor category—the key whose name most closely matches this category name in the user interface is TextEditor (without a space). After expanding this item in the Registry Editor, you'll see a list of subcategories; one of those subcategories, Basic, matches the subcategory displayed in the user interface of the Tools Options dialog box, so this is the subcategory we'll use.

Now that we've found the automation category TextEditor and subcategory Basic, we can plug these values into the DTE.Properties property to retrieve the Properties collection:

Sub GetVBTextEditorProperties()
    Dim properties As Properties
    properties = DTE.Properties("TextEditor", "Basic")
End Sub

The last step in retrieving a Property object is to call the Item method of the Properties collection. The Item method accepts as an argument the name of the property, but this name is not stored anywhere except within the object model. Remember that the Properties object is a collection, and, as with all other collection objects, it can be enumerated to find the objects it contains and the names of those objects. You can use the following macro to examine the names of what will be passed to the Properties.Item method. The macro walks all the categories and subcategories listed in the registry and then uses the enumerator of the Properties collection to find the name of the Property object contained in that collection. Each of these category, subcategory, and property names are then inserted into a text file that the macro creates:

Sub WalkPropertyNames()
    Dim categoryName As String
    Dim key As Microsoft.Win32.RegistryKey
    Dim newDocument As Document
    Dim selection As TextSelection
    'Open a new document to store the information
    newDocument = DTE.ItemOperations.NewFile("GeneralText File").Document
    selection = newDocument.Selection
    'Open the registry key that holds the list of categories:
    key = Microsoft.Win32.Registry.LocalMachine
    key = key.OpenSubKey( _
       "SOFTWAREMicrosoftVisualStudio8.0AutomationProperties")
    'Enumerate the categories:
    For Each categoryName In key.GetSubKeyNames()
        Dim subcategoryName As String
        selection.Insert(categoryName + vbLf)
        'Enumerate the subcategories:
        For Each subcategoryName In _
            key.OpenSubKey(categoryName).GetSubKeyNames()
            Dim properties As Properties
            Dim prop As [Property]
            selection.Insert("  " + subcategoryName + vbLf)
            Try
                'Enumerate each property:
                properties = DTE.Properties(categoryName, subcategoryName)
                For Each prop In properties
                    selection.Insert("   " + prop.Name + vbLf)
                Next
            Catch
            End Try
            Next
        Next
    End Sub

Using the output from this macro, we can find the TextEditor category and the Basic subcategory and then look in the Options dialog box for something that looks like the name Tab Size. The closest match is TabSize. Using this name, we can find the Property object for the Visual Basic text editor Tab Size:

Sub GetVBTabSizeProperty()
    Dim properties As Properties
    Dim prop As [Property]
    properties = DTE.Properties("TextEditor", "Basic")
    prop = properties.Item("TabSize")
End Sub

Now all that's left to do is retrieve the value of this property by using the Property.Value property:

Sub GetVBTabSize()
    Dim properties As Properties
    properties = DTE.Properties("TextEditor", "Basic")
    MsgBox(properties.Item("TabSize").Value)
End Sub

This macro displays the value 4, which is the same value in the Tools Options dialog box for the Tab Size option of the Basic subcategory of the Text Editor category. You set this value the same way you retrieve the value, except the Value property is written to rather than read:

Sub SetVBTabSize()
    Dim properties As Properties
    properties = DTE.Properties("TextEditor", "Basic")
    properties.Item("TabSize").Value = 4
End Sub

By simply changing the category and subcategories passed to the DTE.Properties property and looking at the list of property names generated by the WalkPropertyNames macro, you can modify many of the options shown in the Tools Options dialog box.

Creating Custom Settings

Not only can you examine and modify existing settings, but you can also create your own options for your add-ins. Creating a page in the Options dialog box for your add-in requires a .NET user control and creating an .addin file to let Visual Studio know how to load your Options page. The .addin file that you create for a tools options page does not necessarily need to contain the XML code to declare an add-in, but it can. When the user opens the Options dialog box, all the .addin files that can be found are opened, and, if the settings for a custom tools options page is found, the .NET user control is instantiated and shown in the Options dialog box.

Creating a tools options page is rather easy, especially with the CustomToolsOptionsPage starter kit included with the samples for this book. This starter kit will create both the .addin file and code for a user control that can be hosted on the Tools Options dialog box. Once you create this project, all you need to do is copy the .addin file and the .dll implementing the control into one of the special directories that Visual Studio looks for .addin files, and then start a new instance of Visual Studio. Let's look at the code that the starter kit will generate for you.

Declaring the XML for a Tools Options Page

The XML in the .addin file to declare a tools options page is quite simple; here is a snippet from an .addin file that declares a page:

<ToolsOptionsPage>
    <Category Name="My Custom Category">
        <SubCategory Name="My Custom Subcategory">
            <Assembly>MyCustomPage.dll</Assembly>
            <FullClassName>MyCustomPage.UserControl1</FullClassName>
        </SubCategory>
    </Category>
</ToolsOptionsPage>

This snippet of XML declares a page that will create a node in the tree on the left side of the Tools Options dialog box named My Custom Category. It will also create a node under My Custom Category named My Custom Subcategory. If the user were to select this node, the assembly MyCustomPage.dll will be loaded, and then the class MyCustomPage.UserControl1 is instantiated and then displayed. If you used a name such as Environment as the category, this page is merged into the Environment node in the tree of the Tools Options dialog box. You can specify multiple SubCategory tags within a Category tag, and all those pages are grouped together under one top-level node.

The IDTToolsOptionsPage Interface

An Options page has three stages in its lifetime: creation, interaction, and dismissal. To allow your page to know about these three stages, the user control needs to implement the EnvDTE.IDTToolsOptionsPage interface. This interface has the following signature:

public interface IDTToolsOptionsPage
{
    public void GetProperties(ref object PropertiesObject);
    public void OnAfterCreated(EnvDTE.DTE DTEObject);
    public void OnCancel();
    public void OnHelp();
    public void OnOK();
}

When the user first displays the Options dialog box, Visual Studio sees in the .addin file that you've declared a page, and it creates an instance of your .NET user control. If the IDTToolsOptionsPage interface is implemented on that control, the OnAfterCreated method is called and is passed the DTE object for the instance of Visual Studio that is creating the control. The implementation of this method can perform any initialization steps needed, such as reading values from the system registry and using these values to set up the user interface of the control.

The Options dialog box has three buttons the user can click: OK, Cancel, and Help. If the user clicks OK, the IDTToolsOptionsPage.OnOK method is called, giving your page a chance to store back into the system registry any values that the user might have selected. You should also perform any cleanup work in the OnOK method because the Options page is about to be dismissed. If the user clicks the Cancel button, the OnCancel method is called. No values that the user selected in the page should be persisted, and this method is called so you can perform any cleanup necessary because, as when the user clicks OK, the Options dialog box is about to be closed. If the user clicks Help, the OnHelp method is called, giving your page a chance to display any help necessary to the user. Unlike the other buttons, Help doesn't dismiss the dialog box, so you shouldn't do any cleanup or store or discard values during this method call.

The last method of the IDTToolsOptionsPage interface is the GetProperties method. This method allows users to retrieve a Properties object for the options on your page in the same way they could retrieve a Properties object for other Options pages.

Exposing a Property Object

As you saw earlier, many of the values in the Options dialog box are programmable through the Properties collection. You can also allow the user to set and retrieve the values of your page through the Properties collection by using the GetProperties method. This method returns a System.Object object instance, which is wrapped up into a Properties collection by Visual Studio and handed back to the user when the DTE.Properties property is called with the category and subcategory of your page. By default, the starter kit creates one property, called SampleProperty, and it returns a string with the value property value. You can change the value, type, and name of this property, and you can also add new properties. To modify or add properties, you need to change the interface PropertiesInterface that was generated by the starter kit, making any changes necessary, and then reflect those properties in the class PropertiesImplementation, which implements the PropertiesInterface interface. This interface and class set-up is necessary to allow Visual Studio to properly wrap the object and return a Properties object to the user. You also need to use two attributes to help Visual Studio create the Properties object. The first of these properties, ComVisibleAttribute, will expose the class as a COM object. Visual Studio uses a COM type library to inspect the properties object, and this is possible only with the ComVisibleAttribute being set to true. It is not necessary to set the Register for COM interop value in the project properties; the object needs just to be visible as a COM object. The second attribute, the ClassInterfaceAttribute attribute, adjusts how the type library information is exposed from the class implementing the properties, and it should look like this:

[System.Runtime.InteropServices.ClassInterface
    (System.Runtime.InteropServices.ClassInterfaceType.AutoDual)]

Of course, all these attributes are set up for you by the starter kit, so you should not need to make any of these changes yourself.

To find the property exposed by this object, you will pass the category and subcategory values from the .addin file to the DTE.Properties property and then use the returned Properties collection just as you would for a property built-in to Visual Studio. This macro will display the SampleProperty property for the XML snippet given earlier:

Sub ShowSamplePropertyValue()
    Dim props As Properties

    props = DTE.Properties("My Custom Category", "My Custom Subcategory")
    MsgBox(props.Item("SampleProperty").Value)
End Sub

Looking Ahead

In this chapter, you learned how you can program many of the windows available in Visual Studio. In the next chapter, we'll show you how to program the data in one specific window type: the text editor window.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset