VSPackage Tutorial 4: How to Integrate into the Properties Window, Task List, Output Window, and Options Dialog Box

By using the Visual Studio SDK, you can enable your code to access any tool window in Visual Studio. For example, you can add entries to the Task List, add text to the Output window, or integrate your extension into the Properties window so that users can configure the extension by setting properties. This tutorial shows how to integrate your extension into tool windows in Visual Studio.

By completing this tutorial, you can learn how to do the following things:

  • Create a VSPackage by using the package wizard.

  • Implement the generated tool window.

  • Implement a menu command handler.

  • Create an Options page.

  • Make data available to the Properties window.

  • Integrate into the Properties window.

  • Add text to the Output window and items to Task List.

This tutorial is part of a series that teaches how to extend the Visual Studio IDE. For more information, see Tutorials for Customizing Visual Studio By Using VSPackages.

Create a VSPackage By Using the Package Wizard

To create a VSPackage

  1. Create a VSPackage. For more information about how to create a VSPackage, see How to: Create VSPackages (C# and Visual Basic).

  2. Name the project TodoList, set the language to Visual C#, and on the Select VSPackage Options page, select both Menu Command and Tool Window.

  3. On the Command Options page, set Command name to Todo Manager and Command ID to cmdidTodoCommand.

  4. On the Tool Window Options page, set Window name to Todo Manager and Command ID to cmdidTodoTool.

Implement the Generated Tool Window

The package wizard generated a basic tool window in the form of a user control. However, it has no functionality. To give it functionality, you must add child controls and modify the code in MyControl.cs.

The tool window will include a TextBox in which to type a new ToDo item, a Button to add the new item to the list, and a ListBox to display the items on the list. The completed tool window should resemble the following picture:

Finished Tool Window

To add controls to the tool window

  1. In Solution Explorer, double-click MyControl.cs.

  2. On the form, select the Click Me! button, and then press DEL.

  3. Change the width of the form to 275 pixels.

  4. From the Containers section of the Toolbox, drag a TableLayoutPanel control to the form.

  5. In the Properties window, set the Dock property of the TableLayoutPanel control to Fill.

  6. Drag a TextBox control to the upper-left cell of the TableLayoutPanel, a button to the upper-right cell, and a ListBox to the lower-left cell.

  7. Select the ListBox control. In the Properties window, set ColumnSpan to 2, and then set the Dock property of the ListBox to Fill.

  8. Select the button. Set its Text property to Add.

  9. Select the TextBox control. Set its Dock property to Fill.

  10. Select the TableLayoutPanel control by clicking where there are no child controls. Click the middle vertical line and drag it until the button in the right column is just big enough to display its text.

  11. Drag the middle horizontal line until the cells above the line are just tall enough to hold their controls.

  12. Save your work. The control should resemble the following picture:

    Tool window in designer

By default, the user control constructor in the MyControl.cs file takes no parameters. However, you can customize the constructor to include parameters so that you can save the parent for later use.

To customize the constructor

  1. Right-click the MyControl.cs designer page, and then click View Code.

  2. Find the constructor, which resembles the following code.

    public MyControl()
    {
        InitializeComponent();
    }
    
  3. Replace the existing constructor by using the following code:

    public MyToolWindow _parent;
    public MyControl(MyToolWindow parent)
    {
        InitializeComponent();
        _parent = parent;
    }
    

    Doing this enables the constructor to take a parameter of type MyToolWindow.

  4. Save your work.

  5. Now, add a parameter to the code that calls the constructor.

    In Solution Explorer, open MyToolWindow.cs.

  6. Find the line in the MyToolWindow constructor that resembles the following code.

    control = new MyControl();
    
  7. Change the line by adding this as a parameter, as follows.

    control = new MyControl(this);
    

    Doing this passes the instance of the tool window to the user control. (This is required in a later step to create the constructor for the ToDoItem class.)

Implement a Menu Command Handler

When the TodoList project was created, it included a default handler for the menu item. The handler is in the TodoListPackage.cs file. Now, add code to the handler to display the tool window. You can do this in just a couple of steps because TodoListPackage.cs already contains a function named ShowToolWindow.

To implement the menu item handler

  1. Open TodoListPackage.cs. Notice that the menu item handler contains the following sample code.

    private void MenuItemCallback(object sender, EventArgs e)
    {
        // Show a Message Box to prove we were here
        IVsUIShell uiShell = (IVsUIShell)GetService(typeof(SVsUIShell));
        Guid clsid = Guid.Empty;
        int result;
        Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(uiShell.ShowMessageBox(
                   0,
                   ref clsid,
                   "Package Name",
                   string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.ToString()),
                   string.Empty,
                   0,
                   OLEMSGBUTTON.OLEMSGBUTTON_OK,
                   OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST,
                   OLEMSGICON.OLEMSGICON_INFO,
                   0,        // false 
                   out result));
    }
    
  2. Remove everything in the function, and replace it with a call to ShowToolWindow as follows.

    private void MenuItemCallback(object sender, EventArgs e)
    {
        ShowToolWindow(sender, e);
    }
    

    Save your work, and then press F5 to build the project and open it in the experimental build of Visual Studio. Test whether the tool window opens by clicking ToDo Manager on the Tools menu.

    Close the experimental build before you continue.

