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

LearnVSXNow! #21 - PowerCommands Deep Dive — Analyzing Commands

 

In the previous two parts of this mini-series within the mainstream of LearnVSXNow (Part #19 and #20) I went into the details about the basic architecture of PowerCommands. In the first part we saw the architecture related to command execution; in the second part we looked behind the user interface.

As I mentioned in the introduction of Part #19, PowerCommands is full with values. In this article I go through the code of a few commands showing how those are implemented. In the code you can find many useful code snippets and patterns for your own development; I’d like to encourage you to use them.

I go deep in the following commands:

—  Clear All Panes

—  Clear Recent Project and File List

—  Close All

—  Undo Close

Clear All Panes Command

This command clears all the panes of the output window. The command itself is added to the toolbar of the output window with the following parts of the .vsct file:

<CommandTable>

  <Commands package="guidPowerCommandsPkg">

    <Groups>

      <Group guid="guidClearAllPanesCommand" id="grpidClearAllPanesCommandGroup"

        priority="0x0100">

        <Parent guid="guidSHLMainMenu" id="IDM_VS_TOOL_OUTPUTWINDOW" />

      </Group>

    </Groups>

    <Buttons>

      <Button guid="guidClearAllPanesCommand" id="cmdidClearAllPanesCommand"

        priority="0x0100" type="Button">

        <Parent guid="guidClearAllPanesCommand"

          id="grpidClearAllPanesCommandGroup" />

        <Icon guid="guidClearAllPanesCommandBitmap" id="bmpPic1" />

        <CommandFlag>DynamicVisibility</CommandFlag>

        <CommandFlag>DefaultInvisible</CommandFlag>

        <Strings>

          <ButtonText>Clear All Panes</ButtonText>

        </Strings>

      </Button>

    </Buttons>

  </Commands>

</CommandTable>

The key to attach the command to the toolbar of the Output window is the IDM_VS_TOOL_OUTPUTWINDOW identifier in the logical command group named by the guidSHLMainMenu.

The code executed by the command is simple:

private static void OnExecute(object sender, EventArgs e)

{

  ((DTE2)DynamicCommand.Dte).ToolWindows.OutputWindow.OutputWindowPanes

    .OfType<OutputWindowPane>()

    .ForEach(pane => pane.Clear());

}

This definition is a great example of how to use LINQ and the C# 3.0 syntax to reduce the syntax noise caused by imperative programming. Let me spend a little time with it to explain its value!

The developer’s intention with this command is to iterate through all the panes of the Output window and call the Clear method on each pane. The method above expresses this intention. In the code above OutputWindowPanes is IEnumerable (non-generic), and so we cannot apply directly the ForEach operator on it. This is why we need the OfType<OutputWindowPane> operator: its role is actually converting the typeless IEnumerable resulting from OutputWindowPanes to a strongly typed collection in order the ForEach method can be applied. The compiler’s type inference feature simplifies the action to the pane => pane.Clear(); lambda expression.

Without LINQ and C# 3.0, this command would look like this (using the imperative approach):

foreach (object obj in

  ((DTE2)DynamicCommand.Dte).ToolWindows.OutputWindow.OutputWindowPanes)

{

  OutputWindowPane pane = obj as OutputWindowPane;

  if (pane != null)

  {

    pane.Clear();

  }

}

In a perfect world, where we would have managed objects for each abstraction behind VS SDK and not only COM interop classes, the body of the method above would even be shorter:

Dte.ToolWindows.OutputWindow.OutputWindowPanes

  .ForEach(pane => pane.Clear());

Clear Recent Project List Command

If happens often that we have projects on the recent project list that we do not want to see there either because they just reserve place or we have already deleted them. This command helps to solve this issue. When activating it, we have a dialog with the list of recent projects and we can select all or just a few of them to be removed from the list.

In this part I show you a few implementation details of this command.

The menu item for the Clear Recent Project list is added to the File|Recent Projects menu as the last menu item. This placement is arranged by the following code in the .vsct file:

<CommandTable>

  <Commands package="guidPowerCommandsPkg">

    <Buttons>

      <Button guid="guidClearRecentProjectListCommand"

        id="cmdidClearRecentProjectListCommand" priority="0x0100" type="Button">

        <Icon guid="guidClearRecentProjectListCommandBitmap" id="bmpPic1" />

        <CommandFlag>DynamicVisibility</CommandFlag>

        <CommandFlag>DefaultInvisible</CommandFlag>

        <Strings>

          <ButtonText>Clear Recent Project List</ButtonText>

        </Strings>

      </Button>

    </Buttons>

  </Commands>

  <CommandPlacements>

    <CommandPlacement guid="guidClearRecentProjectListCommand"

      id="cmdidClearRecentProjectListCommand" priority="0x0100">

      <Parent guid="guidSHLMainMenu" id="IDG_VS_FILE_PMRU_CASCADE"/>

    </CommandPlacement>

  </CommandPlacements>

</CommandTable>

By using the IDG_VS_FILE_PMRU_CASCADE command group identifier we can put the command into its right place. The command execution is implemented in the ClearRecentProjectListCommand class:

internal class ClearRecentProjectListCommand : DynamicCommand

{

  // --- Constructor omitted

  protected override bool CanExecute(OleMenuCommand command)

  {

    if (base.CanExecute(command))

    {

      using (RegistryKey rootKey = GetRecentRootKey())

      {

        if (rootKey != null)

        {

          return (rootKey.GetValueNames().Length > 0);

        }

      }

    }

    return false;

  }

 

  private static void OnExecute(object sender, EventArgs e)

  {

    ClearListView view = new ClearListView();

    using (RegistryKey key = GetRecentRootKey())

    {

      key.GetValueNames().ForEach(

          valueName => view.Model.ListEntries.Add(new KeyValue(valueName,

          key.GetValue(valueName).ToString())));

      if ((bool)view.ShowDialog())

      {

        DynamicCommand.Dte.ExecuteCommand("File.SaveAll", string.Empty);

        DeleteRecentFileList(view.Model.SelectedListEntries);

        ReEnumerateFileList();

        DTEHelper.RestartVS(DynamicCommand.Dte);

      }

    }

  }

}

Information about recent projects is stored in the registry. The GetRecentRootKey method is responsible to access that key. Each project on the list is stored in its own registry key under the root key. The CanExecute method simply checks if this root key has any child items and enables or disables the command accordingly.

The OnExecute method is responsible for carry out the action if the command is enabled. It uses the ClearListView’s Model with the list of projects on the recent list and allows the user selecting items on the list. If the dialog returns with a confirmation that the user really wants to delete the selected projects from the list, the real action starts:

 First all files are saved, then the selected entries are removed from the registry. In order the recent project list be refreshed in Visual Studio, VS should be restarted.

Obtaining the root registry key

Our computer may have more than one instance of VS installed. For example after installing VS SDK beside the “live” VS we can run the Experimental Hive. Each VS instance uses a root registry key of its own. How to decide where to look for registry information of a running instance? The “magic” is implemented in the GetRecentRootKey private method of the command class:

private static RegistryKey GetRecentRootKey()

{

  string registryKey;

  ILocalRegistry2 localRegistry =

    DynamicCommand.ServiceProvider.GetService<SLocalRegistry, ILocalRegistry2>();

  localRegistry.GetLocalRegistryRoot(out registryKey);

  return Registry.CurrentUser.OpenSubKey(

          string.Format(@"{0}\{1}\",

          registryKey,

          Microsoft.PowerCommands.Common.Constants.ProjectMRUListKey), true);

}

The shell offers the ILocalRegistry2 interface through the SLocalRegistry service that has a GetLocalRegistryRoot method to obtain that information. The “ProjectMRUList” key under this root is the container of recent projects. For example VS 2008 hold this list under the HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\9.0\ProjectMRUList key.

Deleting the selected entries

The ClearListView’s model has a property called SelectedListEntries with the list of items marked by the user for deletion. Using LINQ it is elegant to write down the code for deleting the selected entries:

private static void DeleteRecentFileList(List<KeyValue> entriesToDelete)

{

  using (RegistryKey key = GetRecentRootKey())

  {

    if (key != null)

    {

      entriesToDelete.ForEach(entry => key.DeleteValue(entry.Key));

    }

  }

}

Re-enumerating the list of files

The entries in the recent project list have value names like FileN where N is the file index from 1 up to 25. After deleting entries there can be gaps in the continuity of key indices. The ReEnumerateFileList method cuts off these gaps by recreating the entries:

private static void ReEnumerateFileList()

{

  int fileCounter = 1;

  using (RegistryKey key = GetRecentRootKey())

  {

    string[] valueNames = key.GetValueNames();

    if (valueNames.Length > 0)

    {

      valueNames.ForEach(

        valueName =>

        {

          key.SetValue(string.Concat(valueName, "_"), key.GetValue(valueName));

          key.DeleteValue(valueName);

        });

      valueNames = key.GetValueNames();

      valueNames.ForEach(

        valueName =>

        {

          key.SetValue(string.Format("File{0}", fileCounter),

            key.GetValue(valueName));

          key.DeleteValue(valueName);

          fileCounter++;

        });

    }

  }

}

The first ForEach recreates the values with a new underscore prefix and then the second re-indexes these values with new continuous indices.

Restarting Visual Studio

In order to refresh the Recent Project List in VS, changing the registry is not enough, VS should be restarted in order changes are reflected in the menu. This is done by the RestartVS static method of the DTEHelper class:

public static void RestartVS(DTE dte)

{

  System.Diagnostics.Process vs = new System.Diagnostics.Process();

  string[] args = Environment.GetCommandLineArgs();

  vs.StartInfo.FileName = Path.GetFullPath(args[0]);

  vs.StartInfo.Arguments = string.Join(" ", args, 1, args.Length - 1);

  vs.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Maximized;

  vs.Start();

  dte.Quit();

}

The method is simple but contains a few “tricks”. A new process is created and then parameterized according to the currently running VS process. First the new process is started then the running VS is quitted using the DTE instance passed as an input parameter.

Similarity with the Clear Recent File List command

The Clear Recent File List has the same pattern to remove items from the recent list as the Clear Recent Project List command. For files, the .vsct attaches the corresponding command to the IDG_VS_FILE_FMRU_CASCADE group. The registry key where the recent file list can be found is under the FileMRUList key.

Close All Command

This command is the one that is really missing from Visual Studio—I do not really know why. There is a Close All But This command in the context menu of document tabs. PowerCommands fills up a gap with implementing Close All. The command is placed to the document tab by the following definition in the command table:

<CommandTable>

  <Commands package="guidPowerCommandsPkg">

    <Groups>

      <Group guid="guidCloseAllDocumentsCommand"

        id="grpidCloseAllDocumentsCommandGroup" priority="0x0100">

        <Parent guid="guidCloseAllDocumentsCommandParentGroup"

          id="grpidCloseAllDocumentsCommandParentGroup" />

      </Group>

    </Groups>

    <Buttons>

      <Button guid="guidCloseAllDocumentsCommand"

        id="cmdidCloseAllDocumentsCommand" priority="0x0100" type="Button">

        <Parent guid="guidCloseAllDocumentsCommand"

          id="grpidCloseAllDocumentsCommandGroup" />

        <Icon guid="guidCloseAllDocumentsCommandBitmap" id="bmpPic1" />

        <CommandFlag>DynamicVisibility</CommandFlag>

        <CommandFlag>DefaultInvisible</CommandFlag>

        <Strings>

          <ButtonText>Close All</ButtonText>

        </Strings>

      </Button>

    </Buttons>

  </Commands>

  <GuidSymbol name="guidCloseAllDocumentsCommandParentGroup"

    value="{D309F791-903F-11D0-9EFC-00A0C911004F}">

    <IDSymbol name="grpidCloseAllDocumentsCommandParentGroup" value="1067" />

  </GuidSymbol>

</CommandTable>

It seems a bit workaround about how the Group representing the Close All command is bound to the document tab. It can be done easier by simply changing the Group definition above to the following:

<Group guid="guidCloseAllDocumentsCommand" id="grpidCloseAllDocumentsCommandGroup"

  priority="0x0100">

  <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_EZDOCWINTAB" />

</Group>

The code executing the command is the simplest that can be at all:

private static void OnExecute(object sender, EventArgs e)

{

  DynamicCommand.Dte.ExecuteCommand("Window.CloseAllDocuments", string.Empty);

}

The key is the ExecuteCommand method of the DTE object where we pass two arguments: the first is the name of the command (we can use this name in the Command window); the second are the optional command arguments. Using the Window.CloseAllDocuments name it does what our command is expected to do.

Undo Close Command

This is one of my favorite commands. Very often happens to me that I close the wrong document. If a have a solution with a few hundred source files it is quite frustrating to search for a file in the Solution Explorer just to open again. Undo Close helps me to solve this issue.

This command is not just very useful, but its architecture allows me to show you precious patterns in PowerCommands that can be used in VSPackage programming.

Overview of Undo Close Command

The functionality of Undo Close can be accessed from the tool window having the same name as the command. This tool window displays a list of recently closed document in a stack where the document at the top is the one recently closed. Selecting one command from this stack and double clicking on it opens it and removes from the stack.

But how this tool window knows which documents are to be displayed? It communicates with a service called UndoCloseManagerService that keeps track of these docs and provides the operations related to manage them. So, UndoCloseManagerService knows those documents. But how it gets the information that a certain document is closed?

The package uses so-called listeners that watch events within the Visual Studio Shell. When they catch an event that relates to closing a document (or even opening it) they communicate with UndoCloseManagerService in order it can handle the proper list of documents.

The UndoCloseManagerService

This service keeps track of recently closed documents. The implementation of the service follows the standard VS SDK service implementation pattern with three types: a service interface, a service “address” markup interface and a service implementation class:

// --- IUndoCloseManagerService.cs

[Guid("AF0C3D86-775F-4BEF-AB72-87D18E36873D")]

[ComVisible(true)]

public interface IUndoCloseManagerService

{

  void PushDocument(IDocumentInfo document);

  IDocumentInfo PopDocument();

  IDocumentInfo PopDocument(IDocumentInfo document);

  IDocumentInfo CurrentDocument { get; }

  IEnumerable<IDocumentInfo> GetDocuments();

  void ClearDocuments();

}

 

// --- SUndoCloseManagerService.cs

[Guid("69CE37EC-74F7-44E8-B915-51D0CE0459A5")]

public interface SUndoCloseManagerService

{

}

 

// --- UndoCloseManagerService.cs

internal class UndoCloseManagerService :

  IUndoCloseManagerService,

  SUndoCloseManagerService

{

  // ...

}

The service uses a stack inside to keep track of recently closed documents. The PushDocument operation puts the closed document—represented by IDocumentInfo—to the top of the stack, PopDocument() takes it off from the top. There is an overloaded PopDocument(IDocumentInfo) that is able to take off an entry from any position of the stack.

IDocumentInfo shows what kind of information is kept on the stack:

public interface IDocumentInfo

{

  string DocumentPath { get; set; }

  int CursorLine { get; set; }

  int CursorColumn { get; set; }

  string DocumentViewKind { get; set; }

}

Not only the full path of the document file is stored but also the last position of the cursor and the kind of the view used to display the document. What the last one is? When we open a document (a file in our project system) it can be displayed by many views. For example, an .xsd file can be displayed as an XML text or the graphical representation of the schema it covers. The textual and the graphical representations are views. Now, let’s see how the UndoCloseManagerService is implemented:

internal class UndoCloseManagerService :

  IUndoCloseManagerService,

  SUndoCloseManagerService

{

  private const int capacity = 20;

  FixedCapacityStack<IDocumentInfo> documents;

 

  public UndoCloseManagerService()

  {

    documents = new FixedCapacityStack<IDocumentInfo>(capacity);

  }

 

  public void PushDocument(IDocumentInfo document)

  {

    IDocumentInfo docInfo = documents.SingleOrDefault(

      info => info.DocumentPath.Equals(document.DocumentPath));

    if (docInfo != null)

    {

      FixedCapacityStack<IDocumentInfo> temp =

        new FixedCapacityStack<IDocumentInfo>(capacity);

      documents.Reverse().ForEach(

        info =>

        {

          if (info.DocumentPath != document.DocumentPath)

          {

            temp.Push(info);

          }

        });

      temp.Push(document);

      documents = temp;

    }

    else

    {

      documents.Push(document);

    }

  }

 

  public IDocumentInfo PopDocument()

  {

    try

    {

      return documents.Pop();

    }

    catch (InvalidOperationException)

    {

      return null;

    }

  }

 

  public IDocumentInfo PopDocument(IDocumentInfo document)

  {

    IDocumentInfo docInfo = documents.SingleOrDefault(

      info => info.DocumentPath.Equals(document.DocumentPath));

    if (docInfo != null)

    {

      FixedCapacityStack<IDocumentInfo> temp =

        new FixedCapacityStack<IDocumentInfo>(capacity);

      documents.Reverse().ForEach(

        info =>

        {

          if (info.DocumentPath != document.DocumentPath)

          {

            temp.Push(info);

          }

        });

      documents = temp;

    }

    return docInfo;

  }

 

  public IDocumentInfo CurrentDocument

  {

    get

    {

      try

      {

        return documents.Peek();

      }

      catch (InvalidOperationException)

      {

        return null;

      }

    }

  }

 

  public void ClearDocuments()

  {

    documents.Clear();

  }

 

  public IEnumerable<IDocumentInfo> GetDocuments()

  {

    return documents;

  }

}

The service uses a FixedCapacityStack structure to keep track of documents. This structure’s name comes from the fact that it can store only a number of items. When more items are to store than its capacity, the element at the bottom of the stack is dropped. In practice the current implementation “remembers” only for the last 20 documents closed.

The methods above are straightforward; maybe PushDocument and PopDocument(IDocumentInfo) need a few comments. When pushing up a document, if the document is already somewhere in the stack, first it is taken off and then put back to the top of the stack. Popping an existing document from the stack uses a simple trick: we pump back the content of the stack into a new instance but leave out the element to be popped.

Handling the events responsible for closed documents

There are three kind of listeners used for guessing out which documents are closed. These are setup in the Initialize method of the package:

public sealed class PowerCommandsPackage : Package, IVsInstalledProduct

{

  protected override void Initialize()

  {

    base.Initialize();

    // --- Non-relevant initialization code omitted

    (this as IServiceContainer).AddService(

          typeof(SUndoCloseManagerService),

          new ServiceCreatorCallback(CreateUndoCloseManagerService),

          true);

    // ...

    // --- Initialize an RTDListener

    RDTListener = new RDTListener(this);

    RDTListener.Initialize();

    RDTListener.BeforeSave += RDTListener_BeforeSave;

    RDTListener.AfterDocumentWindowHide += RDTListener_AfterDocumentWindowHide;

 

    // --- Initialize an SolutionListener

    solutionListener = new SolutionListener(this);

    solutionListener.Initialize();

    solutionListener.AfterCloseSolution += solutionListener_AfterCloseSolution;

    solutionListener.AfterOpenSolution += solutionListener_AfterOpenSolution;

 

    // --- Initialize DocumentListener

    documentListener = new DocumentListener(this);

  }

  // --- Other methods omitted

}

What kind of events do these listener watch and why?

—  RDTListener hooks the events of the Running Document Table (RDT). Visual Studio uses the Running Document Table (RDT) to manage open documents. When a document data is changed, it helps to administer what views and what files (or what other persistence elements, for example database tables, stored procedures, etc.) are modified. When a file or the solution is closed RDT is the infrastructure element in the background that helps Visual Studio to pop-up the Save changes window. From our aspect the only important event is the AfterDocumentWindowHide event that is the one firing when a document has been closed.

—  SolutionListener is bound to the solution-related events. For UndoCloseManagerService the relevant event is the AfterCloseSolutionEvent.

—  DocumentListener hooks the event when a document is closed.

The event methods related to the SolutionListener are crucial for the setup and cleanup tasks related to opening and closing a solution:

int solutionListener_AfterOpenSolution(object pUnkReserved, int fNewSolution)

{

  documentListener.Initialize();

  documentListener.DocumentClosing += documentListener_DocumentClosing;

  return Microsoft.VisualStudio.VSConstants.S_OK;

}

 

int solutionListener_AfterCloseSolution(object pUnkReserved)

{

  UndoCloseManager.ClearDocuments();

  UndoCloseToolWindow.Control.ClearDocumentList();

  return Microsoft.VisualStudio.VSConstants.S_OK;

}

The AfterOpenSolution event sets up the DocumentListener to watch for closed documents. The AfterCloseSolution event clears the list of recently closed documents in the UndoCloseManagerService and form the Undo Close tool window. When a document is about to be closed, the following code is executed:

void documentListener_DocumentClosing(Document Document)

{

  int cursorLine = 1;

  int cursorOffset = 1;

  TextSelection selection = Document.Selection as TextSelection;

  if (selection != null)

  {

    cursorLine = selection.ActivePoint.Line;

    cursorOffset = selection.ActivePoint.LineCharOffset;

  }

  if (docInfo != null && !docInfo.DocumentPath.Equals(Document.FullName))

  {

    if (Document.FullName.IndexOf(docInfo.DocumentPath) == -1)

    {

      // --- Prevent adding code behind documents

      docInfo = new DocumentInfo(Document.FullName, cursorLine, cursorOffset,

        GetViewKind(Document));

    }

  }

  else

  {

    docInfo = new DocumentInfo(Document.FullName, cursorLine, cursorOffset,

      GetViewKind(Document));

  }

}

This method puts the information about the document into the docInfo field of the package so that it can be sent to the UndoCloseManagerService later. Not only the name and path of the document is saved but also the current cursor position and the view used by the document when it was closed. The event handler code for these events are private methods in the package. When a document is closed, the RDTListener_AfterDocumentWindowHide method is invoked:

int RDTListener_AfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame)

