The small Calculate tool window sample we created in Part #6 and #7 allows me to show that the current VS IDE object model has many opportunities to make the object model behind packages better. In this part we are not going to develop any new functionality but change our code to gain reusable types and methods.
The object model behind VSX is very rich: we have a few hundred types with a few thousands of methods altogether. If we start developing VS add-ins and/or packages it is not enough to remember types and methods but also we must be able to find appropriate GUIDs and other constants.
I think the most difficult thing for developers in VSX is they must have a lot of lexical knowledge and cope with mixed .NET and COM patterns. A good help could be for most programmers if the programming model (object model) behind VSX was simpler.
Microsoft developed a few managed layer above the interoperability assemblies (one of them is called MFP that stands for Managed Package Framework). I think the types and patters in MPF are great! My concern is that MPF focuses only on a few aspects of VSX development.
In this article I would like to show you a few ways to make the object model behind VSM easier to learn and use. My aim is to encourage you to create your own toolset (object model over VSX) when you feel you need it.
In this article a start to create a library called VsxTools. This time I use this library only for demonstration purposes but as we learn VSX together I intend to turn this library into a real tool. In this article I illustrate my imaginations about how managed VSX types could ease our life with two examples:
Improving Activity Log handling
Simplifying output window and pane handling
Source code on CodePlex
By the time you read this article, I put all the sample codes and articles on CodePlex (http://www.codeplex.com/LearnVSXNow). When you download the latest source code, you will find a PackageStartupSamples directory with a PackageStartupSamples.sln files. Load this file into VS 2008 to see all the samples in the articles. I plan to update these samples along with the new releases of VS 2008 SDK (and of course in case you find a bug and contact me).
Creating the VsxTools library
If we really want to create reusable code the best solution is to encapsulate it into a separate class library. This is exactly what we are going to do. Let’s create a C# class library called VsxTools. Please, add this class library to the solution holding the StartupToolsetRefactored package. Since we add VSX code to VsxTools, we need the VS SDK interop and MPF assemblies, and so we have to add the following references to our class library:
Microsoft.VisualStudio.OLE.Interop
Microsoft.VisualStudio.Shell.9.0
Microsoft.VisualStudio.Interop
Microsoft.VisualStudio.Interop.8.0
Microsoft.VisualStudio.Interop.9.0
Our library is ready to expand with our own managed VSX types.
Improving Activity Log handling
When writing into the activity log, we need to write down about half dozen lines of code. The following extract illustrates this:
private void LogCalculation(string firstArg, string secondArg, string operation,
string result)
{
string message = String.Format("Calculation executed: {0} {1} {2} = {3}",
firstArg, operation, secondArg, result);
IVsActivityLog log =
Package.GetGlobalService(typeof(SVsActivityLog)) as IVsActivityLog;
if (log == null) return;
log.LogEntry(
(result == "#Error")
?(UInt32) __ACTIVITYLOG_ENTRYTYPE.ALE_ERROR
: (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION,
"Calculation", message);
}
This method has more noise than I would like to see:
I have to know the names IVsActivityLog and SVsActivityLog in order to obtain an object serving me.
I have to check if I really got a service object instance before using it.
I must know “magic” constant names like __ACTIVITYLOG_ENTRYTYPE.ALE_ERROR and __ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION. If I see the names I know what semantics they have, but they are not intuitive. When I know the desired semantics, the related names do not come into my mind easily. I even have to cast them to System.UInt32.
In this case we use the LogEntry method because we log only the type, source and message. If we had to log other fields, we would have to find the method with the correct name.
There must be a way to remove that noise. Would not it be easier to use something like this below?
string message = String.Format("Calculation executed: {0} {1} {2} = {3}",
firstArg, operation, secondArg, result);
ActivityLog.Write( result == "#Error" ? ActivityLogType.Error : ActivityLogType.Information,
"Calculation", message);
In this solution we reduce the syntax noise:
We have to remember one intuitive name to reach the service: ActivityLog.
We have intuitive enumerations for the log entry type.
We use one overloaded method for all parameter combinations of logging: Write.
We have no casts, no null reference checks.
This very simple pattern combined with IntelliSense makes us more efficient when typing code.
Coding the ActivityLog pattern
We can develop the pattern above. The frame of the pattern is based on three types:
// --- Represents entry types instead of __ACTIVITYLOG_ENTRYTYPE constants
public enum ActivityLogType
{
Information,
Warning,
Error
}
// --- Represents an entity holding all log entry properties
public sealed class ActivityLogEntry
{
...
public ActivityLogType Type { get; set; }
public string Source { get; set; }
public string Message { get; set; }
public Guid? Guid { get; set; }
public int? Hr { get; set; }
public string Path { get; set; }
...
}
// --- Provides log services through static Write methods
public static class ActivityLog
{
public static void Write(ActivityLogEntry entry);
public static void Write(string source, string message);
...
public static void Write(string source, string message, Guid guid, int hr);
...
public static void Write(ActivityLogType type, string source, string message);
...
}
ActivityLogType’s role is quite clear. The static ActivityLog class provides services through its Write method that accepts a variety of arguments due to method overloading. There might be cases when we do not exactly what properties of a log event are to be written, since it comes to light only at runtime. The Write method having only one ActivityLogEntry parameter would solve this issue. Inside that method we can make a decision which IVsActivityLog method to call. For example, if only the Hr and Path properties are used, we can call the LogEntryHrPath method.
Implementation details
I have added the ActivityLog.cs file to the VsxTools project and implemented the pattern above. In ActivityLogEntry I have implemented a few constructor, each setting a different set of properties. The main “logic” is within the ActivityLog static class. I have created a few private properties and methods:
public static class ActivityLog
{
...
private static UInt32 MapLogTypeToAle(ActivityLogType logType)
{
switch (logType)
{
case ActivityLogType.Information:
return (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION;
case ActivityLogType.Warning:
return (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_WARNING;
default:
return (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR;
}
}
private static IVsActivityLog Log
{
get
{
return Package.GetGlobalService(typeof (SVsActivityLog)) as IVsActivityLog;
}
}
private static void LogEntry(ActivityLogType type, string source,
string message)
{
IVsActivityLog log = Log;
if (log != null)
{
log.LogEntry(MapLogTypeToAle(type), source, message);
}
}
...
}
These methods are quite simple. Following the patter of LogEntry I have created all the other IVsActivityLog methods including LogEntryGuid and the others. The public Write methods use each other and these private methods:
public static void Write(string source, string message)
{
Write(ActivityLogType.Information, source, message);
}
public static void Write(ActivityLogType type, string source, string message)
{
LogEntry(type, source, message);
}
That’s all. Implementing the pattern above we have a much simple and easy-to-remember model of activity logging.
Changing the old code to use ActivityLog
We are ready, and you can change the LogCalculation method in the CalculationControl.cs file:
private void LogCalculation(string firstArg, string secondArg, string operation,
string result)
{
string message = String.Format("Calculation executed: {0} {1} {2} = {3}",
firstArg, operation, secondArg, result);
ActivityLog.Write( result == "#Error" ? ActivityLogType.Error : ActivityLogType.Information, "Calculation", message);
}
Now, you can compile and run the StartupToolsetRefactored sample and check if our activity log has the calculation messages. Just to remember you: in Part #7 you will find details about how to display the activity log. Please, do not forget to put the /log switch to the command line arguments on the Debug tab of the project properties to allow writes into the activity log.
Looking behind the output window handling
As I promised in the introduction I will show you a way of simplifying output window handling. In Part #7 we have already written output to the VS output window using the IVsOutputWindow and IVsOutputWindowPane service interfaces:
private void LogCalculationToOutput(string firstArg, string secondArg,
string operation, string result)
{
string message = String.Format("Calculation executed: {0} {1} {2} = {3}
",
firstArg, operation, secondArg, result);
IVsOutputWindow outWindow =
Package.GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow;
Guid generalWindowGuid = VSConstants.GUID_OutWindowGeneralPane;
IVsOutputWindowPane windowPane;
outWindow.GetPane(ref generalWindowGuid, out windowPane);
windowPane.OutputString(message);
}
The highlighted part has the same “noisy nature” as the Activity Log handling code we had before refactoring. Before we start reducing the noise, let’s have a look behind the VS output window and its related services.
Visual Studio has only one output window, but this can host many panes that are individual output areas. Visual Studio itself defines a few output window panes and packages can define their own panes as well. A package can write to any known output window panes (either defined by VS IDE or a third party VSPackage). The following picture shows the General pane defined by VS IDE and a custom My Debug pane.
The VS Shell supports output windows with two simple services:
SVsOutputWindow service is responsible to manage (obtain, create and delete) the output window panes and it does not output any messages. This service retrieves an IVsOutputWindow or IVsOutputWindow2 instance (this latter is an extension to the previous one). This service interfaces are responsible to put the output into the corresponding window pane.
SVsGeneralOutputWindowPane service simply retrieves an IVsOutputWindowPane instance for the General output window pane.
The responsibilities are divided among the interfaces as follows:
| Service interface |
Responsibility |
| IVsOutputWindow |
This service interface has only three methods that manage the instantiation and access to the output window panes. The names of methods really tell what they do: CreatePane, DeletePane, GetPane
|
| IVsOutputWindow2 |
Extends the IVsOutputWindow interface with a new method to obtain the ID of the active output window pane: GetActivePaneGUID
|
| IVSOutputWindowPane |
This interface defines the operations that manage the visibility and content of the physical window pane behind the service instance.
The pane can be brought into the foreground with the Activate method or be hidden with the Hide method. Each window pane has a name that is handled by the GetName and SetName methods. The content is managed with the Clear, OutputString and OutputStringThreadSafe methods.
The output sent to the window pane can also be put to a task list using the OutputTaskItemString, OutputTaskItemStringEx and FlushToTaskList methods.
|
| IVsOutputWindowPane2 |
Extends the IVsOutputWindowPane interface to add output messages and tie them to a corresponding message on the error list (OutputTaskItemStringEx2 method).
|
To summarize the table above: we use IVsOutputWindow to manage the panes and IVsOutputWindowPane to manage the output of a concrete window pane.
Window panes are identified by GUIDs. The Visual Studio IDE uses three common output window panes that have predefined GUIDs in the Microsoft.VisualStudio.VSConstants type:
| Window Pane |
GUID (VSContants member) |
| General |
GUID_OutWindowGeneralPane |
| Build |
GUID_BuildOutputWindowPane |
| Debug |
GUID_OutWindowDebugPane |
When a package creates a window pane, it has to use its own GUID. If the package publishes its GUID, we can obtain a reference to the window pane and use it just like as VS IDE built-in panes. When the package does not publish it, we can write utilities to obtain the GUID using the IVSOutputWindow2 service interface.
Window pane management methods
The IVsOutputWindow interface retrieved through the SVsOutputWindow service has three pane management methods:
public interface IVsOutputWindow
{
int GetPane(ref Guid rguidPane, out IVsOutputWindowPane ppPane);
int CreatePane(ref Guid rguidPane, string pszPaneName, int fInitVisible,
int fClearWithSolution);
int DeletePane(ref Guid rguidPane);
}
All methods’ first parameter is the GUID of the window pane. Looking at their signatures — I guess — it is obvious what they intend to do. When calling the CreatePane method you can pass three additional parameters:
pszPaneName defines the initial name of the pane (later it can be changed).
fInitVisible allows you to set the initial visibility of the pane. If it set to true (non-zero value) it immediately gets visible as soon as it is created. Actually it means that the pane is visible in the output window, but the output window itself can be hidden! Use the View|Output function to activate the VS Output Window.
fClearWithSolution flag can be set to true in order to clear the window pane content automatically when the currently loaded solution is closed.
You may think that CreatePane and DeletePane sign an error in case of the VS Shell’s built-in output panes. No. These methods allow deleting and recreating those panes, so you must use them in mind of this fact.
The most often used method is GetPane which retrieves an IVsOutputWindowPane instance that can be used to write output.
Sending output to the pane
The IVsOutputWindowPane service interface provides operation to write output to the pane. This interface allows you to put text into the output pane and allows you to send items to the task list at the same method call. In this article I treat only the output methods that write directly to the pane (handling task lists is the topic for a later article). You can send output to the pane in a thread-safe or “thread-unsafe” method choosing the appropriate methods of OutputString and OutputStringThreadSafe. It always needs consideration when to use thread-safe writing and when not. I am sure you can clearly decide knowing the circumstances. If the situation is not clear, use OutputStringThreadSafe.
Simplifying output window and pane handling
As you could see, managing output panes and writing messages to them requires writing a few lines of code. As I told you before this “few lines” disturbs me, since I consider it “noisy”. In this part I show you a solution that removes this noise. I do not think it is “the perfect” one, but I sure this is one. If you have comments and ideas about the design and implementation I am going to show you, please share it with me and the community.
How our solution will look like?
As you are a developer, code lines tell you much more than any other description. The essence of my solution can be summarized in a few lines of code that is an extract from the CalculationControl.cs file:
public partial class CalculationControl : UserControl
{
...
private void LogCalculationToOutput(string firstArg, string secondArg,
string operation, string result)
{
string message = String.Format("Calculation executed: {0} {1} {2} = {3}",
firstArg, operation, secondArg, result);
OutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane));
pane.WriteLine(message);
}
...
[PaneName("My Debug")]
[InitiallyVisible(true)]
[ThreadSafe(true)]
private sealed class MyDebugPane: OutputPaneDefinition
{}
...
}
The highlighted code creates a new output window pane called “My Debug” and puts messages there in a thread-safe fashion. The noise is removed by with the GetPane method of the OutputWindow class that creates the window pane if necessary. The window pane itself is declared as a simple type definition decorated with attributes. All the work making the wheels moving is done behind the scenes.
If you would like to write to the General pane, the code above even would shorter:
OutputWindow.General.WriteLine(message);
Basic pattern of the solution
The solution I showed you is based on three types sharing the responsibilities:
| Type |
Responsibility |
| OutputPaneDefinition |
This class is used to derive output window pane definition types (OWPD). An OWPD type is used only as a definition; it can be decorated by attributes that define the characteristics of the pane. OutputWindow and OutputWindowPane use an instance of this type to obtain the attribute values through properties.
|
| OutputWindow |
This static class undertakes the management of output window panes just like as the IVsOutputWindow service interface. The class also provides static properties to direct access to the standard VS Shell output window panes. The class provides an exception handling mechanism that allows redirecting the output messages to the General or Debug pane or even to a virtual pane (Silent pane).
|
| OutputWindowPane |
This class is responsible for putting the message to the physical pane behind the class instance. It provides the same operations as the IVsOutputWindowPane service interface (it does not support task list handling operations). It offers Write and WriteLine methods with the similar signatures to the System.Console class.
However it is not fully correct, you can handle this type as a wrapper class around the functionality of IVsOutputWindowPane.
|
Thread safety is handled a bit differently from the original concept. When defining a window pane with OutputPaneDefinition we can declare the pane as ThreadSafe by default. When writing to the pane the output is handled in a thread-safe manner.
I put the implementation into the VsxTools project we created at the beginning of the article.
Defining a pane type
If we want to use the VS Shell’s standard panes, we simply use the corresponding General, Debug or Build static properties of the OutputWindow class to obtain a reference to the window pane.
If we create a VSPackage we may need our own output window pane or even panes; and send the output there. In the “traditional” way we represent the window pane with a GUID. In my solution I represent the pane with a type deriving from OutputWindowDefinition. This type recognizes a set of attributes that describe the characteristics of our pane. The default constructor of OutputWindowDefinition sets the instance properties according to the decorating attributes. Here is the blueprint of this type:
public abstract class OutputPaneDefinition
{
protected OutputPaneDefinition();
public virtual Guid GUID { get; }
public string Name { get; }
public bool InitiallyVisible { get; }
public bool ClearWithSolution { get; }
public bool ThreadSafe { get; }
public bool IsSilent { get; internal set; }
}
We can define a window pane to be a silent pane that means there is no physical output pane, any messages sent to this pain is taken off silently. The Guid property can be overridden in order to allow defining a class for an existing window pane (for example a pane defined by a third-party package, or for our own package defined by an external assembly. To illustrate the usage of these properties let me show you an extract from the OutputWindow class how the standard Debug pane and the virtual Silent pane is declared:
public static class OutputWindow
{
...
private sealed class DebugPane : OutputPaneDefinition
{
public override Guid GUID
{
get { return VSConstants.GUID_OutWindowDebugPane; }
}
}
...
private sealed class SilentPane : OutputPaneDefinition
{
public SilentPane()
{
IsSilent = true;
}
}
...
}
OutputWindowDefinition recognizes the following attributes:
public sealed class PaneNameAttribute: StringAttribute {...}
public sealed class InitiallyVisibleAttribute: BoolAttribute {...}
public sealed class ClearWithSolutionAttribute: BoolAttribute {...}
public sealed class ThreadSafeAttribute: BoolAttribute {...}
To define an output pane for your own use create a new type like in the following sample:
[Guid("6D71C5F7-200C-4322-A264-65C78CF511AA")]
[PaneName("My Own Pane")]
[InitiallyVisible(false)]
[ClearWithSolution(true)]
[ThreadSafe(true)]
private sealed class MyOwnPane: OutputPaneDefinition
{}
For the implementation details, please look for the OutputWindowDefinition.cs file.
Managing panes with the OutputWindow class
I created the OutputWindow class around the services IVsOutputWindow provides. Besides these operations I added some minor functionality. I declared an OutputPaneHandling property with an enumeration type having the same name to allow handling problems when a physical output pane cannot be obtained to write the output. The enumeration has the following values:
| Value |
Semantics |
| Silent |
No exception is generated, the output is simply not sent to any window pane.
|
| ThrowException |
A WindowPaneNotFoundException is raised.
|
| RedirectToGeneral |
The output is redirected to the General pane.
|
| RedirectToDebug |
The output is redirected to the Debug pane.
|
The blueprint of this class is the following:
public static class OutputWindow
{
public static OutputPaneHandling OutputPaneHandling { get; set; }
public static OutputWindowPane General { get; }
public static OutputWindowPane Build { get; }
public static OutputWindowPane Debug { get; }
public static OutputWindowPane Silent { get; }
public static OutputWindowPane CreatePane(Type type);
public static OutputWindowPane GetPane(Type type);
public static bool DeletePane(Type type);
}
As you can see the CreatePane, GetPane and DeletePane methods accept Type parameters and that type must reflect to one of the WindowPaneDefinition derived types. The types representing the built-in window panes are private nested types of OutputWindow so you cannot pass their typeof() as argument of these methods and so you cannot incidentally create or delete them. I changed the behavior of the original CreatePane method so that it creates only a non-existing pane and it retrieves the pane even it has just been created or existed. The GetPane method tries to create the pane if that has not been yet created.
If you look into the OutputWindow.cs file you can see the interesting implementation details.
Writing messages to the output pane
As I mentioned before the OutputWindowPane class is actually a wrapper around a physical pane represented by an IVsOutputWindowPane instance. When designing and implementing this wrapper class I have made small “twists”.
IVsOutputWindowPane has separate methods to write in a thread-safe and unsafe fashion: OutputStringThreadSafe and OutputString. I wanted to hide from the developer which method to call when writing any output. I added a ThreadSafe property to the type that can be set with a Boolean value and this property determines which output method to call. If you remember, the WindowPaneDefinition type recognizes the ThreadSafeAttribute. When a pane instance is created, its ThreadSafe property is set to the initial value determined by the ThreadSafeAttribute of the related window pane definition type.
IVsOutputWindowPane has a GetName and SetName method that is wrapped into a Name property.
To see what members OutputWindowPane has, here is its definition:
public sealed class OutputWindowPane
{
internal OutputWindowPane(OutputPaneDefinition paneDef,
IVsOutputWindowPane pane);
public bool ThreadSafe { get; set; }
public string Name { get; set; }
public bool IsVirtual { get; }
public void Activate();
public void Hide();
public void Clear();
public void Write(string output);
public void Write(string format, params object[] parameters);
public void Write(IFormatProvider provider, string format,
params object[] parameters);
public void WriteLine(string output);
public void WriteLine(string format, params object[] parameters);
public void WriteLine(IFormatProvider provider, string format,
params object[] parameters)
}
The constructor can be only internally called in order to make OutputWindow a real factory class for OutputWindowPane. In construction type an instance of the pane definition class is also passed to the constructor along with the physical pane instance. The IsVirtual property allows asking if the pane is a real physical window pane or only a virtual (silent) one.
The class provides a set of Write and WriteLine methods instead of the original OutputString and OutputStringThreadSafe methods to mimic System.Console-like behavior.
The implementation of the above methods is quite simple; you can look it in the OutputWindowPane.cs file.
Try the solution
At this point you can build and run the StartupToolsetRefactored package and see that pressing the Calculate button produces the output in a new output pane named “My Debug”. If you have time, please try some small changes in the code (within the CalculationControl class’s LogCalculationToOutput method) and look for their effect:
// --- Original code lines:
OutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane));
pane.WriteLine(message);
// --- Change 1: Writing to two panes
OutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane));
pane.WriteLine(message);
OutputWindow.General.WriteLine(message);
// --- Change 2: Changing the pane name
OutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane));
pane.Name = "My Debug (modified)";
pane.WriteLine(message);
// --- Change 3: Reflecting to an invalid pane
OutputWindowPane pane = OutputWindow.GetPane(typeof(int));
pane.WriteLine(message);
// --- Change 4: Throwing an exception (VS 2008 will stop!)
OutputWindow.OutputPaneHandling = OutputPaneHandling.ThrowException;
OutputWindowPane pane = OutputWindow.GetPane(typeof(int));
pane.WriteLine(message);
// --- Change 5: Silent exception (No output will be shown)
OutputWindow.OutputPaneHandling = OutputPaneHandling.Silent;
OutputWindowPane pane = OutputWindow.GetPane(typeof(int));
pane.WriteLine(message);
// --- Change 6: Throwing and handling exception
OutputWindow.OutputPaneHandling = OutputPaneHandling.ThrowException;
try
{
OutputWindowPane pane = OutputWindow.GetPane(typeof (int));
pane.WriteLine(message);
}
catch (WindowPaneNotFoundException ex)
{
OutputWindow.General.WriteLine(ex.Message);
}
Where we are?
In this article we changed our StartupToolsetRefactored VSPackage to provide helper classes as part of the VsxTools library. This helper classes are managed classes that reduce the “noise” of using the VS Shell interop classes provided by VS 2008 SDK. We created such kind of reusable code for Activity Log handling and for output window pane management.
Now, all source code (including source for previous samples) and articles can be accessed on CodePlex (http://www.codeplex.com/LearnVSXNow).
I hope you found the helper classes I treated in this article useful. However, I do not wanted to dedicate this articles to the concrete solution for the “noise reduction”, but rather to show you that it is worth to create managed classes around the VS Shell interop types. Microsoft started this work with MPF but there are many possibilities to make the VSX development experience more enjoyable!
When started this series I did not have plans to create a toolset of my own for making VSX programming a joy, but now I am definitely considering this with the support of the VSX community...
Posted
Feb 04 2008, 06:44 PM
by
inovak