Introduction
Based on an existing add-in, this article shows a simple framework for creating
add-ins that provide multiple refactoring functions. It shows how to interact
with the source code model, automating editing functions in the source, and
generating new classes.
Background
A year ago, I wrote an article that provided a Visual Studio add-in to refactor
a variable into a property. Since then this add-in has been in general use in
my work and I have added several other refactoring functions. To this purpose,
I implemented a little framework for my add-ins to make it easier to add other
refactoring functions.
The add-in now sports the following features:
-
Make Property
- point to a variable and turn it into a property with setter and getter
methods.
-
Make Function
- select a piece of code, turn it into a function, and insert a call to the
function at the original location.
-
Make Collection Class - select a class and generate a typed collection
class (based on
CollectionBase
).
-
Make Mock Object
- select a class (even framework classes) and generate a collection class that
overrides all available functions and properties to allow you to simulate this
class.
-
Move To Super Class
- take an element and move it to the superclass.
-
What is This - a simple demonstration to show how to pick up the edit
point in the code and work out what it is.
The Plug-in Framework
When you create a new project to build a VS plug-in, VS.NET generates a class
called connect
. This class contains all the necessary interfaces
that will be called by VS.NET. There are three kinds of calls:
-
initialization and cleanup calls: these happen at startup, when add-ins are
loaded or when VS.NET shuts down.
-
the Query call: this call occurs often, whenever VS.NET needs to know whether
to enable your command in its popup menus. Your code has to determine the
circumstance and then can respond with a status.
-
the Exec call: your add-in command has been invoked and you need to run it.
You do not need to have one connect
class per command, however if
you make multiple ones available in this class (as is done here), the connect
call has to work out which command is affected in the Query and Exec calls.
The plug-in framework solves this by creating a base class called VS-Plugin
.
Each kind of command listed above is implemented as a sub class of VS-Plugin
.
The connect
class is now only aware of a collection of these
plug-ins, and when a Query or Exec call comes, it simply iterates through its
collection and passes the request on to the appropriate class.
The VSPlugIn
class also contains several utility functions to do
things such as:
-
what is currently highlighted, a class, a function, etc.
-
the ability to link into the necessary command structures into VS.NET.
-
putting up a dialog to ask the user for a target project.
-
displaying messages in the Output window of VS.NET.
-
etc.
Most plug-ins also have a separate class to do the actual work - e.g., the MakeMockObject
plug-in class has a class called MockObjectBuilder
. This is simple
common sense of separating concerns, the plug-in class' purpose is to
understand the request's detail and to deliver the resulting code into the
right place, the MockObjectBuilder
class' purpose is to generate
the Mock class.
How to add another Plug-in
There are only a few steps that need to be done:
1. Create a new VSPlugIn subclass
VSPlugIn
has several abstract properties and methods which you must
implement:
-
cmdName
: the short command, e.g.: "MakeProperty
".
-
qualifiedName
: the fully qualified command, e.g.: "RefactorAddIn.Connect.MakeProperty
".
-
IconId
: the ID of the icon to show in the menu, e.g.: 54.
-
position
: where in the menu to position the command (1=
first).
-
shortDescription
: the text to show in the menu.
-
long
Description
: the text for the tool help.
-
doQueryStatus()
: implement the logic to determine if the
method currently makes sense and should be active or be inactive (or
invisible).
-
doExec()
: implement the actual action of the command.
2. Extend the Connect class
The connect
class has a method called OnStartupComplete
.
You must add your plug-in to the other plug-in's collection and call the
methods to have it show up in the right tool bars.
CommandBar popup = InsertSubMenu("Code Window", "Refactoring");
CommandBar classViewCommandBar = applicationObject.CommandBars["Class View Item"];
VSPlugIn plugin = new PropertyMaker(applicationObject, addInInstance);
Plugins.Add(plugin);
plugin.InsertCommand(classViewCommandBar,false);
plugin.InsertCommand(popup, false);
The first two lines are already given, they provide two commandbars, one a
separate popup menu that is part of the Code Window menu (the one that pops up
when you right click in the code window), and the other is the menu that pops
up when you right click in the Class Explorer.
You need to add code in similar to the last four lines shown above:
-
create an instance of your plug-in.
-
add the plug-in into the plug-ins collection.
-
if you want the plug-in be available for each commandbar, call the plug-in's
InsertCommand
method. The boolean should only be true for special circumstances when
developing - see below in Points of Interest.
The above example makes the property maker available on both menus. You can
target other menubars or leave them out altogether. In this instance, you can
still bind to your command via the keyboard mapping available in the
Tools->Options dialog (select Folder Environment and item Keyboard) - or you
can call your command from macros.
Points of Interest
Programming Visual Studio.NET is not for the faint hearted. Good documentation
is not easy to come by. However, I found the book "Inside Microsoft Visual
Studio. NET" (Brian Johnson, Craig Skibo, Marc Young, Microsoft Press,
ISBN 0-7356-1874-7) to be an excellent help. Still you need to read it several
times.
Visual Studio .NET is written in COM, which makes life less pleasant. Here are a
few pointers:
-
Collections start with index 1.
-
If you ask a collection for something it doesn't have, you won't get a null but
an exception. So if you want to check if something is in the collection, you
have to write something like this:
protected bool HasCommand(string commName,out Command command)
{
command = null;
try
{
Commands commands = _applicationObject.Commands;
command = commands.Item(commName,-1);
return true;
}
catch (System.Exception){}
return false;
}
-
Not all commands will work under all circumstances. For instance, the command
InsertClass
, which inserts a new class into the code model, only
works with C++, not for C# or VB.NET. However, InsertMethod
does.
-
Not all windows give their content out. While the documentation makes a lot of
effort telling us how to access the tree controls in some of their windows, you
need to almost read between the lines to work out that this is not available
for some windows, e.g.: the Class View window.
-
It is highly recommended to peruse and understand other examples of plug-ins.
Graceful acknowledgement to Erick
Sgarbi
and his article on a VS Plug-in where I borrowed several techniques including
the one above.
-
If you are interested in CodeDom (defining code constructs and then generating
source code), look at my
CollectionBuilder
class for an example.
-
When you publish your add-in to Visual Studio, you create an object called a
command. This command is the structure by which other parties in VS.NET can
recognize your plug-in and invoke it. Be aware that on shutdown, VS.NET saves
this command and reloads it on restart. Hence you do not have to recreate a
command every time your add-in is loaded. In fact, you shouldn't, because this
gums up the works. The
VSPlugIn::InsertCommand
method has a
boolean parameter called deleteIfExists
; if true, it will
delete existing commands before creating a new one. This parameter should only
be true during developing and debugging your add-in. In default mode (=false),
it will try to find and use the existing command and create one only if it
doesn't exist yet.
-
It is also possible to create during debugging multiple instances of controls
(the things that make a command visible in a pop up menu). The
InsertCommand
method tries to flush these out by calling flushControls()
. If you
don't do this, all kinds of weird things can happen, sending you around the
bend.
History
- Version 1.1 - Based upon feedback I have added several changes
- Make Property is now configurable in the way the private variable is named.
- The setup adds a registry entry for
HKEY_CURRENTUSER\ Software\ MethodsConsulting\ RefactorAddIn\ PrivatevarCase.
Possible values are:
- underscore - prepends an underscore to the variable
- m_underscore - prepends an 'm_' to the variable name
- camel - use camel casing
- Move To SuperClass now works with Variables, Properties and functions
- Plus various fixes to smaller annoyances.
Enjoy