{

  if (CommandsPage.DisabledCommands.SingleOrDefault(

      cmd => cmd.Guid.Equals(typeof(UndoCloseCommand).GUID) &&

             cmd.ID.Equals((int)UndoCloseCommand.cmdidUndoCloseCommand)) == null)

  {

    // --- Prevent being called twice

    if (UndoCloseManager.CurrentDocument == null ||

      !UndoCloseManager.CurrentDocument.DocumentPath.Equals(docInfo.DocumentPath))

    {

      UndoCloseManager.PushDocument(docInfo);

      UndoCloseToolWindow.Control.UpdateDocumentList();

    }

  }

  return Microsoft.VisualStudio.VSConstants.S_OK;

}

The method communicates with the UndoCloseManagerService only if the Undo Close Command is enabled in the configuration options. The method pushes the document to the undo stack and updates the view of the Undo Close tool window.

The Undo Close tool window

Just like other tool windows in packages created by MPF, the Undo Close Tool window uses the pattern separating UI roles into a WindowPane derived class and a WinForms user control derived class. The UndoCloseToolWindow class is inherited from WindowPane and it is straightforward, so I do not treat it here. The UndoCloseControl class represents the UI of the tool window and also implements user interaction. This class hosts a single ListViewControl enumerating the recently closed files in their stack order. UndoCloseControl has the following blueprint:

