Tuesday, April 12, 2011

Writing into Visual Studio Output Window. Part 1


Lets assume that we have some Visual Studio 2010 Isolated Shell application (I suppose everything mentioned here should be valid for ordinary vspackages, but didn’t test, so cannot be sure). It may perform some operations (actually it is usually written to perform some operation, isn’t it?) and our task is to put log of these operations somewhere. Visual Studio has such nice feature like output window. And it is perfect candidate for such log, so post creates simple class to make it easy to write anything into log.

Prerequisites: Visual Studio 2010 with created default Shell Isolated application (containing AboutBoxPackage).
As you could possibly know, output window in Visual Studio consists of several parts. The first part is window object itself.
image
IVsOutputWindow interface is responsible for Output window operations in Visual Studio (as you could see from pervious post, don’t forget to take a look at IVsOutputWindow2, IVsOutputWindow3 if you are missing something in the IVsOutputWindow). By the way those interfaces are located into Microsoft.VisualStudio.Shell.Interop namespace.
The there are elements called output window pane, they are responsible for actual data, have name, etc. You can see name of the currently displayed pane beside Show output from: label.
image
These panes are handled by similarly named interface (even interfaces): IVsOutputWindowPane (and IVsOutputWindowPane2, maybe there will be some others in the future).

OutputWindowWriter