Create an Options Page

You can provide a page in the Options dialog box so that users can change settings for the tool window. Creating an Options page requires both a class that describes the options and an entry in the TodoListPackage.cs file.

To create an Options page

  1. In Solution Explorer, right-click the ToDoList project, point to Add, and then click Class.

  2. In the Add New Item dialog box, name the file ToolsOptions.cs, and then click Add.

    Visual Studio creates a class named ToolsOptions in this file, but you must modify the class header so that the class is derived from DialogPage.

    Add the Microsoft.VisualStudio.Shell namespace to the existing using/imports directives, as follows.

    using Microsoft.VisualStudio.Shell;
    
  3. Modify the ToolsOptions class declaration to inherit from DialogPage.

    class ToolsOptions : DialogPage
    
  4. The Options page in this tutorial will only provide one option named DaysAhead. To add this option, add a property named DaysAhead to the ToolsOptions class as follows.

    class ToolsOptions : DialogPage
    {
        private double _daysAhead;
    
        public double DaysAhead
        {
            get { return _daysAhead; }
            set { _daysAhead = value; }
        }
    }
    

    This class stores a single option as a private member named _daysAhead. The class then provides a public property named DaysAhead for accessing the option.

  5. Save ToolsOptions.cs.

Now you must make the project aware of this Options page so that it will be correctly registered and available to users.

To make the Options page available to users

  1. In Solutions Explorer, open TodoListPackage.cs.

  2. Find the line that contains the ProvideToolWindowAttribute attribute, and then add a ProvideOptionPageAttribute attribute immediately after it, as follow.

    [ProvideToolWindow(typeof(MyToolWindow))]
    [ProvideOptionPage(typeof(ToolsOptions), "To-Do", "General", 101, 106, true)]
    

    Note

    You do not have to include the word 'Attribute' in attribute declarations.

  3. Save TodoListPackage.cs.

    The first parameter to the ProvideOptionPage constructor is the type of the class ToolsOptions, which you created earlier. The second parameter, "To-Do", is the name of the category in the Options dialog box. The third parameter, "General", is the name of the subcategory of the Options dialog box where the Options page will be available. The next two parameters are resource IDs for strings; the first is the name of the category, and the second is the name of the subcategory. The final parameter sets whether this page can be accessed by using Automation.

    When your Options page is accessed, it should resemble the following picture.

    Options Page

    Notice the category To-Do and the subcategory General.

Make Data Available to the Properties Window

By following principles for good object-oriented design, you can make a class named ToDoItem that stores information about the individual items in the To-Do list.