public partial class UndoCloseControl : UserControl

{

  // --- Constructors

  public UndoCloseControl();

  public UndoCloseControl(IServiceProvider provider);

 

  // --- Public methods

  public void UpdateDocumentList();

  public void ClearDocumentList();

 

  // --- Private methods

  private void UndoCloseControl_Load(object sender, EventArgs e);

  private void lstDocuments_DoubleClick(object sender, EventArgs e);

  private void lstDocuments_KeyPress(object sender, KeyPressEventArgs e);

  private void OpenDocument();

  private void InitializeList();

}

The default constructor is used only by the WinForms designer; the window pane class uses the other one accepting the IServiceProvider parameter. This is the provider we use to obtain VS services from the user control.

From the methods above the two interesting methods are UpdateDocumentList and OpenDocument. The first has a nice pattern to display icons associated with the recently closed documents. The second demonstrates how to open a document in VS by code.

The implementation of UpdateDocumentList is the following:

public void UpdateDocumentList()

{

  lstDocuments.BeginUpdate();

  ClearDocumentList();

  undoCloseManager.GetDocuments().ForEach(

    info =>

    {

      string imageKey;

      Icon icon = null;

      imageKey = Path.GetExtension(info.DocumentPath);

      if (!lstDocuments.SmallImageList.Images.ContainsKey(imageKey))

      {

        icon = NativeMethods.GetIcon(info.DocumentPath);

        if (icon != null)

        {

          lstDocuments.SmallImageList.Images.Add(imageKey, icon);

        }

      }

      ListViewItem item = new ListViewItem(info.DocumentPath, rootGroup)

        { Tag = info, ImageKey = imageKey };

      lstDocuments.Items.Add(item);

    });

  lstDocuments.EndUpdate();

}