First of all lets create some simple object that will handle our text writing operations. Lets call it OutputWindowWriter. As you already know, .Net Framework has several classes which are responsible for text output. And they are derived from TextWriter. And we will follow this thing too:
namespace OutputWindowTest.AboutBoxPackage { /// <summary> /// This class is repsonsible for writing messages into Visual Studio Output Window. /// </summary> class OutputWindowWriter : TextWriter { ... } }

Lets create output window object, since we don’t know whether it will be used or not, it is a good idea to put it into property which is instantiated on first access (by the way you will need your package object to get DTE instance, so please modify constructor in appropriate way, or provide it in some other way, its up to you):

/// <summary> /// Output window pane. /// </summary> private IVsOutputWindowPane _outputPane; /// <summary> /// Gets output window object. /// </summary> private IVsOutputWindow OutputWindow { get { if( _outputWindow == null ) { DTE dte = ( DTE )( ( _package as IServiceProvider ).GetService( typeof( DTE ) ) ); IServiceProvider serviceProvider = new ServiceProvider( dte as Microsoft.VisualStudio.OLE.Interop.IServiceProvider ); _outputWindow = serviceProvider.GetService( typeof( SVsOutputWindow ) ) as IVsOutputWindow; } return _outputWindow; } }

Great! Now we have output window. But wait, that interface doesn’t know how to write anything. This interface exposes just pane operations: CreatePane, DeletePane, GetPane. As you can understand from their names they are supposed to create, delete and get pane. Just one remark – they perform those operations based on the pane Guid. So lets create some. Go to Tools \ Create GUID menu item:

image

And store it in some variable, plus we will need pane’s name too:

/// <summary> /// Name of the custom output pane. /// </summary> private const string PaneName = "Custom Log Pane"; /// <summary> /// Guid for the custom output pane. /// </summary> private static readonly Guid PaneGuid = new Guid( "AB9F45E4-2001-4197-BAF5-4B165222AF29" );

Now we are ready to create our new pane, similar to the output window we will initialize it on demand (I have skipped error handling here, just to make pane operations easier to see, but I suggest to check result returned by GetPane and CreatePane method to ensure that they were executed successfully):

/// <summary> /// Output window pane. /// </summary> private IVsOutputWindowPane _outputPane; /// <summary> /// Returns output pane. /// </summary> private IVsOutputWindowPane OutputPane { get { if( _outputPane == null ) { Guid generalPaneGuid = PaneGuid; IVsOutputWindowPane pane; OutputWindow.GetPane( ref generalPaneGuid, out pane ); if( pane == null ) { OutputWindow.CreatePane( ref generalPaneGuid, PaneName, 1, 1 ); OutputWindow.GetPane( ref generalPaneGuid, out pane ); } _outputPane = pane; } return _outputPane; } }

First ‘1’ in CreatePane method call stands for create visible flag, the second one to clear pane text with solution closure, if you don’t need any of these options you should change them. I needed them, so my sample contains these values.

Now we are ready to actually write something. If you will take a look at TextWriter using some tool like Reflector (or read about it somewhere). You will probably know that at the end all write operations with TextWriter comes to

public virtual void Write(char value) { }

And generally we will need to override just this method to make it work, but I suggest to override also Write method which has one string arguments, this should increase performance of some operations (in all cases we will have to deal with string writing method of the output window). These methods are transferred into simple OutputString method call like this:

/// <summary> /// Writes a message into our output pane. /// </summary> /// <param name="message">Message to write.</param> public override void Write( string message ) { OutputPane.OutputString( message ); }

Additionally you may need Clear operation which simply redirects to Clear method of the output pane.

Generally, we are ready to write something into OutputWindow. Since I’m using default package provided by Visual Studio, I won’t modify anything and just put some data into its navigation event inside about box, like this:

private void hyperlink_RequestNavigate( object sender, System.Windows.Navigation.RequestNavigateEventArgs e ) { OutputWindowWriter writer = new OutputWindowWriter( Package ); writer.WriteLine( "Request navigate method started." ); if( e.Uri != null && string.IsNullOrEmpty( e.Uri.OriginalString ) == false ) { writer.WriteLine( "Uri wasn't empty and equals to {0}.", e.Uri.OriginalString ); string uri = e.Uri.AbsoluteUri; Process.Start( new ProcessStartInfo( uri ) ); e.Handled = true; } writer.WriteLine( "Request navigate method ended." ); }

Now run it, go to Help \ About menu item and click on More Info link. After that return to application, open its output window if it is not visible and you will see something like this:

image

Pane was created, name present, text was written. Great!
That's all for today, I'll post some enhancements to it next part later. To be continued…

P.S. Complete code of the OutputWindowWriter is here:

using System; using System.IO; using System.Text; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio; using EnvDTE80; using EnvDTE; namespace OutputWindowTest.AboutBoxPackage { /// <summary> /// This class is repsonsible for writing messages into Visual Studio Output Window. /// </summary> class OutputWindowWriter : TextWriter {   #region Constants /// <summary>   /// Name of the custom output pane. /// </summary>   private const string PaneName = "Custom Log Pane";   /// <summary>   /// Guid for the custom output pane.   /// </summary> private static readonly Guid PaneGuid = new Guid( "AB9F45E4-2001-4197-BAF5-4B165222AF29" );   #endregion   #region Members   /// <summary>   /// Output window.   /// </summary> private IVsOutputWindow _outputWindow;   /// <summary>   /// Output window pane.   /// </summary> private IVsOutputWindowPane _outputPane;   /// <summary>   /// Parent package.   /// </summary> private AboutBoxPackage _package;   #endregion   #region Properties   /// <summary>   /// Initializes new instance of the writer.   /// </summary>   /// <param name="package">Parent package.</param> public OutputWindowWriter( AboutBoxPackage package )   {   if( package == null )   throw new ArgumentNullException( "package" );   _package = package;   }   /// <summary>   /// Gets output window object.   /// </summary> private IVsOutputWindow OutputWindow   {   get   {   if( _outputWindow == null )       {       DTE dte = ( DTE )( ( _package as IServiceProvider ).GetService( typeof( DTE ) ) ); IServiceProvider serviceProvider = new ServiceProvider( dte as Microsoft.VisualStudio.OLE.Interop.IServiceProvider );          _outputWindow = serviceProvider.GetService( typeof( SVsOutputWindow ) ) as IVsOutputWindow;       }       return _outputWindow;      }   }   /// <summary>   /// Returns output pane.   /// </summary> private IVsOutputWindowPane OutputPane   {   get   {   if( _outputPane == null )       {       Guid generalPaneGuid = PaneGuid;       IVsOutputWindowPane pane;       OutputWindow.GetPane( ref generalPaneGuid, out pane );       if( pane == null )       {           OutputWindow.CreatePane( ref generalPaneGuid, PaneName, 1, 1 );           OutputWindow.GetPane( ref generalPaneGuid, out pane );       }       _outputPane = pane;       }       return _outputPane;      }   }   #endregion   #region Methods    /// <summary>   /// Writes a message into our output pane.   /// </summary>   /// <param name="message">Message to write.</param> public override void Write( string message )   {   OutputPane.OutputString( message );   }   /// <summary>   /// Writes a character into our output pane.   /// </summary>   /// <param name="ch">Character to write.</param> public override void Write( char ch )   {   OutputPane.OutputString( ch.ToString() );   }   /// <summary>   /// Clears output pane.   /// </summary> public void Clear()   {   OutputPane.Clear();   }   /// <summary>   ///   /// </summary> public override Encoding Encoding   {     get { throw new NotImplementedException(); }   }   #endregion  } }

2 comments:

  1. How to navigate to particular line the code using output window.?

    ReplyDelete
  2. I haven't done this before, so I'm not sure whether it works. But I would start from something like this:

    Get output pane somehow, for example, like this

    DTE2 dte = ( _package as IServiceProvider ).GetService( typeof( DTE ) ) as DTE2;
    OutputWindow outputWindow = dte.ToolWindows.OutputWindow;
    OutputWindowPane pane = outputWindow.OutputWindowPanes.Item( PaneName );

    It has TextDocument which you might try to use, maybe something like this would work (maybe not, or maybe you will find some other method)
    TextPoint point = pane.TextDocument.StartPoint;
    pane.TextDocument.Selection.GotoLine( lineNumber );

    ReplyDelete