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

LearnVSXNow! #20 - PowerCommands Deep Dive — Commands and UI

 

In the last part I complained you about the nine hours of jet lag that “inspired” me to start with a deep dive on PowerCommands. Now I am back at Hungary (to be honest I finished Part #19 here at home). However, jet lag effect is over and I do not wake up at 4 o’clock in the morning, but I am still inspired to go on with the work I started.J

In Part #19 I analyzed the basic architecture of PowerCommands focusing on how commands are represented and executed. In that article we saw there is a CommandManagerService in the package responsible for registering commands. Each command is encapsulated into its own class inherited from the DynamicCommand class.

In this article I dive into details about PowerCommands UI. There are two different groups of UI in the package:

—  PowerCommands option pages that integrate with the Tools|Options dialog

—  User interface related to commands

In this article I show you details for both.

Using option pages

Visual Studio extensions—packages and Add-ins—can add their own option pages to Visual Studio integrated into the Tools|Options dialog. The pattern we use to add option pages is very similar to the pattern as we add a tool window, but it has some additional elements:

—  There is a user interface for the option page created as a standard WinForms user control

—  A class representing a wrapper for the user interface of the option page is responsible to embed the UI into the Options dialog.

—  Options have to be persisted between Visual Studio sessions.

—  Decoration attributes are added to the package class: just like tool windows, menus and services, option pages also have to be registered to support the on-demand package loading mechanism.

In the previous part we saw how the option pages are registered with PowerCommands:

// --- Some attributes have been omitted

[ProvideProfileAttribute(typeof(CommandsPage), "PowerCommands", "Commands", 15600,

  1912, true, DescriptionResourceID = 197)]

[ProvideOptionPageAttribute(typeof(CommandsPage), "PowerCommands", "Commands",

  15600, 1912, true)]

[ProvideProfileAttribute(typeof(GeneralPage), "PowerCommands", "General", 15600,

  4606, true, DescriptionResourceID = 2891)]

[ProvideOptionPageAttribute(typeof(GeneralPage), "PowerCommands", "General", 

  15600, 4606, true)]

public sealed class PowerCommandsPackage : Package, IVsInstalledProduct

{

  // --- Package body

}

In Part #19 I have treated attributes in details; here I just summarized the information there: ProvideOptionPage attribute declares the information required for accessing option pages. The ProvideProfile attribute declares information required for persisting information with the standard VS setting management mechanism. As a result of using ProvideProfile here the information set in option pages is persisted by Visual Studio.

Now, let’s have a look at the Options dialog after PowerCommands has been installed:

In the figure we see the PowerCommands category with two option pages named General and Commands as the selected page. Both pages have separate classes responsible to hold option values.

“General” option page

To introduce the concept of option pages implementation of the General page is a good candidate due to its simplicity. The implementation of this option page contains the GeneralControl class that is a WinForms user control and GeneralPage class that is inherited from the DialogPage class of the Microsoft.VisualStudio.Shell namespace. The UI of the page contains only two checkboxes:

The code behind this user control is simple:

using System;

using System.Windows.Forms;

 

namespace Microsoft.PowerCommands.OptionPages

{

  public partial class GeneralControl : UserControl

  {

    private GeneralPage optionPage;

 

    public GeneralPage OptionPage

    {

      get { return optionPage; }

      set { optionPage = value; }

    }

 

    public GeneralControl()

    {

      InitializeComponent();

    }

 

    private void chkFormatOnSave_CheckedChanged(object sender, EventArgs e)

    {

      OptionPage.FormatOnSave = chkFormatOnSave.Checked;

    }

 

    private void RemoveAndSortUsingsOnSave_CheckedChanged(object sender,

      EventArgs e)

    {

      OptionPage.RemoveAndSortUsingsOnSave = chkRemoveAndSortUsingsOnSave.Checked;

    }

 

    private void GeneralControl_Load(object sender, EventArgs e)

    {

      chkFormatOnSave.Checked = OptionPage.FormatOnSave;

      chkRemoveAndSortUsingsOnSave.Checked = OptionPage.RemoveAndSortUsingsOnSave;

    }

  }

}

The control can interact with the option page behind it through its OptionPage property that is assumed to set before the user control is first displayed. When it’s time to display the page, the GeneralControl_Load event updates the controls to be displayed from the information stored behind OptionPage. When the user changes the information displayed, changes are propagated back to the store—to OptionPage.

As you see, there is a clear separation between the UI of the page and the information it holds. The other key class in this pattern is the GeneralPage type:

using System;

using System.ComponentModel;

using System.Drawing;

using System.Runtime.InteropServices;

using System.Windows.Forms;

using Microsoft.VisualStudio.Shell;

 

namespace Microsoft.PowerCommands.OptionPages

{

  [ComVisible(true)]

  [ClassInterface(ClassInterfaceType.AutoDual)]

  [Guid("DF0D89F1-C9A3-47BF-B277-42E0C178F1A0")]

  public class GeneralPage : DialogPage

  {

    GeneralControl control;

    private bool formatOnSave;

    public bool FormatOnSave

    {

      get { return formatOnSave; }

      set { formatOnSave = value; }

    }

 

    private bool removeAndSortUsingsOnSave;

    public bool RemoveAndSortUsingsOnSave

    {

      get { return removeAndSortUsingsOnSave; }

      set { removeAndSortUsingsOnSave = value; }

    }

 

    [Browsable(false),

      DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]

    protected override IWin32Window Window

    {

      get

      {

        control = new GeneralControl();

        control.Location = new Point(0, 0);

        control.OptionPage = this;

        return control;

      }

    }

  }

}

The DialogPage base class implements the mechanism to integrate the page’s UI into the Options dialog and also the persistence of settings on the page. The FormatOnSave and RemoveAndSortUsingsOnSave Boolean properties are the ones that store the option information. When information is about to read from the VS setting store or to write, the mechanism behind DialogPage uses properties. It reads or writes them and persists (or recalls) them through serialization.

The Window property provides the binding between the Options dialog and the page UI. It works exactly on the same way as in case of tool windows (exactly the same UI binding mechanism is behind). The method returns the window handle of the GeneralControl instance created here. Although this code works well it would be better to initialize control in the class constructor and simply returning the instance here.

Since the Window property is not to be written into the VS setting store, it is marked with the Browsable(false) and DesignerSerializationVisibility(Hidden) attributes.

“Commands” option page

The structure of the “Commands” option page uses the same pattern as the General page, so from this aspect I should not tell too much about it. However, the implementation of both the user controls representing the page and the DialogPage derived class behind it has a few great techniques that really worth to look into. Let’s start with the code of the user control:

public partial class CommandsControl : UserControl

{

  ICommandManagerService commandManagerService;

  IList<RowItem> items;

 

  private CommandsPage optionPage;

  public CommandsPage OptionPage

  {

    get { return optionPage; }

    set { optionPage = value; }

  }

 

  public CommandsControl()

  {

    InitializeComponent();

  }

 

  private void CommandsControl_Load(object sender, EventArgs e)

  {

    commandManagerService = optionPage.Site.

      GetService<SCommandManagerService, ICommandManagerService>();

 

    items = commandManagerService.GetRegisteredCommands()

            .OrderBy(command => command.GetType().Name)

            .Select(

                command =>

                new RowItem()

                {

                  Command = command.CommandID,

                  CommandText = GetDisplayName(command.GetType()),

                  Enabled = OptionPage.DisabledCommands.SingleOrDefault(

                              cmd => cmd.Guid.Equals(command.CommandID.Guid) &&

                              cmd.ID.Equals(command.CommandID.ID)) == null

                }).ToList();

 

    gridVisibility.DataSource = items;

    gridVisibility.Columns[0].Width = 200;

    gridVisibility.Columns[0].ReadOnly = true;

  }

 

  private void gridVisibility_CellValueChanged(object sender,

    DataGridViewCellEventArgs e)

  {

    if (e.ColumnIndex == 1)

    {

      RowItem item = gridVisibility.CurrentRow.DataBoundItem as RowItem;

      optionPage.DisabledCommands.Remove(item.Command);

 

      if (!item.Enabled)

      {

        optionPage.DisabledCommands.Add(item.Command);

      }

    }

  }

 

  private void gridVisibility_MouseLeave(object sender, EventArgs e)

  {

    gridVisibility.EndEdit();

  }

 

  private string GetDisplayName(Type command)

  {

    string displayName = string.Empty;

 

    DisplayNameAttribute att =

      TypeDescriptor.GetAttributes(command)

        .OfType<DisplayNameAttribute>()

        .FirstOrDefault();

 

    if (att != null)

    {

      displayName = att.DisplayName;

    }

    return displayName;

  }

}

The control inside keeps a list of RowItem instances describing the status (enabled or disabled) of the command to be displayed in a grid. The CommandsControl_Load event uses a LINQ query to combine information about the commands registered and the status of commands stored in the OptionPage instance. In PowerCommands the ICommandManagerService contract defines the operations to handle command registrations.

Note, that we request GetService operation from the site of the user control. It works, since implements the IServiceProvider interface and accesses the same chain of service providers as the package can access.

Another nice feature is the way display names of commands are collected. Each class representing commands is decorated with the DisplayName attribute like this:

  [Guid("7F95D8FB-4996-4763-AF41-A2154A831F77")]

  [DisplayName("Copy Path")]

  internal class CopyPathCommand : DynamicCommand

  { ... }

The GetDisplayName private method extracts the value of the attribute with a LINQ query.

Not only the user control but also the CommandsPage class has a few pearls to show.

[ComVisible(true)]

[ClassInterface(ClassInterfaceType.AutoDual)]

[Guid("7A9E9816-5ADD-4CBD-9C46-1901A492640D")]

public class CommandsPage : DialogPage

{

  CommandsControl control;

  private IList<CommandID> disabledCommands = new List<CommandID>();

 

  [TypeConverter(typeof(DisabledCommandsDictionaryConverter))]

  public IList<CommandID> DisabledCommands

  {

    get { return disabledCommands; }

    set { disabledCommands = value; }

  }

 

  [Browsable(false),

    DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]

  protected override IWin32Window Window

  {

    get

    {

      control = new CommandsControl();

      control.Location = new Point(0, 0);

      control.OptionPage = this;

      return control;

    }

  }

}

The option page keeps track of disabled commands. When information is persisted, the DisabledCommands property uses a custom type converter (the DisabledCommandsDictionaryConverter) to manage the serialization. This converter converts the value of that property to a string and vice versa:

public class DisabledCommandsDictionaryConverter : StringConverter

{

  IList<CommandID> disabledCommands;

 

  public override bool CanConvertFrom(ITypeDescriptorContext context,

    Type sourceType)

  {

    return true;

  }

 

  public override bool CanConvertTo(ITypeDescriptorContext context,

    Type destinationType)

  {

    return true;

  }

 

  public override object ConvertTo(ITypeDescriptorContext context,

    CultureInfo culture, object value, Type destinationType)

  {

    disabledCommands = value as IList<CommandID>;

    StringBuilder builder = new StringBuilder();

    disabledCommands.ForEach(

        cmdId =>

        {

          builder.Append(string.Format("{0},{1};", cmdId.Guid, cmdId.ID));

        });

    return builder.ToString();

  }

 

  public override object ConvertFrom(ITypeDescriptorContext context,

    CultureInfo culture, object value)

  {

    Guid cmdGuid;

    int cmdId;

    disabledCommands = new List<CommandID>();

 

    try

    {

      if (!string.IsNullOrEmpty(value.ToString()))

      {

        value.ToString().Split(';').ForEach(

            item =>

            {

              string[] subItems = item.Split(',');

              if (subItems.Count() == 2)

              {

                cmdGuid = new Guid(subItems[0]);

                int.TryParse(subItems[1], out cmdId);

                disabledCommands.Add(new CommandID(cmdGuid, cmdId));

              }

            });

      }

    }

    catch

    {

    }

    return disabledCommands;

  }

}

This class handles a simple string representation of disabled commands: commands are separated by semicolons; within a command GUID and ID are divided by a comma. The implementation of ConvertFrom and ConvertTo uses LINQ extension methods and lambda expressions presenting a cool solution.

Command user interfaces

There are a few commands having their own user interfaces, like Extract to Constant or Clear Recent File List. Event the user interfaces are simple, Pablo Galiano made a clear and great implementation to follow. He used WPF forms with styles in XAML and used the MVP (Model-View-Presenter) pattern for binding commands with their UI. In this part of this paper I go into implementation details.

The Model-View-Presenter pattern

Giving even a brief overview about MVP—do not mix Model-View-Presenter with Most Valuable Professional J—is beyond the scope of this deep dive, you can find a good article on this topic by Jean-Paul Boodhoo here. The Mvp folder of PowerCommands source contains a lightweight implementation of this design pattern.

MVP pattern separates the responsibility of the UI in a clear way and provides an implementation pattern that help TDD style development. It is considered a derivative of MVC (Model-View-Controller) pattern. I tried to make a good description of the pattern, but when I read a good summary on Wikipedia I decided to cite the pattern description from the first to the last word:

The View is defined as an interface that the Presenter will use for getting and setting data to and from the Model. The View implementation will instantiate the Presenter object and provide a reference to itself (the formal constructor parameter is the View interface while the actual parameter is a concrete View class). When the event methods of the View are triggered, they will do nothing else than invoking a method of the Presenter which have no parameters and no return value. The Presenter will then get data from the View, through the View interface variable that the Presenter stored when the constructor was called. Then the Presenter invokes methods of the Model, and then it sets data from the Model into the View through the View interface.

Pablo’s implementation in PowerCommands extracts some stereotype behavior for this pattern. For example, the behavior for a modal dialog can be described as in the IModalView.cs file:

public interface IModalView

{

  void OK();

  void Cancel();

}

There is also a definition of the Presenter behavior:

// --- IPresenter.cs

using System.Collections;

 

namespace Microsoft.PowerCommands.Mvp

{

  public interface IPresenter

  {

    ICollection CommandBindings { get; }

  }

}

 

// --- Presenter.cs

using System.Collections;

using System.Collections.Generic;

using System.Windows.Input;

 

namespace Microsoft.PowerCommands.Mvp

{

  public class Presenter<TModel, IView> : IPresenter

  {

    private List<CommandBinding> bindings = new List<CommandBinding>();

 

    public Presenter(TModel model, IView view)

    {

      this.Model = model;

      this.View = view;

    }

 

    protected IView View { get; private set; }

    protected TModel Model { get; private set; }

 

    protected void AddCommandBinding(CommandBinding binding)

    {

      bindings.Add(binding);

    }

 

    ICollection IPresenter.CommandBindings

    {

      get { return bindings; }

    }

  }

}

The MVP pattern defines the responsibilities and the cooperation among the roles. This cooperation is captured by the MvpFactory class:

using System;

using System.Windows;

 

namespace Microsoft.PowerCommands.Mvp

{

  public static class MvpFactory

  {

    public static void Create<TModel, IView, TView, TPresenter>(out TModel model,

      out TView view, out TPresenter presenter)

      where TModel : new()

      where IView : class

      where TView : FrameworkElement, IView, new()

      where TPresenter : Presenter<TModel, IView>

    {

      model = new TModel();

      view = new TView();

      presenter =

        (TPresenter)Activator.CreateInstance(typeof(TPresenter), model, view);

      AddCommandBindings(view, presenter);

      view.DataContext = model;

    }

 

    private static void AddCommandBindings(FrameworkElement view,

      IPresenter presenter)

    {

      view.CommandBindings.AddRange(presenter.CommandBindings);

    }

  }

}

If you go back to the definition cited, you will recognize that MvpFactory puts together the pieces. Looking at the generic constraints, you can see that TView is declared as a FrameworkElement derived type, so it is strongly bound to WPF. If you know WPF, you may guess its architects designed it with MVP in mind, at that is not far away from the reality. Look at the elegant view.DataContext = model assignment: here we connect the view with its model in a “native” way.

Extract to Constant Command UI

To demonstrate how to use the MVP pattern the best solution is to dive into the details of the Extract to Constant command. Here I show you how the parts of this command are put together to form the whole function, but right now I will not explain how actually the constant extraction is carried out.

The command implemented in the OnExecute method of the ExtractToConstantCommand class:

private static void OnExecute(object sender, EventArgs e)

{

  // --- Some code omitted, we focus on the UI code

  // --- Initializing the UI with MVP pattern

  ExtractToConstantModel model;

  ExtractToConstantView view;

  ExtractToConstantPresenter presenter;

 

  MvpFactory.Create<ExtractToConstantModel, IModalView, ExtractToConstantView,

    ExtractToConstantPresenter>(out model, out view, out presenter);

  try

  {

    // --- Some preparation code omitted

    if ((bool)view.ShowDialog())

    {

      // --- “OK” button is pressed in the dialog

    }

  }

  catch (COMException)

  {

  }

}

The method prepares the UI by creating the cooperating elements with the Create method of the MvpFactory static class. The ExctractToConstantModel and ExtractToConstantPresenter represent the corresponding Model and Presenter roles in the MVP pattern. The view is stereotyped by IModalView and is implemented by ExtractToConstantView. The physical view is a WPF form defined in XAML:

  

The view behaves so that it allows selecting a visibility specifier from the combobox. The Identifier textbox indicates with a red border if the given identifier is invalid and disables the OK button. The code behind the view is really simple:

// --- Using clauses omitted for clarity

namespace Microsoft.PowerCommands.Commands.UI

{

  public partial class ExtractToConstantView : Window, IModalView

  {

    public ExtractToConstantView()

    {

      InitializeComponent();

    }

 

    void IModalView.OK()

    {

      this.DialogResult = true;

      this.Close();

    }

 

    void IModalView.Cancel()

    {

      this.DialogResult = false;

      this.Close();

    }

  }

}

This simplicity comes from the fact that the view itself does not deal with the model through code, but via XAML template bindings. The view uses XAML resources, styles and a few other mechanisms of WPF that is beyond the scope of this paper. The presenter binds the view with the model:

using System.Windows.Input;

using Microsoft.PowerCommands.Mvp;

 

namespace Microsoft.PowerCommands.Commands.UI

{