The “trick” of this method is that it dynamically refreshes the image list of the ListView with the icon associated to the file extension. It uses the GetIcon method of the NativeMethod helper class to obtain the icon related to the file extension.

OpenDocument’s implementation is also simple:

private void OpenDocument()

{

  ListViewItem item = lstDocuments.SelectedItems.OfType<ListViewItem>().First();

  DocumentInfo docInfo = item.Tag as DocumentInfo;

 

  if (docInfo != null)

  {

    undoCloseManager.PopDocument(docInfo);

    lstDocuments.Items.Remove(item);

 

    if (File.Exists(docInfo.DocumentPath))

    {

      DTE dte = serviceProvider.GetService<SDTE, DTE>();

      try

      {

        DTEHelper.OpenDocument(dte, docInfo);

      }

      catch (COMException)

      { }

    }

  }

}

The method obtains the information for the selected document from the UndoCloseManagerService and uses the DTEHelper class to open it. The OpenDocument method of DTEHelper uses the passed docInfo to open the document:

public static void OpenDocument(DTE dte, IDocumentInfo docInfo)

{

  if (File.Exists(docInfo.DocumentPath))

  {

    Window window = dte.OpenFile(docInfo.DocumentViewKind, docInfo.DocumentPath);

    if (window != null)

    {

      window.Visible = true;

      window.Activate();

 

      if (docInfo.CursorLine > 1 || docInfo.CursorColumn > 1)

      {

        TextSelection selection = window.Document.Selection as TextSelection;

 

        if (selection != null)

        {

          selection.MoveTo(docInfo.CursorLine, docInfo.CursorColumn, true);

          selection.Cancel();

        }

      }

    }

  }

}

The OpenFile method of DTE is used to open the file by with the specified view and fully specified file name. In case the cursor information was stored that is restored.

Where we are?

In this article we went deep into the code of a few command implementations. I found a few patterns and code snippet very useful, here is a short list of them:

—  Adding command buttons to the output window and to the document tabs

—  Using LINQ and C# 3.0 syntax for command execution

—  Adding  command buttons to the Recent Project and Recent File menus

—  Accessing the registry entries of the currently running VS instance

—  Restarting Visual Studio

—  Executing built-in VS commands through the DTE object

—  Responding to events raised by the solution, any document or the Running Document Table

—  Accessing information related to a document

—  Closing and opening documents

Use and modify these snippets and patterns in your own VSPackage development projects!


Posted May 20 2008, 08:40 PM by inovak
Filed under: