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

LearnVSXNow! Part #45: Embedding Visual Studio 2010 Editor into a Tool Window

It was a long time ago when I posted the last article of the LearnVSXNow! series, because I was busy with several projects including two books. In the recent days I have spent a lot of time examining the new Visual Studio 2010 editor from extensibility point of view. I needed to embed an editor into a tool window so that I can try how several components work there.

I started from the EditorToolWindow sample posted by Chris Granger who is a PM in the Visual Studio Editor Team.

This sample is great but did not show several things such as turning on or off read-only mode or creating a tool window where the editor is accompanied by several other controls. In this blog post I show you how you can solve these things.

The editor window is designed so that it can be embedded into tool windows. Visual Studio itself uses the editor in several tool windows, for example, in the Output window and in Find Result windows. You can easily create your own tool windows embedding the editor, but it is not as trivial as creating a tool window embedding your own WPF user controls. The CodeEditorInToolWindow sample demonstrates how you can reuse the editor.

This sample is a Visual Studio package that implements two tool windows and adds related display commands to the View ð Other Windows menu:

  • ToolWindowWithEditor places the editor so that it uses the full client area of the tool window.
  • ToolWindowWithCompoundEditor creates a grid and puts the editor into one of the grid cells. It also defines a checkbox you can use to turn on or off read-only mode.

Package Type Definition

Listing 1 shows the definition of the class representing the Visual Studio package hosting the tool windows.

Listing 1: CodeEditorInToolWindowPackage.cs

  1. using System;
  2. using System.Runtime.InteropServices;
  3. using System.ComponentModel.Design;
  4. using Microsoft.VisualStudio;
  5. using Microsoft.VisualStudio.Shell.Interop;
  6. using Microsoft.VisualStudio.Shell;
  7.  
  8. namespace DiveDeeper.CodeEditorInToolWindow
  9. {
  10.   [PackageRegistration(UseManagedResourcesOnly = true)]
  11.   [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
  12.   [ProvideMenuResource("Menus.ctmenu", 1)]
  13.   [ProvideToolWindow(typeof(ToolWindowWithEditor))]
  14.   [ProvideToolWindow(typeof(ToolWindowWithCompoundEditor))]
  15.   [Guid(GuidList.guidCodeEditorInToolWindowPkgString)]
  16.   public sealed class CodeEditorInToolWindowPackage : Package
  17.   {
  18.     protected override void Initialize()
  19.     {
  20.       base.Initialize();
  21.  
  22.       var mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
  23.       if (null == mcs) return;
  24.       var toolwndCommandID = new CommandID(GuidList.guidCodeEditorInToolWindowCmdSet,
  25.                                            (int)PkgCmdIDList.cmdidShowCodeEditor);
  26.       var menuToolWin = new MenuCommand(ShowToolWindowWithEditor, toolwndCommandID);
  27.       mcs.AddCommand(menuToolWin);
  28.       toolwndCommandID = new CommandID(GuidList.guidCodeEditorInToolWindowCmdSet,
  29.                                        (int)PkgCmdIDList.cmdidShowCompoundCodeEditor);
  30.       menuToolWin = new MenuCommand(ShowToolWindowWithCompoundEditor, toolwndCommandID);
  31.       mcs.AddCommand(menuToolWin);
  32.     }
  33.  
  34.     private void ShowToolWindowWithEditor(object sender, EventArgs e)
  35.     {
  36.       ShowToolWindow(typeof(ToolWindowWithEditor));
  37.     }
  38.  
  39.     private void ShowToolWindowWithCompoundEditor(object sender, EventArgs e)
  40.     {
  41.       ShowToolWindow(typeof(ToolWindowWithCompoundEditor));
  42.     }
  43.  
  44.     private void ShowToolWindow(Type toolWindowType)
  45.     {
  46.       var window = FindToolWindow(toolWindowType, 0, true);
  47.       if ((null == window) || (null == window.Frame))
  48.       {
  49.         throw new NotSupportedException(Resources.CanNotCreateWindow);
  50.       }
  51.       var windowFrame = (IVsWindowFrame)window.Frame;
  52.       ErrorHandler.ThrowOnFailure(windowFrame.Show());
  53.     }
  54.   }
  55. }

This code does not contain any special thing; it initializes the commands popping up tool windows using the standard Visual Studio SDK pattern. However, the definition of the tool window pane is much more complex than the standard pattern.

Tool Window Pane Embedding an Editor

Listing 2 shows the source code of ToolWindowWithEditor class.

Listing 2: ToolWindowWithEditor.cs

  1. using System;
  2. using System.Runtime.InteropServices;
  3. using System.Windows.Forms;
  4. using Microsoft.VisualStudio.ComponentModelHost;
  5. using Microsoft.VisualStudio.Editor;
  6. using Microsoft.VisualStudio.OLE.Interop;
  7. using Microsoft.VisualStudio.Shell.Interop;
  8. using Microsoft.VisualStudio.Shell;
  9. using Microsoft.VisualStudio.Text;
  10. using Microsoft.VisualStudio.Text.Editor;
  11. using Microsoft.VisualStudio.TextManager.Interop;
  12. using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
  13.  
  14. namespace DiveDeeper.CodeEditorInToolWindow
  15. {
  16.   [Guid("de51a9b1-6595-42ef-bd69-ca3457f28ab8")]
  17.   public class ToolWindowWithEditor : ToolWindowPane, IOleCommandTarget
  18.   {
  19.     IVsTextView _ViewAdapter;
  20.     IVsTextBuffer _BufferAdapter;
  21.     IVsEditorAdaptersFactoryService _EditorAdapterFactory;
  22.     IOleServiceProvider _OleServiceProvider;
  23.     ITextBufferFactoryService _BufferFactory;
  24.     IWpfTextViewHost _TextViewHost;
  25.  
  26.     public ToolWindowWithEditor()
  27.       : base(null)
  28.     {
  29.       Caption = Resources.ToolWindowTitle;
  30.       BitmapResourceID = 301;
  31.       BitmapIndex = 1;
  32.     }
  33.  
  34.     override public object Content
  35.     {
  36.       get { return TextViewHost; }
  37.     }
  38.  
  39.     private void InitializeEditor()
  40.     {
  41.       const string message = "This is an editor window embedded into a tool window.";
  42.  
  43.       var componentModel = (IComponentModel)Microsoft.VisualStudio.Shell.Package.
  44.         GetGlobalService(typeof(SComponentModel));
  45.       _OleServiceProvider = (IOleServiceProvider)GetService(typeof(IOleServiceProvider));
  46.       _BufferFactory = componentModel.GetService<ITextBufferFactoryService>();
  47.  
  48.       _EditorAdapterFactory =
  49.         componentModel.GetService<IVsEditorAdaptersFactoryService>();
  50.       _BufferAdapter =
  51.         _EditorAdapterFactory.CreateVsTextBufferAdapter(_OleServiceProvider,
  52.         _BufferFactory.TextContentType);
  53.       _BufferAdapter.InitializeContent(message, message.Length);
  54.  
  55.       _ViewAdapter = _EditorAdapterFactory.CreateVsTextViewAdapter(_OleServiceProvider);
  56.       ((IVsWindowPane)_ViewAdapter).SetSite(_OleServiceProvider);
  57.  
  58.       var initView = new[] { new INITVIEW() };
  59.       initView[0].fSelectionMargin = 0;
  60.       initView[0].fWidgetMargin = 0;
  61.       initView[0].fVirtualSpace = 0;
  62.       initView[0].fDragDropMove = 1;
  63.  
  64.       _ViewAdapter.Initialize(_BufferAdapter as IVsTextLines, IntPtr.Zero,
  65.         (uint)TextViewInitFlags.VIF_HSCROLL |
  66.         (uint)TextViewInitFlags.VIF_VSCROLL |
  67.         (uint)TextViewInitFlags3.VIF_NO_HWND_SUPPORT, initView);
  68.     }
  69.  
  70.     public IWpfTextViewHost TextViewHost
  71.     {
  72.       get
  73.       {
  74.         if (_TextViewHost == null)
  75.         {
  76.           InitializeEditor();
  77.           var data = _ViewAdapter as IVsUserData;
  78.           if (data != null)
  79.           {
  80.             var guid = Microsoft.VisualStudio.Editor.DefGuidList.guidIWpfTextViewHost;
  81.             object obj;
  82.             var hr = data.GetData(ref guid, out obj);
  83.             if ((hr == Microsoft.VisualStudio.VSConstants.S_OK) &&
  84.               obj != null && obj is IWpfTextViewHost)
  85.             {
  86.               _TextViewHost = obj as IWpfTextViewHost;
  87.             }
  88.           }
  89.         }
  90.         return _TextViewHost;
  91.       }
  92.     }
  93.  
  94.     public override void OnToolWindowCreated()
  95.     {
  96.       // --- Register key bindings to use in the editor
  97.       var windowFrame = (IVsWindowFrame)Frame;
  98.       var cmdUi = Microsoft.VisualStudio.VSConstants.GUID_TextEditorFactory;
  99.       windowFrame.SetGuidProperty((int)__VSFPROPID.VSFPROPID_InheritKeyBindings,
  100.         ref cmdUi);
  101.       base.OnToolWindowCreated();
  102.     }
  103.  
  104.     protected override bool PreProcessMessage(ref Message m)
  105.     {
  106.       if (TextViewHost != null)
  107.       {
  108.         // copy the Message into a MSG[] array, so we can pass
  109.         // it along to the active core editor's IVsWindowPane.TranslateAccelerator
  110.         var pMsg = new MSG[1];
  111.         pMsg[0].hwnd = m.HWnd;
  112.         pMsg[0].message = (uint)m.Msg;
  113.         pMsg[0].wParam = m.WParam;
  114.         pMsg[0].lParam = m.LParam;
  115.  
  116.         var vsWindowPane = (IVsWindowPane)_ViewAdapter;
  117.         return vsWindowPane.TranslateAccelerator(pMsg) == 0;
  118.       }
  119.       return base.PreProcessMessage(ref m);
  120.     }
  121.  
  122.     int IOleCommandTarget.Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt,
  123.       IntPtr pvaIn, IntPtr pvaOut)
  124.     {
  125.       var hr =
  126.         (int)Microsoft.VisualStudio.OLE.Interop.Constants.OLECMDERR_E_NOTSUPPORTED;
  127.       if (_ViewAdapter != null)
  128.       {
  129.         var cmdTarget = (IOleCommandTarget)_ViewAdapter;
  130.         hr = cmdTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
  131.       }
  132.       return hr;
  133.     }
  134.  
  135.     int IOleCommandTarget.QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[]
  136.       prgCmds, IntPtr pCmdText)
  137.     {
  138.       var hr =
  139.         (int)Microsoft.VisualStudio.OLE.Interop.Constants.OLECMDERR_E_NOTSUPPORTED;
  140.       if (_ViewAdapter != null)
  141.       {
  142.         var cmdTarget = (IOleCommandTarget)_ViewAdapter;
  143.         hr = cmdTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
  144.       }
  145.       return hr;
  146.     }
  147.   }
  148. }

ToolWindowWithEditor follows the Managed Package Framework pattern by means of setting up the caption and bitmap resources of the pane in the constructor and retrieving the control responsible for the UI in the Content property. However, all the other magic is required to embed the editor window properly into the window pane.

The key is the TextViewHost property — this is what the Content property retrieves. TextViewHost returns an IWpfTextViewHost instance and the concrete implementation is the WpfTextViewHost class of the Microsoft.VisualStudio.Text.Editor.Implementation namespace. Because WpfTextViewHost indirectly derives from System.Windows.UIElement, the window pane handles it is the same way as any other WPF controls, hosts it as the UI of the tool window.

The getter of TextViewHost invokes the InitializeEditor method that does the lion’s share of the work. After InitializeEditor prepares the editor instance, TextViewHost uses the IVsUserData interface to query the IWpfTextViewHost instance representing the editor.

InitializeEditor handles a lot of things related to COM interoperability and transparency between the new editor and old editors used prior to Visual Studio 2010.

  1. private void InitializeEditor()
  2. {
  3.   const string message = "This is an editor window embedded into a tool window.";
  4.  
  5.   var componentModel = (IComponentModel)Microsoft.VisualStudio.Shell.Package.
  6.     GetGlobalService(typeof(SComponentModel));
  7.   _OleServiceProvider = (IOleServiceProvider)GetService(typeof(IOleServiceProvider));
  8.   _BufferFactory = componentModel.GetService<ITextBufferFactoryService>();
  9.  
  10.   _EditorAdapterFactory =
  11.     componentModel.GetService<IVsEditorAdaptersFactoryService>();
  12.   _BufferAdapter =
  13.     _EditorAdapterFactory.CreateVsTextBufferAdapter(_OleServiceProvider,
  14.     _BufferFactory.TextContentType);
  15.   _BufferAdapter.InitializeContent(message, message.Length);
  16.  
  17.   _ViewAdapter = _EditorAdapterFactory.CreateVsTextViewAdapter(_OleServiceProvider);
  18.   ((IVsWindowPane)_ViewAdapter).SetSite(_OleServiceProvider);
  19.  
  20.   var initView = new[] { new INITVIEW() };
  21.   initView[0].fSelectionMargin = 0;
  22.   initView[0].fWidgetMargin = 0;
  23.   initView[0].fVirtualSpace = 0;
  24.   initView[0].fDragDropMove = 1;
  25.  
  26.   _ViewAdapter.Initialize(_BufferAdapter as IVsTextLines, IntPtr.Zero,
  27.     (uint)TextViewInitFlags.VIF_HSCROLL |
  28.     (uint)TextViewInitFlags.VIF_VSCROLL |
  29.     (uint)TextViewInitFlags3.VIF_NO_HWND_SUPPORT, initView);
  30. }

The old editor provided two interfaces, IVsTextBuffer and IVsTextView to access the text buffer behind the editor and to provide services to the view representing the UI of the editor, respectively. For backward compatibility reasons the new editor still provides these interfaces through the adapter pattern.

Line 5 and Line 6 obtain an IComponentModel instance that provides access to the default MEF composition container and the Visual Studio catalogs. This instance is used to obtain an ITextBufferFactoryService (in Line 8) — a factory that creates text buffers — instance and an IVsEditorAdaptersFactoryService — a factory that creates editor adapters — instance (in Line 10 and 11).

The statement starting with Line 12 creates the buffer and its related adapter; Line 13 initializes the content of the buffer with the predefined string message. Line 17 creates the view of the editor and the adapter to this view. In order this adapter could access the core Visual Studio services, it must be sited. The _OleServiceProvider field initialized in Line 7 provides a stub to obtain the service instances from within the adapter. Lines 20 to 29 are responsible to set up the initial properties of the view through an instance of the INITVIEW structure and a set of TextViewInitFlags. As a consequence of this initialization, the view will have a horizontal and a vertical scrollbar; it will be hosted in a WPF control (with no window handle), and it supports drag and move operations (to drag the selected text to another window, or drop text from other windows to the editor surface).

The window pane implemented by ToolWindowWithCodeEditor provides command routing from the IDE to the editor. This behavior is implemented by the following methods:

  1. public class ToolWindowWithEditor : ToolWindowPane, IOleCommandTarget
  2. {
  3.   public override void OnToolWindowCreated() { ... }
  4.   protected override bool PreProcessMessage(ref Message m) { ... }
  5.   int IOleCommandTarget.Exec(...) { ... }
  6.   int IOleCommandTarget.QueryStatus(...) { ... }
  7. }

OnToolWindowCreated sets the key bindings of the window frame so that inherits the bindings from the text editor. PreProcessMessage is called to preprocess keyboard messages before Visual Studio handles them. The implementation in ToolWindowWithCodeEditor allows the view of the editor to catch and process navigation messages. The Exec and QueryStatus methods of IOleCommandTarget let the editor respond to command execution and command status queries sent to the window pane hosting the editor, respectively.

Tool Window Pane Embedding an Editor Indirectly

An editor can be embedded into a tool window indirectly, for example, as the ToolWindowWithCompoundEditor sample implements it. This tool window adds a checkbox to the UI that allows turning the read-only mode of the editor on or off, as Figure 1 shows.

fS01

Figure 1. Tool window with an editor and a check box

The majority of the window pane source code is exactly the same as shown in Listing 2. There are only a few changes as Listing 3 shows.

Listing 3: ToolWindowWithCompoundEditor.cs

  1. [Guid("8BF4030D-C2AC-4F02-BDCA-1B2AC69477AB")]
  2. public class ToolWindowWithCompoundEditor : ToolWindowPane, IOleCommandTarget
  3. {
  4.   // --- Members that are the same as in ToolWindowWithEditor.cs are omitted
  5.   // --- for the sake of simplicity
  6.  
  7.   override public object Content
  8.   {
  9.     get
  10.     {
  11.       if (_Control == null)
  12.       {
  13.         _Control = new CompoundEditorControl(this);
  14.         _Control.EditorPlaceHolder.Content = TextViewHost;
  15.       }
  16.       return _Control;
  17.     }
  18.   }
  19.  
  20.   public void SetReadOnly(bool isReadOnly)
  21.   {
  22.     uint flags;
  23.     _BufferAdapter.GetStateFlags(out flags);
  24.     var newFlags = isReadOnly
  25.       ? flags | (uint)BUFFERSTATEFLAGS.BSF_USER_READONLY
  26.       : flags & ~(uint)BUFFERSTATEFLAGS.BSF_USER_READONLY;
  27.     _BufferAdapter.SetStateFlags(newFlags);
  28.   }
  29. }

Instead of simply retrieving the TextViewHost, the Content property assigns it to a ContentControl instance named EditorPlaceHolder in the CompoundEditorControl WPF user control. When this control is instantiated the window pane instance is passed so it can utilize the SetReadOnly method to turn on or off the read-only mode. The flags indicating and influencing the state of the buffer can be get and set through the buffer adapter with the GetStateFlags and SetStateFlags methods, respectively.

The XAML definition of CompoundEditorControl is really simple as Listing 4 shows.

Listing 4: CompoundEditorControl.xaml

 

  1. <UserControl x:Class="DiveDeeper.CodeEditorInToolWindow.CompoundEditorControl"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  5.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6.              mc:Ignorable="d"
  7.              d:DesignHeight="300" d:DesignWidth="300">
  8.   <Grid>
  9.     <Grid.RowDefinitions>
  10.       <RowDefinition Height="*" />
  11.       <RowDefinition Height="Auto" />
  12.     </Grid.RowDefinitions>
  13.     <ContentControl x:Name="EditorPlaceHolder" />
  14.     <CheckBox x:Name="IsReadOnly" Grid.Row="1" Margin="8" Click="IsReadOnly_Click">
  15.       Use read-only editor
  16.     </CheckBox>
  17.   </Grid>
  18. </UserControl>

The code-behind file responds to the event when the IsReadOnly check box is clicked, and calls the SetReadOnly method of the window pane, as Listing 5 shows.

Listing 1-5: CompoundEditorControl.xaml.cs

  1. using System.Windows;
  2. using System.Windows.Controls;
  3.  
  4. namespace DiveDeeper.CodeEditorInToolWindow
  5. {
  6.   public partial class CompoundEditorControl : UserControl
  7.   {
  8.     private readonly ToolWindowWithCompoundEditor _Pane;
  9.  
  10.     public CompoundEditorControl()
  11.     {
  12.       InitializeComponent();
  13.     }
  14.  
  15.     public CompoundEditorControl(ToolWindowWithCompoundEditor pane)
  16.     {
  17.       InitializeComponent();
  18.       _Pane = pane;
  19.     }
  20.  
  21.     private void IsReadOnly_Click(object sender, RoutedEventArgs e)
  22.     {
  23.       if (IsReadOnly.IsChecked.HasValue)
  24.       {
  25.         _Pane.SetReadOnly(IsReadOnly.IsChecked.Value);
  26.       }
  27.     }
  28.   }
  29. }

I hope, this information helps you to host your the code editor in your tool windows. You can find the souece code of the package attached to this post.


Posted Jul 28 2010, 11:14 AM by inovak

Comments

isenatore wrote re: LearnVSXNow! Part #45: Embedding Visual Studio 2010 Editor into a Tool Window
on Sun, Nov 14 2010 21:55

Hi,

I have a problem try setting Language Service.

Guid xmllanguageserviceid = new Guid("{f6819a78-a205-47b5-be1c-675b3c7f0b8e}");   //XML

      _BufferAdapter.SetLanguageServiceID(ref xmllanguageserviceid);

the editor not colorized the text.

if i use CSharp language service (694DD9B6-B865-4C5B-AD85-86356E9C88DC)  work correctly.

dima wrote re: LearnVSXNow! Part #45: Embedding Visual Studio 2010 Editor into a Tool Window
on Fri, Jun 17 2011 13:49

Hi,

first of all, great series!

I'm searching for a good sample project or article for how to create my own custom designer for my own file (xml - based). E.g. like Entity Framework Designer or UML. Especially just for VS2010 and WPF based.

My idea is, to write an axtension, that shows the MEF composition of a project or other application as a tree and manipulate parts (attributes, add/remove exports/imports, ...)

I found many about custom editors, but nothing about designers and WPF integration.

Can you help me please. Did you know some good books or something, that bring me forward?

Thank you!

Spingbridge topsoil supplier wrote re: LearnVSXNow! Part #45: Embedding Visual Studio 2010 Editor into a Tool Window
on Sat, Feb 16 2013 13:36

I think this is a real great post.Really looking forward to read more. Awesome.

buy clomid no prescription wrote re: LearnVSXNow! Part #45: Embedding Visual Studio 2010 Editor into a Tool Window
on Mon, Feb 25 2013 11:32

DdG6n8 Very good blog article.Thanks Again. Really Great.

buy clomid wrote re: LearnVSXNow! Part #45: Embedding Visual Studio 2010 Editor into a Tool Window
on Fri, Mar 1 2013 5:13

rPiF2r Major thanks for the blog.Thanks Again. Really Cool.