VSXReloaded — Part #6: Creating Options Pages

17 Jan 2017 Visual Studio Extensibility

When you implement an extension package for Visual Studio, you often need to provide UI to allow the user to manage package specific settings. Though you can create your custom UI from scratch, there is an easier and faster way to integrate these settings with the IDE—by adding options pages.

In this post, you will learn the fundamentals of implementing them.

Creating the Sample Package

Let’s start with creating a VSPackage that integrates a tool window. Later, we will add options pages that allow the user to configure the text size and color of the message in the tool window.

Follow these steps:

1. Start a new Visual Studio project and create a VSIX Project—name it SimpleOptionsPagesPackage. 2. Add a Visual Studio Package item (OptionsPagesPackage.cs) to the project. 3. Create a new Custom Tool Window—name the file GreetingToolWindow.cs.

Changing the Tool Window UI

At this point, you have a very simple VSPackage that shows the same tool window you created in Part #5.

4. Modify the GreetingToolWindow.xaml file to change the UI:

<UserControl x:Class="SimpleOptionsPagesPackage.GreetingToolWindowControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             Background="{DynamicResource VsBrush.Window}"
             Foreground="{DynamicResource VsBrush.WindowText}"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300"
             Name="MyToolWindow">
    <Grid>
        <TextBlock x:Name="Welcome" Margin="10" HorizontalAlignment="Center"
                   VerticalAlignment="Center" FontSize="24"
                   Background="{DynamicResource VsBrush.Window}"
                   Foreground="{DynamicResource VsBrush.WindowText}">
            Welcome in Visual Studio!
        </TextBlock>
    </Grid>
</UserControl>

5. Remove the unused button1_Click method from the GreetingToolWindow.xaml.cs file:

namespace SimpleOptionsPagesPackage
{
    using System.Windows.Controls;

    public partial class GreetingToolWindowControl : UserControl
    {
        public GreetingToolWindowControl()
        {
            this.InitializeComponent();
        }
    }
}

Now, the modified tool window package is ready to run. With the View | Other Windows | GreetingToolWindow command you can check the new outlook (Figure 1) when running the package in the Experimental Instance.

f0601

Figure 1: The modified tool window

Adding an Options Page Grid

The easiest way to provide a setting UI is a property grid that looks similar to the Properties window in the VS IDE. You only need to define the structure of configuration settings. Add a new C# file to the project (GreetingsOptionsGrid.cs):

using System.ComponentModel;
using Microsoft.VisualStudio.Shell;

namespace SimpleOptionsPagesPackage
{
    public class GreetingsOptionsGrid: DialogPage
    {
        [Category("Text settings")]
        [DisplayName("Font size")]
        [Description("Size of the greeting text")]
        public int TextSize { get; set; } = 32;

        [Category("Text settings")]
        [DisplayName("Color")]
        [Description("Color of the greeting text")]
        public string TextColor { get; set; } = "Red";
    }
}

The GreetingsOptionsGrid class derives from the DialogPage class (it is declared in the Microsoft.VisualStudio.Shell namespace), which takes care of creating the property grid UI according to the class properties and its decorations.

Each property sets the Category, DisplayName, and Description attributes that set up the visual appearance of the property grid.

You need to bind the options page to OptionsPagesPackage. Add the ProvideOptionPage line to the adornments of the package class:

[PackageRegistration(UseManagedResourcesOnly = true)]
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] 
[Guid(PackageGuidString)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[ProvideToolWindow(typeof(GreetingToolWindow))]
[ProvideOptionPage(typeof(GreetingsOptionsGrid), "Greetings Package", 
  "Greetings Page", 0, 0, true)]
public sealed class OptionsPagesPackage : Package
{
    // ...
}

Now, as you run the modified package in the Experimental Instance (Ctrl+F5), you can observe that the freshly created options page (Figure 2).

f0602

Figure 2: The options page of the Greetings Package

Hint: Check how the property adornments of the GreetingsOptionsGrid class show up in the UI.

The package-specific options page behaves just like the ones shipped with Visual Studio out-of-the-box. As Figure 3 shows, the IDE provides search support through the Quick Launch (Ctrl+Q) command.

f0603

Figure 3: The Greetings Page shows up in Quick Launch search results

How It Works

Just like packages and their related tool windows, options pages are also registered with Visual Studio when you deploy the corresponding VSIX package. The goal of this registration is to prepare the IDE for loading your package when the user displays a page in the Options dialog (with the Tools | Options command).

After starting the IDE, you can display the Options dialog and browse among the pages. The VS Shell does not load the package unless you select one of its options pages—provided it has not been loaded earlier.

The ProvideOptionsPage attribute has several constructors. The one we used has six arguments:

[ProvideOptionPage(typeof(GreetingsOptionsGrid), "Greetings Package",
  "Greetings Page", 0, 0, true)]

The first is a type that represents the class implementing the options page. The second and third arguments register the category, and the subcategory of the page (check Figure 2), respectively. These are non-localized names. You can provide localized names with resources; in this case, the fourth and fifth argument specify the corresponding resource IDs. The true value passed in the sixth argument tells that the options page can be accessed through the Visual Studio automation mechanism.

Note: At this point, it does not matter whether you use the true or false value of the sixth argument. This sample works with both. For future use, the best way is always to pass true.

The options page now stores the information that we can set in the Options dialog, but we have not used these settings yet. Let’s modify the code so that we can reflect the changes of the text settings in the tool window.

The Package base class—OptionsPagesPackage derives from this class—provides a method, GetDialogPage, which can query the information stored in a particular options page. We have several alternative ways to convey this info to the custom control of the tool window’s UI. In this sample, I just pass down the package instance to the control and read the text settings there.

Change the body of the GreetingToolWindowControl class (GreetingToolWindowControl.xaml.cs):

using System.Windows.Media;
using System.Windows.Controls;

namespace SimpleOptionsPagesPackage
{
    /// <summary>
    /// Interaction logic for GreetingToolWindowControl.
    /// </summary>
    public partial class GreetingToolWindowControl : UserControl
    {
        private readonly GreetingToolWindow _toolWindow;

        /// <summary>
        /// Initializes a new instance of the <see cref="GreetingToolWindowControl"/> class.
        /// </summary>
        public GreetingToolWindowControl(GreetingToolWindow toolWindow)
        {
            _toolWindow = toolWindow;
            InitializeComponent();
            Loaded += ToolWindowLoaded;
        }

        private void ToolWindowLoaded(object sender, System.Windows.RoutedEventArgs e)
        {
            var color = ColorConverter.ConvertFromString(TextColor);
            if (color != null)
            {
                Welcome.Foreground = new SolidColorBrush((Color)color);
            }
            Welcome.FontSize = TextSize;
        }

        private OptionsPagesPackage Package => _toolWindow.Package as OptionsPagesPackage;

        private int TextSize => (Package?.GetDialogPage(typeof(GreetingsOptionsGrid))
            as GreetingsOptionsGrid)?.TextSize ?? 12;

        private string TextColor => (Package?.GetDialogPage(typeof(GreetingsOptionsGrid))
            as GreetingsOptionsGrid)?.TextColor ?? "Red";
    }
}

The constructor receives the GreetingToolWindow instance—the one that represents the tool window within the IDE—so that it can access the package instance through the tool window’s Package property.

The private TextSize and TextColor properties obtain the options page setting and provide a safe conversion to meaningful values—to avoid problems from faulty settings, such as an invalid color name.

The ToolWindowLoaded event handler method takes care of applying the text settings.

Note: In this implementation, for the sake of simplicity, you should close and then display the GreetingsToolsWindow to let the text settings be updated according to the current option values.

This solution requires passing the tool window instance to its UI. Thus, we need to modify the GreetingToolWindow class, too:

public class GreetingToolWindow : ToolWindowPane
{
    public GreetingToolWindow() : base(null)
    {
        this.Caption = "GreetingToolWindow";
        this.Content = new GreetingToolWindowControl(this);
    }
}

Now, you can try to provide new text settings in the options page (Figure 4).

f0604

Figure 4: Changed text settings

When you close and then display the tool window again (Figure 5), it shows the welcome text with the new settings.

f0605

Figure 5: The tool window with the new text settings

Where We Are

In this post, you learned how easy is to create a simple options page with a property grid. In the next post, you will learn to replace the property grid with customized UI.