Contents
The title may sound a bit outdated (because Visual Studio 2005 is just released), but at my work we have got some big projects written with Visual Studio 6.0. So there is still a need for me to use it. On the other hand, I'm fascinated by the .NET Framework and its abilities. After writing a few addins in plain C++ dealing with the ugly COM stuff, I really wanted to switch over to .NET. So, I wrote a wrapper addin that wraps the calls from the Visual Studio IDE to a COM registered .NET library. Later on I reworked on this wrapper addin to use mixed mode code. So there is no need for registering the .NET stuff anymore. The .NET library collects all the DLL files in the same directory and looks for a custom attribute to identify a .NET addin and presents them to the IDE. Also the management of the settings for all the addins is done by this library. The main goal of the design was to simplify the extension, by just copying a new addin DLL into the Addin directory, automatically recognized by the IDE on next start up.
This picture shows how the addin system works. The netwrapper.dll implements the IDSAddIn
interface like a normal VS6 addin. It simply redirects the IDSAddIn
calls to the VS6AddIn.dll which is fully written in C#. This module loads on startup all the DLL files in the same directory, checking for the VS6AddInPlugInAttribute
and adds each addin to the Visual Studio 6 IDE via the AddCommand
of the IApplication
interface. (When it's called for the first time, it also calls the AddCommandBarButton
member to create the toolbar). The implementation of the ICommand
interface in the netwrapper.dll is a bit tricky. The methods of the installed plugins are unknown at compile time. I rewrote the GetIDsOfNames
member of the IDispatch
interface, by simply numbering the names in ascending order. I also rewrote the Invoke
member of IDispatch
, it calls the InvokeCommand
of the PlugInManager
class with the command name as parameter. The PlugInManager
class uses the command name to identify the correct addin to be called, and calls its InvokeCommand
member.
Copy the netwrapper.dll to Visual Studio's Addin directory e.g. "c:\Program Files\ Microsoft Visual Studio\COMMON\MSDev98\AddIns" Create a subdirectory called netmultiaddin inside the addins directory and copy all other DLL files to this subdirectory. Start-up Visual Studio 6.0 and a new toolbar should appear containing all the installed .NET addins. If you add a new addin later on, it doesn't automatically appear if you restart the IDE, you have to add it manually to the toolbar from the customization dialog (or disable the Netwrapper addin and re-enable it after restarting the IDE).
- VS6AddInPlugInDefs.dll
- Contains the definition of necessary interfaces and the custom attribute used to mark an assembly as .NET addin (
IConfigPlugIn
, IVS6AddInPlugIn
, SetModifiedEventHandler
, VS6AddInPlugInAttribute
).
- Netwrapper.dll
- Holds the COM interfaces needed by the IDE to act as an addin. It redirects calls to this addin to the .NET world.
- Vs6addin.dll
- Holds the
PlugInManager
class and the Options
addin.
- Vs6addinUtils.dll
- Holds the utility classes to be used by some addins.
|
|
AutoComment.dll |
|
Comment out the current selection by a line or block comment. |
|
|
CStringEvaluator.dll |
|
Inspect the contents of a CString variable by selecting a CString variable while in debug break mode. If the CString contains an SQL SELECT statement the result set can be viewed in a data grid. |
|
|
GetFont.dll |
|
Create a LOGFONT declaration, containing the font data selected by common dialog for font selection. |
|
|
IDManager.dll |
|
Contains two addins: The Resource ID sorter, which renumbers IDs in the resource.h file, and the Resource ID array builder, which creates an array declaration, containing the resource ID and its name. |
|
|
MenuBinder.dll |
|
Finds and opens the menu handler, a dialog class if appropriate and its dialog resource, of a selected menu entry. |
|
|
RegexSearcher.dll |
|
Provides a full .NET regular expression for a search over directories, project or open files. |
|
|
SimpleTools.dll |
|
Contains two plugins: ShowDesign opens the dialog resource of an open dialog implementation or header file. ShowHeader opens the header file of a C++ implementation file and vice versa. |
|
|
SnippetManager.dll |
|
Provides a manager for dragging, dropping and managing code snippets to and from. |
|
|
TCHandler.dll |
|
Wraps the selection in the currently opened file with a predefined try /catch handler. |
|
|
WinDiffer.dll |
|
Calls windiff or a similar program on two text selections. |
- Start Visual Studio 2003
- Create a new class library project.
- Add a reference to VS6AddInPlugInDefs.dll.
- Add a reference to Visual C++ shared objects (6.0) (DSSharedObjects COM component).
- (Optional) add a reference to Visual C++ debugger (6.0), Visual C++ project system (6.0) and Visual C++ text editor (6.0).
- (Optional) add a reference to VS6AddInUtils.dll.
- Create a new class which implements the
VS6AddIn.IVS6AddInPlugIn
interface.
- If your addin needs to be configured by the user, add one or more user controls which implement the
VS6AddIn.IConfigPlugIn
interface.
- Add the
VS6AddIn.VS6AddInPlugInAttribute
to your assembly file e.g. [assembly: VS6AddIn.VS6AddInPlugInAttribute( typeof( <YourNameSpace>.<YourClassName>),"<YourCommandName>")]
, this can be done multiple times, if your DLL contains more than one addin.
- Add functionality to the
InvokeCommand
member of the addin.
- Build the project
string Commanddefs {get;}
Return "YourCommandName\nButtontext\nStatusbartext\nTooltiptext" for your plugin.
string CommandName {get;}
Return "YourCommandName" here, the same as in the assembly file.
int ButtonNr {get;}
Return the number of the button in the wrappers toolbar bitmap (0 to 15 are the standard bitmaps for your use).
int ButtonType {get;}
Return the type of the button (of kind DSSharedObjects.DsButtonType
).
Bitmap LargeBitmap {get;}
Return a 32 by 32 pixel, 16 colour bitmap for the large toolbar (for future use), RGB(192,192,192) is transparent.
Bitmap SmallBitmap {get;}
Return a 16 by 16 pixel, 16 colour bitmap for the small toolbar (for future use) RGB(192,192,192) is transparent.
bool InvokeCommand(DSSharedObjects.Application app);
Starts the command, app
is the application object of the Visual Studio instance, returns true
on success otherwise false
.
System.Windows.Forms.Control[] GetConfigObjects();
Returns the GUI objects for the configuration of your addin, an array of objects derived from System.Windows.Forms.Control
which implement the IConfigPlugIn
interface.
void ShutDown(bool bLastTime);
If your addin needs some cleanup, do it here. bLastTime
is true
if the user is removing (uninstalling) the addin. bLastTime
is false
if the Visual Studio is exiting.
void LoadDefault();
Load default data into the subcontrols.
void LoadData();
Load data (maybe from registry) into the subcontrols.
void SaveData();
Save data (maybe to the registry) from the subcontrols.
void About();
Show the Author information.
Don't forget to call System.Runtime.InteropServices.Marshal.ReleaseComObject()
for each used COM object from the IDE, otherwise the IDE will lock up on exit, waiting for those COM objects to be released. I've discovered some strange behaviour when the .NET wrapper for the COM object increments the reference counter. So, if you pass a wrapped COM pointer to a function call, always call the ReleaseComObject
before exiting the function. But if you pass the pointer to a constructor of a class and store it in a member variable, don't call ReleaseComObject
at the end of the constructor. There maybe some well defined logic, I don't know about that. You can easily check if your addin misses some ReleaseComObject
calls. After closing the IDE, bring up the task manager and search for the msdev.exe process, if it's still there with no activity, it waits for the COM pointers to be released, but the framework releases it if the AppDomain is shut down, so this is a deadlock situation.
The following code snippet shows how I handle the COM pointers:
Using DSTextEditor;
public bool InvokeCommand(DSSharedObjects.Application App)
{
ITextDocument txt = null;
ITextSelection sel = null;
try
{
txt = App.ActiveDocument as ITextDocument;
if (txt!=null)
{
sel = txt.Selection as ITextSelection;
if (sel!=null && sel.Text!="")
{
return true;
}
}
}
catch(Exception ex)
{
return false;
}
finally
{
if (txt!=null)
Marshal.ReleaseComObject(txt);
if (sel!=null)
Marshal.ReleaseComObject(sel);
if (App!=null)
Marshal.ReleaseComObject(App);
}
}
In my first attempt I exposed the VS6AddIn.PlugInManager
through a wrapper class to COM. In the netwrapper.dll I instantiated this COM object and called its members through COM. This approach needs a registration of the VS6AddIn.DLL with the regasm.exe tool using the /codebase switch because it cannot reside in the same directory as the netwrapper.dll (this confuses the IDE on startup, on trying to register these addin DLLs). But if you use the /codebase switch Microsoft strongly recommends you to sign your assembly. That's the reason why all my DLLs are signed with a strong name. Use the Debug or Release build configuration for this type of addin. Use regasm.exe /codebase vs6addin.dll /tlb:vs6addin.tlb to register the Pluginmanager and regasm.exe /u /codebase vs6addin.dll /tlb:vs6addin.tlb to unregister it.
Later on I decided to remove the ugly COM stuff from the netwrapper.dll using mixed mode code. So, I added a new build configuration named CLR (using the /clr switch for the compiler). _MANAGED
is only defined if this configuration is used. This approach is very easy to install, just copy the netwrapper.dll to the IDE's addin directory and copy all the .NET addin DLLs and the dependent DLLs to a subdirectory called netmultiaddin. The CLR configuration is the same as the Release configuration for all projects, except the Netwrapper project.
Before the first build of the whole solution you have to supply your own strong name, generated by the sn.exe tool from the .NET Framework SDK. Generate a keypair file rename it to vs6addin.snk and replace all the vs6addin.snk files in the project with your own file. If you use the mixed mode version of the netwrapper.dll you can also remove all the key files, but don't forget to remove the reference to it in the General Properties of the Buildconfiguration ("Wrapper Assembly Key File
") and in the assembly files of each addin ([assembly: AssemblyKeyFile()]
).
Basically the projects should build on each Visual Studio .NET (2002, 2003 and 2005), but I have developed it with Visual Studio 2003, so no changes should be necessary to rebuild it with this version of Visual Studio.
While this is a computer program, there is still some unfinished stuff to do. After reading Nick Hodapps article (see Links section), I've tried to implement the netwrapper.dll as a MFC-Extension DLL, but that doesn't work with the CLR switch. So, if someone knows how this can be solved, let me know.
Another misbehavior is the handling of the toolbar bitmaps. The MSDEV receives the complete toolbar bitmap through a resource identifier and an instance handle of the DLL containing the bitmap. So this time, the netwrapper.dll contains the fixed toolbar, and the addins bitmaps are ignored, because I don't know how to provide a dynamic bitmap to the IDE. There is already a function in the PlugInManager
class (of vs6addin.dll) that compiles a complete toolbar bitmap out of the different addin bitmaps. But the question is, how to present this to the IDE. Also, I don't know how to convert the created bitmap to 4 bits per pixel, but I think this can be solved easily. Another problem occurs on changing the title of the newly created toolbar on first start-up. I have used the trick mentioned by Nick Hodapp (see links section), but this doesn't work for Unicodebuilds. But these unsolved problems don't reduce the functionality, it's just "cosmetic".