To make data available in the Properties window

  1. In Solution Explorer, right-click the ToDoList project, point to Add, and then click Class.

  2. In the Add New Item dialog box, name the file ToDoItem.cs, and then click Add.

    When the tool window is available to users, the items in the ListBox will be represented by ToDoItem instances. When the user selects one of these items in the ListBox, the Properties window will display information about the item.

    To make data available in the Properties window, make the data into public properties of a class and then document them by using two special attributes, Description and Category. Description is the text that appears at the bottom of the Properties window. Category defines where the property should appear when the Properties window is displayed in Categorized view. In the following picture, the Properties window is in Categorized view, the Name property in the To-Do Fields category is selected, and the description of the Name property is displayed at the bottom of the window.

    Properties Window

  3. Add the following namespaces to the top of the ToDoItem.cs file, after the existing using/imports statements.

    using System.ComponentModel;
    using System.Windows.Forms;
    using Microsoft.VisualStudio.Shell.Interop;
    
  4. Begin implementing the ToDoItem class as follows. Make sure to add the public access modifier to the class declaration.

    public class ToDoItem
    {
        private string _name;
        [Description("Name of the To-Do item")]
        [Category("To-Do Fields")]
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                _parent.UpdateList(this);
            }
        }
    
        private DateTime _dueDate;
        [Description("Due date of the To-Do item")]
        [Category("To-Do Fields")]
        public DateTime DueDate
        {
            get { return _dueDate; }
            set
            {
                _dueDate = value;
                _parent.UpdateList(this);
                _parent.CheckForErrors();
            }
        }
    }
    

    Notice that this code has two properties, Name and DueDate. These are the two properties that will appear in the Properties window, as shown in the previous picture. Each property is preceded by the Description and Category attributes, which provide the information for display in the Properties window. Examine these two attributes for the Name property; the strings should match the ones in the picture.

  5. Add the following constructor function at the top of the class.

    private MyControl _parent;
    public ToDoItem(MyControl parent, string name)
    {
        _parent = parent;
        _name = name;
        _dueDate = DateTime.Now;
    
        double daysAhead = 0;
        IVsPackage package = _parent._parent.Package as IVsPackage;
        if (package != null)
        {
            object obj;
            package.GetAutomationObject("To-Do.General", out obj);
    
            ToolsOptions options = obj as ToolsOptions;
            if (options != null)
            {
                daysAhead = options.DaysAhead;
            }
        }
    
        _dueDate = _dueDate.AddDays(daysAhead);
    }
    

    First, this code declares a private member named _parent, which corresponds to the user control that contains the TextBox, Button, and ListBox controls that you created earlier. The constructor takes the user control as a parameter, together with a string that is the name for this ToDo item. The first three lines in the constructor save the user control, the name, and the current date and time.

    You can use the current date and time as a basis for enabling the DaysAhead option on the Option page that you created earlier. Because the current date and time are not typically used as a due date, you can advance the current date by the number of days that are specified on the Options page. .

    The code declares a local variable called daysAhead that is set by using the value in the DaysAhead option. The next line obtains the parent of the user control, and from there, the package member. (This is where you use the _parent member that you added to the MyControl.cs class earlier.)

    If this package member is not null, an object is declared that will hold the ToolsOptions instance. To get the instance, the code calls the GetAutomationObject member of the package and passes the name of the category and subcategory as a single dot-delimited string, To-Do.General. The results are passed as an output parameter back into the obj variable.

    The obj variable is then cast to the ToolsOptions class and saved in a variable named options. If this variable is not null, the code obtains the DaysAhead member and saves it into the _daysAhead variable.

    The code then advances the _duedate variable by the number of days ahead by using the AddDays method.

  6. Because instances of the ToDoItem class will be stored in the ListBox and the ListBox will call the ToString function that this class inherits from the base Object class to retrieve the string to display for the item, you must overload the ToString function.

    Add the following code to ToDoItem.cs, after the constructor and before the final two closing braces in the file.

    public override string ToString()
    {
        return _name + " Due: " + _dueDate.ToShortDateString();
    }
    
  7. Open MyControl.cs.

  8. Add stub methods to the MyControl class for the CheckForError and UpdateList methods. Put them after the ProcessDialogChar and before the two final braces in the file.

    public void CheckForErrors()
    {
    }
    
    public void UpdateList(ToDoItem item)
    {
    }
    

    The CheckForError method will call a method that has the same name in the parent object, and that method will check whether any errors have occurred and handle them correctly. The UpdateList method will update the ListBox in the parent control; the method is called when the Name and DueDate properties in this class change. You will implement these methods in a later step.

