Extending Developer Studio with Add-ins

Richard Grimes

Microsoft Developer Studio is an Automation server that exposes its functionality through an object model: You can access these objects through scripting and extend it through add-ins. This article will explain what the object model is and how to develop a DevStudio add-in.

Most Microsoft applications expose an object hierarchy. Word and Excel, for example, expose an application object, document objects, and collections of objects. DevStudio hasn't been left out—it too has an object model that's available to the developer. Mark Davis introduced you to the object model in the October 1997 issue of Visual C++ Developer (see "Enhancing the Visual C++ 5.0 Workplace with VBScript Macros") and showed you how to access the DevStudio objects through scripting. You aren't restricted to using macros to get access to these objects; you can also do it using C++. To get access to these objects, you need to develop a DLL called an add-in. DevStudio will give you a pointer to the Application object, which is your starting point to the other objects in the hierarchy.

In addition to the object model, all the commands that are available to the user through the DevStudio menu are also available as commands to the script developer. You can add commands by exposing a Command object within your add-in. Furthermore, the DevStudio objects can generate events that your add-in Command object can catch and react to. In this article, I'll show you how to add a new command, how to catch DevStudio events, and how to access the DevStudio object model.

DevStudio object model

The DevStudio object model is shown in Figure 1. As with other Microsoft applications, the top object in DevStudio is the Application object. You can get access to all other objects from this object. Some of these objects, like the Document object, are collections that hold zero or more other objects. Other objects are available in more than one form—for example, a Document object can also be treated as a TextDocument object.