Integrate into the Properties Window

Now write the code that manages the ListBox, which will be tied to the Properties window.

You must add a handle to the button that reads the TextBox, creates a ToDoItem instance, and adds the instance to the ListBox.

To integrate with the Properties window

  1. Switch to the design view of MyControl.cs. Replace the existing button1_Click handler function by using the following code.

    private void button1_Click(object sender, EventArgs e)
    {
        if (textBox1.Text.Length > 0)
        {
            var item = new ToDoItem(this, textBox1.Text);
            listBox1.Items.Add(item);
            TrackSelection();
            CheckForErrors();
        }
    }
    

    This code creates a new ToDoItem instance and passes the user control instance as a parameter together with the text that the user entered in the TextBox control. Next, the code adds the item to the ListBox. (The ListBox will call the ToString method of the ToDoItem instance to retrieve the string to display in the ListBox.) Next, the code calls the TrackSelection function, which you will write in a later step. Finally, the code checks for errors.

  2. Switch back to the design view of MyControl.cs to add the code that handles user selection of a new item in the ListBox.

  3. Click the ListBox control. In the Properties window, double-click the SelectedIndexChanged event. Doing this adds a stub for a SelectedIndexChanged handler and assigns it to the event.

  4. Fill in the SelectedIndexChanged handler as follows, and stub in the method it calls.

    private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
    {
        TrackSelection();
    }
    private void TrackSelection()
    {
    }
    
  5. Save your work. You can build your project and look for typos.

  6. Now, fill in the TrackSelection function, which will provide integration with the Properties window. This function is called when the user adds an item to the ListBox or clicks an item in the ListBox.

    Now that you have a class that the Properties window can use, you can integrate the Properties window with the tool window. When the user clicks an item in the ListBox in the tool window, the Properties window should be updated accordingly. Similarly, when the user changes a ToDo item in the Properties window, the associated item should be updated.

    Note

    As an alternative, you can generate PropertyChanged events directly by implementing the INotifyPropertyChanged interface.

    Put the code for updating the Properties window in the TrackSelection function. Doing this will tie the ToDoItem object to the Properties window; you do not have to write any additional code to modify the ToDoItem when the user changes a value in the Properties window. The Properties window will automatically call the set property accessors to update the values. However, you must finish the UpdateList method that you created when you wrote the code for the ToDoItem class.

  7. Add the following namespace declarations to the top of the MyControl.cs file, after the existing using/imports statements.

    using System;
    using System.Runtime.InteropServices;
    using Microsoft.VisualStudio.Shell.Interop;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Shell;
    
  8. Implement the TrackSelection function as follows.

    private SelectionContainer mySelContainer;
    private System.Collections.ArrayList mySelItems;
    private IVsWindowFrame frame = null;
    private void TrackSelection()
    {
        if (frame == null)
        {
            var shell = GetService(typeof(SVsUIShell)) as IVsUIShell;
            if (shell != null)
            {
                var guidPropertyBrowser = new
                    Guid(ToolWindowGuids.PropertyBrowser);
                shell.FindToolWindow((uint)__VSFINDTOOLWIN.FTW_fForceCreate,
                    ref guidPropertyBrowser, out frame);
            }
        }
        if (frame != null)
        {
            frame.Show();
        }
        if (mySelContainer == null)
        {
            mySelContainer = new SelectionContainer();
        }
    
        mySelItems = new System.Collections.ArrayList();
    
        var selected = listBox1.SelectedItem as ToDoItem;
        if (selected != null)
        {
            mySelItems.Add(selected);
        }
    
        mySelContainer.SelectedObjects = mySelItems;
    
        var track = GetService(typeof(STrackSelection))
            as ITrackSelection;
        if (track != null)
        {
            track.OnSelectChange(mySelContainer);
        }
    }
    
  9. Add the following code just after the closing brace at the end of the TrackSelection function.

    protected override object GetService(Type service)
    {
        object obj = null;
        if (_parent != null)
        {
            obj = _parent.GetVsService(service);
        }
        if (obj == null)
        {
            obj = base.GetService(service);
        }
        return obj;
    }
    

    This code calls the GetService function. This function first tries to obtain the service from the parent tool window by calling its GetService function. If that fails, it tries to obtain it from the GetService function of the object. Because the GetService function in the parent tool window is not public, the code calls GetVsService instead. You must add the GetVsService function.

  10. Open MyToolWindow.cs. Add the following code to the end of the class, just before the final two closing braces in the file.

    internal object GetVsService(Type service)
    {
        return GetService(service);
    }
    
  11. Save MyToolWindow.cs.

    The first time the TrackSelection function runs, it calls GetService to obtain an instance of the Visual Studio shell. It then uses that instance to obtain an object for the Properties window. To get the Properties window object, the code starts by using the GUID that represents the Properties window. (The GUIDs for the tool windows are members of the ToolWindowGuids80 class.) The code then calls the FindToolWindow function of the shell, by passing the GUID, to get the Properties window object. Doing this saves it in the frame variable so that when the function is called again, this process of obtaining the Properties window does not have to be repeated.

    Next, the method calls the Show method of the frame variable to display the Properties window.

    The next code gathers the selected items in the ListBox. The ListBox is not configured to enable multiple selection. To pass the selected item to the Properties window, you must use a container. Therefore, the code gathers the selected item and puts it in an ArrayList, and then puts that ArrayList in a container of type SelectionContainer.

    Next, the code calls GetService to obtain an instance of ITrackSelection, which is the Visual Studio object that tracks selected objects in the user interface (UI) and displays their properties. Then the code directly calls the ITrackSelection OnSelectChange event handler, and passes the SelectionContainer that is holding the selected item. The result is that the Properties window displays the properties for the selected item.

    When the user changes a ToDoItem object in the Properties window, the Properties window automatically calls the set accessor functions in the ToDoItem object. That updates the object, but you still have to update the ListBox.

  12. In an earlier step, you added code in the set accessor functions to call an UpdateList function in MyControl.cs. Now, add the rest of the UpdateList function code.

    Switch back to MyControl.cs.

  13. Implement the UpdateList method as follows.

    public void UpdateList(ToDoItem item)
    {
        var index = listBox1.SelectedIndex;
        listBox1.Items.RemoveAt(index);
        listBox1.Items.Insert(index, item);
        listBox1.SetSelected(index, true);
    }
    

    This code determines which item is selected and will correspond to the ToDoItem that is being modified. The code removes the item from the ListBox, and then re-inserts it. Doing this updates the line in the ListBox for the item. Then the code sets the selection back to the same item.

  14. Save your work.

Add Text to the Output Window and Items to the Task List

To add strings to the Task List and Output window, you must first obtain objects that refer to those two windows. Then, you can call methods on the objects. For the Task List, you create a new object of type Task, and then add that Task object to the Task List by calling its Add method. To write to the Output window, you call its GetPane method to obtain a pane object, and then you call the OutputString method of the pane object.

To add text to the Output window and the Task List

  1. Open MyControl.cs.

  2. Expand the button1_Click method by inserting the following code before the call to TrackSelection().

    private void button1_Click(object sender, EventArgs e)
    {
        if (textBox1.Text.Length > 0)
        {
            var item = new ToDoItem(this, textBox1.Text);
            listBox1.Items.Add(item);
    
            //Insert this section------------------ 
            var outputWindow = GetService(
                typeof(SVsOutputWindow)) as IVsOutputWindow;
            IVsOutputWindowPane pane;
            Guid guidGeneralPane =
                VSConstants.GUID_OutWindowGeneralPane;
            outputWindow.GetPane(ref guidGeneralPane, out pane);
            if (pane != null)
            {
                pane.OutputString(string.Format(
                    "To Do item created: {0}\r\n",
                    item.ToString()));
            }
            //-------------------------------------
    
            TrackSelection();
            CheckForErrors();
        }
    }
    

    This code obtains the object for the Output window. The object exposes an IVsOutputWindow interface. The code then obtains an IVsOutputWindowPane object that includes the OutputString function, which ultimately writes to the Output window.

  3. Now implement the CheckForErrors method, as follows.

    public void CheckForErrors()
    {
        foreach (ToDoItem item in listBox1.Items)
        {
            if (item.DueDate < DateTime.Now)
            {
                ReportError("To Do Item is out of date: "
                    + item.ToString());
            }
        }
    }
    

    This code calls the ReportError method, which you will create next, together with some other methods that help to add items to the Task List.

  4. Add the following code to the end of the class, just before the two closing braces.

    [Guid("72de1eAD-a00c-4f57-bff7-57edb162d0be")]
    public class MyTaskProvider : TaskProvider
    {
        public MyTaskProvider(IServiceProvider sp)
            : base(sp)
        {
        }
    }
    private MyTaskProvider _taskProvider;
    private void CreateProvider()
    {
        if (_taskProvider == null)
        {
            _taskProvider = new MyTaskProvider(_parent);
            _taskProvider.ProviderName = "To Do";
        }
    }
    private void ClearError()
    {
        CreateProvider();
        _taskProvider.Tasks.Clear();
    }
    private void ReportError(string p)
    {
        CreateProvider();
        var errorTask = new Task();
        errorTask.CanDelete = false;
        errorTask.Category = TaskCategory.Misc;
        errorTask.Text = p;
    
        _taskProvider.Tasks.Add(errorTask);
    
        _taskProvider.Show();
    
        var taskList = GetService(typeof(SVsTaskList))
            as IVsTaskList2;
        if (taskList == null)
        {
            return;
        }
    
        var guidProvider = typeof(MyTaskProvider).GUID;
        taskList.SetActiveProvider(ref guidProvider);
    }
    

    At the start of this code is a specialized TaskProvider class named MyTaskProvider that includes a GUID. Next is a member variable of this new class type, followed by a method that creates the new instance when it is required.

    Next come two important methods, ClearError, which clears out the existing task items, and ReportError, which adds items to the Task List.

    The ReportError method creates a new instance of Task, initializes the instance, and then adds the instance to the Task List. The new Task List entries are only visible when the user selects the ToDo item in the drop-down list at the top of the Task List. The final two lines in the code automatically select the ToDo item from the drop-down list and bring the new task items into view. The GUID is required when the TaskProvider class is inherited because the SetActiveProvider method requires a GUID as a parameter.