One confusing aspect is trying to decide which object you need and, once you've made that decision, determining how to get the object you need. In the code that's available in this month's Subscriber Downloads at www.pinpub.com/vcd, I'll give an example of getting the TextSelection so that I can edit a document. The TextSelection object is the Selection property of a TextDocument object, which in turn is obtained by casting a Document object to the TextDocument type (using COM's cast operator, also known as QueryInterface()).

Figure 1. DevStudio object model.

Commands

When you select a menu item in DevStudio, you're performing a command. If you're interested in the commands that you can perform, take a look at the Customize item of the Tools menu: If you select the Keyboard tab, you'll be able to select a Category (for example, All Commands) and then take a look at the commands in the Command box. Many of these correspond to menu items (for instance, FileClose), but some correspond to actions you can perform (for example, WordTranspose). All commands are characterized by one significant property: They have no parameters.

To perform a command, you can either select the appropriate menu item, execute it in a VBScript macro, click on a toolbar button that runs the command, or run it from the command line. To run a command from the command line, you use the -ex switch:

msdev -ex ApplicationExit

This performs the rather pointless task of running DevStudio and then performing the ApplicationExit command that shuts down DevStudio.

Events

There are two objects in the VC object model that generate events: the Application object and the Debugger object. The Application object generates events like the opening and closing of windows and workspaces, project builds, and the DevStudio shutting down. The Debugger object generates one event—a breakpoint is hit. Since DevStudio knows nothing about your Command object until it connects, these events are handled using dispinterfaces.

Add-ins

A DevStudio add-in is a DLL COM server. When you install it, DevStudio will determine from the add-in's type library the objects in the server that support the IDSAddIn interface. It can then load these objects and call the IDSAddIn::OnConnection() method. These DSAddIn objects can then add new Command objects to the DevStudio object model; each Command object can implement one or more new commands. Once added, these commands can be accessed via scripts or on the command line; if you prefer, you can create a toolbar and add the commands as buttons. Commands and toolbars are added by calling appropriate methods on the Application object; DevStudio passes a reference to this object as a parameter to the OnConnection() method. When the add-in is removed, DevStudio calls OnDisconnection(), which you can use to do any clean-up that's necessary.

Add-ins can also handle events in your add-in. To do this, the add-in must create a notification object with the appropriate dispinterface and pass it to the Application object of DevStudio to attach it to a connection point object.

Add-in example

As an example, in the rest of this article, I'll develop an add-in that will catch the NewDocument event when a new document is created. In response, this add-in will add a copyright notice at the top of the file. Because the add-in will add a comment, I need to determine the type of document so I can format the comment correctly, since C++ comments and HTML comments have different syntax, for example. I also need to get the date (for the copyright year), and I need to get the copyright string.

Determining the document type is quite easy: The event has a reference to the new document as a parameter, and the type is obtained through its Language property. The copyright string has to come from the user and is held in a persistent store so that it can be used whenever DevStudio is started. In this example, the copyright string is held as a value in the Registry. I put this in the HKEY_CURRENT_USER hive so that multiple users of the same machine will get different strings. The value is put in there by the new command added by the add-in—NewCopyrightName(). Since commands don't take parameters, this command has to get the string through a modal dialog.

Developing add-ins

So how do you develop an add-in? The easiest way is to use the Visual C++ DevStudio Add-in wizard, shown in Figure 2.

Figure 2.DevStudio Add-in wizard.

The two edit boxes at the top provide text that's used to describe the add-in. The two check boxes at the bottom are more interesting—the first will generate code in the OnConnection() to create a toolbar, and the second will add support to handle events in your Command object.

The classes that the wizard generates are shown in Figure 3. Notice that I've called the project Copyright.

Figure 3. Add-in classes created by the AppWizard

The CDSAddIn object implements IDSAddIn and creates an instance of the CCommands object. This has an ApplicationEvents object and a DebuggerEvents object, which are used to handle events, and it also implements the commands that you want to add to DevStudio. Notice that the command the wizard creates is called CopyrightCommandMethod(). I want this to be called NewCopyrightName(), so the first task is to edit the Copyright.odl file, the Commands.h file, and the Commands.cpp file to make this change.

Both CDSAddIn and CCommands are ATL classes. This might seem odd, but it's perfectly possible—the project is an MFC DLL that implements ATL objects. Using ATL means that you have the option of using MFC and ATL helper classes. In particular, it's useful to be able to use CString from the former and CComPtr<> and CRegKey from the latter. Since the add-in is only used with DevStudio, which will have the MFC libraries already loaded, code size isn't an issue, as it is with ActiveX objects downloaded over a network.

To add a new command to DevStudio, an add-in calls the AddCommand() method of the Application object. This method takes several strings. The first string contains substrings separated by newlines and gives the name of the command, followed by strings used for the toolbar button, the status bar, and the tooltip. The wizard will generate a string resource called IDS_CMD_ STRING. However, the default value that it provides appears to bear very little resemblance to the project, so I've used ResourceView to change it to this:  

\nNew Name\nChanges the copyright _
  name\nChanges the copyright name

[Note: In code, an underscore (_) as the last character of a line indicates that the line has been broken for layout purposes. To run the code in Visual C++, you must recombine the broken line into a single line.—Ed.] Notice that the first string is missing—this is the name of the command, and it's added to the string in OnConnection() as the variable szCommand. I've changed this to NewCopyrightName.

The next parameter of AddCommand() is the name of the method in the CCommands class that implements this command, so I've edited the code to NewCopyrightName.

Since the command requires a dialog box, the next task is to add a dialog resource, which is pretty trivial with ResourceView. I've implemented this dialog box with a class called CNewName (added with ClassWizard)—the single text box on the dialog box has a bound data member called m_strName, which is used by NewCopyrightName() and NewDocument(). I've also added a public data member to the CCommands class to hold this string.

NewCopyrightName() looks like this:

STDMETHODIMP CCommands::NewCopyrightName()

{

    AFX_MANAGE_STATE(AfxGetStaticModuleState());

   CRegKey copyrightName;

   DWORD dwRet;

   dwRet = copyrightName.Create(HKEY_CURRENT_USER,

      _T("Software\\PinPub\\VC AddIns\\Copyright"));

   if (dwRet != ERROR_SUCCESS)

      return HRESULT_FROM_WIN32(dwRet);

   if (m_strName.IsEmpty())

   {

      TCHAR strName[256];

      DWORD dwSize = 256;

      dwRet = copyrightName.QueryValue(strName,

         _T("Name"), &dwSize);

      m_strName = strName;

   }

   CNewName dlg;

   dlg.m_strName = m_strName;

   if (dlg.DoModal() == IDOK)

      m_strName = dlg.m_strName;

   copyrightName.SetValue(m_strName, _T("Name"));

   return S_OK;

}

First, this checks the Registry for the string and, if it exists, uses it to initialize the dialog box. Next the modal dialog is created, and when it's dismissed, the result is saved in m_strName and written to the Registry. I use the ATL class CRegKey because it simplifies access to the Registry and ensures that the Registry handle is correctly closed.

When OnConnection() is called, it should check this Registry value and, if it doesn't exist, call NewCopyrightName():

STDMETHODIMP CDSAddIn::OnConnection(IApplication* pApp,

      VARIANT_BOOL bFirstTime,

      long dwCookie, VARIANT_BOOL* OnConnection)

{

   // other code

   CRegKey copyrightName;

   TCHAR strName[256];

   DWORD dwSize = 256;

   DWORD dwRet;

   copyrightName.Create(HKEY_CURRENT_USER,

     _T("Software\\PinPub\\VC AddIns\\Copyright"));

   dwRet = copyrightName.QueryValue(strName,

     _T("Name"), &dwSize);

   if (dwRet != ERROR_SUCCESS || lstrlen(strName) == 0)

      m_pCommands->NewCopyrightName();

   else

      m_pCommands->m_strName = strName;

   return S_OK;

}

 

Notice that access to the CCommands object is via the variable m_pCommands. This was created earlier in this method with the wizard-generated code:

CCommandsObj::CreateInstance(&m_pCommands);

Why is CCommandsObj used rather than CCommands? The reason is that ATL classes can't be created—they're abstract base classes. Instead, to create an object, you need to use a class derived from CComObject<>. If you look in the header for this class, you'll find the typedef:

typedef CComObject<CCommands> CCommandsObj;

The final piece of code is to implement the event handler. When a new document is created, the Application object will generate the NewDocument event and send it to all the clients attached to its connection point. The CDSAddIn object made this connection in OnConnection() with this line:

m_pCommands->SetApplicationObject(pApplication);

This caches an interface pointer to the Application object in the CCommand object and makes the connection between these two objects. When the NewDocument event is generated, it's sent to the CCommands object and is handled in the NewDocument() method:

CCommands::XApplicationEvents::NewDocument()

In this method, I need to get the current year (for the copyright date) and then determine the document type. The pointer passed to the event handler is a pointer to the Document dispinterface, so to get the document type, I need to access the ITextDocument interface:

USES_CONVERSION;

CComQIPtr<ITextDocument, &IID_ITextDocument>

        pTextDoc(theDocument);

HRESULT hr = S_OK;

CComBSTR bstrLang;

hr = pTextDoc->get_Language(&bstrLang);

if (FAILED(hr))

   return hr;

LPTSTR strLang = W2T(bstrLang);

Notice that I'm using the ATL CComQIPtr class to do the QueryInterface() to get the interface that I'm interested in. Once I've determined the type of document, I use this to get the correct comment type:

CComBSTR bstrComment;

if (lstrcmp(strLang, DS_CPP) == 0)

   bstrComment = L"// ";

else if (lstrcmp(strLang, DS_HTML_IE3) == 0

   || lstrcmp(strLang, DS_HTML_RFC1866) == 0)

   bstrComment = L"<!-- ";

else if (lstrcmp(strLang, DS_IDL) == 0)

   bstrComment = L"// ";

else if (lstrcmp(strLang, DS_VBS) == 0)

   bstrComment = L"\' ";

else if (lstrcmp(strLang, DS_JAVA) == 0)

   bstrComment = L"// ";

else bstrComment = L"// ";

These DS_ string constants are defined in TextDefs.h file in the objmodel directory in your DevStudio include directory. This directory also has the headers for the interfaces implemented by the DevStudio objects.

Next, I need to determine whether I can write to the Document, which, for our purposes, means determining whether it's a text document.

CComBSTR bstrType;

hr = pTextDoc->get_Type(&bstrType);

if (FAILED(hr))

   return hr;

LPTSTR strType = W2T(bstrType);

if (lstrcmp(strType, DS_TEXT_DOCUMENT) == 0)

{

Next, it's time to construct the copyright string. The first thing to do is get the year, then the copyright string. Note that the event handler is managed by an embedded object within the CCommands class, and this object's class is a nested class of CCommands. This means that I can't access data members of the CCommands directly. However, this embedded object is initialized with a pointer to the outer CCommands object, which is held in m_pCommands. Notice also that if the document type is HTML, then I need to add a closing symbol to the comment. 

   CComBSTR bstrText;

   bstrText = bstrComment;

   bstrText += L"Copyright ";

   SYSTEMTIME st;

   GetLocalTime(&st);

   TCHAR strYear[5];

   wsprintf(strYear, _T("%ld"),st.wYear);

   bstrText += strYear;

   bstrText += L" ";

   bstrText += (LPCTSTR)m_pCommands->m_strName;

   if (lstrcmp(strLang, DS_HTML_IE3) == 0

      || lstrcmp(strLang, DS_HTML_RFC1866) == 0)

      bstrText += L" -->";

   bstrText += L"\n";

For example, if your copyright string was "Your Name Here", and you'd just created a new C++ file, this code would generate the following:

//Copyright 1998 Your Name Here

For an HTML file, it would generate this:

<!--Copyright 1998 Your Name Here -->

Finally, the comment is written to the document. This is done by accessing the ITestSelection interface, then moving to the start of the document and writing the comment as the entire text of the document. Since the document is empty, this is quite acceptable.

   CComPtr<IDispatch> pDisp;

   hr = pTextDoc->get_Selection(&pDisp);

   if (FAILED(hr))

      return hr;

   CComQIPtr<ITextSelection, &IID_ITextSelection>

         pTextSel;

   pTextSel = pDisp;

   hr = pTextSel->StartOfDocument(CComVariant());

   if (FAILED(hr))

      return hr;

      

   hr = pTextSel->put_Text(bstrText);

}

This completes the add-in, which can now be compiled.

Installing the add-in

Installing an add-in is relatively painless. Select the Customize menu item from the Tools menu, and click on the Add-ins and Macro Files tab. The build process of the add-in will have registered the add-in as a COM object, but I still need to tell DevStudio the name of the server file. I do this by using the Browse button to select Copyright.dll.

When I dismiss the Customize dialog box, it loads the Add-In, then creates the DSAddIn object and calls OnConnection(), which creates the GetName dialog box shown in Figure 4.

Figure 4 GetName dialog box.

The add-in also produces a toolbar, and when I click on this button it also brings up the GetName dialog box. The name of the toolbar is Toolbar1 (the only way to change the name of the toolbar is by hand, through the Customize dialog box).

Testing the add-in is easy. If I click on the New Text File button on the Standard toolbar, the Document type will be unknown, so the add-in defaults "//" as the comment. To create a Document of a known type, I use the New dialog box on the File menu. On this dialog box, I've selected the Files tab and the HTML Page option to get the window shown in Figure 5.

Figure 5 HTML Page with a copyright comment.

This shows that the add-in can select the correct Document type.

There are two more tests I need to do. The first is to look on the Commands tab of the Customize dialog box and confirm that the NewCopyrightName command has been added to the Commands box. The second is to use this command by starting a new instance of msdev at the command line with this:

msdev -ex NewCopyrightName

Since the add-in will be installed, this will run the command, which will give the GetName dialog box.

Debugging tips

Since an add-in is a DLL loaded as part of DevStudio, you need to load an instance of it to edit the add-in. The best way to do this is to select msdev.exe as the "Executable for debug session" from the Debug tab of the project settings. Remember that msdev.exe is in the DevStudio\SharedIDE\bin directory. Also be careful of loading the add-in into the instance of DevStudio that you use to compile the project, because the linker won't be able to access the DLL during the link phase. You'll need to remember to unload the add-in before you compile it.

Conclusion

The DevStudio object model gives you access to components of DevStudio. You can hook into the events of these objects with a DevStudio add-in that can also be used to add a new command. In this article, I've shown you how to create an add-in, and I've introduced you to the add-in AppWizard that generates a hybrid MFC/ATL project. I use the example to catch the NewDocument event and add a copyright notice to the top of a new file. This example was adapted from my new book, Professional ATL COM Programming (WROX Press).

Download sample code here.

Richard Grimes was a semiconductor scientist, but now he writes and programs COM, DCOM, and ATL. He's the author of Professional DCOM Programming and co-author of Beginning ATL COM Programming. His latest book is Professional ATL COM Programming. All are published by WROX Press. richard.grimes@demon.co.uk.