Trying It Out

To test the extension

  1. Press CTRL+F5 to open the experimental build of Visual Studio.

  2. In the experimental build, on the Tools menu, click ToDo Manager.

    The tool window that you designed should open.

  3. Type something in the TextBox and then click Add.

    You should see that the item is added to the ListBox.

  4. Type something else and then click Add again.

    As you add items, the initial date is set to the current date and time. This triggers an error and also an entry in the Task List.

  5. On the View menu, click Output to open the Output window.

    Notice that every time that you add an item, a message is displayed in the Output window.

  6. Click one of the items in the ListBox.

    The Properties window displays the two properties for the item.

  7. Change one of the properties and then press ENTER.

    The item is updated in the ListBox.

What's Next

In this tutorial you created a tool window that is integrated with another tool window in Visual Studio. Visual Studio has several tool windows that you can work with, and the GUIDs for these can be found in the ToolWindowGuids class. You also created a class that contains properties that the Properties window can access. You provided accessor functions that the Properties window uses. In the set accessor function, you called into your own code to handle changes that were made in the Properties window. Doing this provides a two-way communication mechanism. Finally, you learned how to add items to the Task List, how to bring the items into view, and how to add text to the Output window.

In the next tutorial, Tutorial: How to Integrate Help Documentation into Visual Studio, you can learn how to connect your extensions to a Help file by using the Visual Studio context-sensitive Help feature.

See Also

Concepts

Output Window (Visual Studio SDK)

Task List

Other Resources

Menus and Toolbars

Tool Windows

Properties Window and Property Pages