Create Apps That Are Easily Extensible with Our Smart
|Snap-ins are simple in-process COM servers that can be hosted by a client applicationsimilar to the way that an ActiveX control is hosted by an ActiveX control container. Although they aren't part of the application, you can visually integrate snap-ins with the application's menu bar and toolbar.|
This article assumes you're familiar with C++ and COM
Code for this article: snapins.exe (261KB)
Steve Zimmerman is a senior software engineer and an adjunct professor at the University of Phoenix. He can be reached at firstname.lastname@example.org.|
If you're doing serious commercial software development, it's likely you are writing an application that must be easily extensible. Perhaps you plan to eventually incorporate features that will be developed by other product teams in your company. Maybe the application will be packaged as several products with varying levels of functionalitynamed the Standard, Professional, and Enterprise Editions, of coursemeaning that you must be able to conditionally enable certain components when the product is installed. You might even have plans to publish an API that allows third-party developers to write extension components that integrate seamlessly into your application.
For example, let's say you've just written a simple word processor like the WordPad application that ships with Windows® 95 and Windows NT® 4.0. Now you want to
create a Professional Edition that provides some of the tools found in other commercial word processors, such as autocorrection, spellchecking, document statistics, and duplicate word removal. In addition, you want to let other vendors provide tools that snap into the application, such
as syntax highlighting, grammar checking, or document
While none of these extension components, which I'll
call snap-ins, are part of the standard product, you want them added to the application's menu bar and toolbar
once they are installed so they are visually integrated. For now, let me merely define a snap-in as a simple in-process COM server that can be hosted by a client applicationæsimilar to the way that an ActiveX™ control is hosted by an ActiveX control container. A little later on,
I'll fill you in on the details of a snap-in implementation
I have developed that uses an interface called, appropriately, ISnapIn.
In this article, I'll show you how to write an application that supports snap-ins. In the spirit of the example given above, I'll convert the MFC WordPad sample into an extensible snap-in client. I'll also provide you with three extension components that snap into it: a snap-in that counts and displays the number of the words in the document; a snap-in that removes all duplicate entries of a word (it fixes the "to to" mistake in "I went to to the store"); and a snap-in that automatically corrects common spelling errors.
Before going any further, I want to make it clear that
I'm not attempting to come up with a replacement for
full-featured ActiveX controls. Therefore, if your goal is
to create general-purpose controls such as the Microsoft Calendar Control that support the umpteen control
interfaces expected by a well-rounded ActiveX control container, you'll have to read elsewhere. Furthermore, I'm
not going to show you how to write a container applica-
tion that is anywhere near as robust as Visual Basic®
or Microsoft Excel. What I am going to do is show you
how to give your application a basic level of extensibility without going to all the effort required to be a full-blown control container.
I should also point out that since the sample code I've written uses the Active Template Library (ATL) and takes advantage of new interfaces provided by the ActiveX SDK, I've developed it using Visual C++® 5.0. If you're still using Visual C++ 4.2b, you'll be able to compile the code, but you'll need to download the latest version of ATL and create your own makefiles (Visual C++ 5.0 project files are not backwards-compatible).
The Ideal Snap-In
Before I get to my specific snap-in implementation, let's talk about the general case. While it has obvious advantages, writing an extensible application is tricky. When designing the application, you can't make any assumptions about what snap-ins will be available at runtime or you'll end up releasing a new version of the product every time a new snap-in is created. Similarly, your snap-ins can't rely too much on the implementation details of the host application or they'll need to be rewritten every time the application is modified. In short, the less your application and its snap-ins know about each other, the better. With that goal in mind, I've come up with the following guidelines.
A snap-in should be self-registering. When a new snap-in is installed, the user should not have to perform any special action from within the host application to enable it. Once registered, the snap-in should appear in the application's toolbar and menu bar automatically. Similarly, when a snap-in is removed, it should require no action from within the application and should leave no trace.
If appropriate, a snap-in should be able to work with more than one host. While a snap-in must expect specific interfaces to be made available by its clients, the snap-in should work with any application that exposes the interfaces it recognizes. In other words, it should be possible to add a spellchecker snap-in to any application that provides it with an interface methodcalled something like GetDocumentBufferthat exposes an LPSTR.
A snap-in should be context sensitive. Depending upon the state of the host application, a snap-in should be able to perform different functions. For example, if a WordPad user selects a block of text with the mouse, a word-count snap-in would display the number of currently selected words. If no text is selected, the snap-in would instead display the total number of words in the document.
A snap-in should receive notification messages from its container when the user clicks the toolbar button or menu item associated with the snap-in, or whenever the internal state of the application changes in a way that affects the snap-in.
While a snap-in could be implemented in a number of ways, it is an ideal candidate for COM due to the nature of the communication between the snap-in and its container. Specifically, a snap-in exposes a well-defined set of functions that allows it to be fully integrated with the host application. In turn, the host exposes one or more interfaces that are recognized by the snap-in. While this model is similar to the traditional ActiveX control container approach, its implementation is likely to be much simpler because a snap-in and its host each need to expose only a single interface (in addition to IUnknown, of course). However, according to the latest ActiveX specification, an ActiveX control is simply a self-registering COM object that supports IUnknown. Thus, in a very real sense, snap-ins are a specialized form of ActiveX controls and a snap-in host is an ActiveX control container!
The ISnapIn Interface
It is unlikely that my implementation of a snap-in interfacewhich I named simply ISnapIn for lack of a better namewill meet the needs of every application, but what I've come up with will hopefully help you get headed in the right direction. An explanation of each of the ISnapIn methods is shown in Figure 1. You'll notice that the ISnapIn interface gives the snap-in the flexibility to provide its container with one or more of the following visual elements: menu item text, status bar text, tooltip text, and various sizes of toolbar buttons. However, the snap-in can run silently without any visual interface at all.
The ISnapIn interface methods are divided into two functional categories: methods such as GetMenuTextID that provide user-interface integration and methods that respond to user actions. While the user-interface functions are required for menu-merging and toolbar support, the functions that do the real work are the action functions. Specifically, OnStateChange is called whenever the state of the host application changes, and OnCommand is called when the user selects the snap-in from the toolbar, menu, or other user interface mechanism. Obviously, the action performed by these two interface methods depends entirely upon the functionality of the snap-in. In the case of my souped-up version of WordPad, the application calls each snap-in's OnStateChange method whenever the user changes the text of the document. The word-count snap-in, for example, counts each word in the document and displays the result in the application's status bar, as shown at the bottom of Figure 2.
|Figure 2: The Improved WordPad Application|
See No Evil
Obviously, a host application must share information with its snap-ins for them to do anything useful. The WordPad sample, for example, exposes the window handle of the rich edit control containing the user's document. However, it wouldn't make sense to implement an ISnapIn interface that always expected its container to pass it a rich edit controlor any other application-specific data, for that matterbecause you'd have to come up with a new interface every time you wrote an application that exposed something different. Instead, I've implemented ISnapIn so that the only context information passed to the snap-in by its container is a pointer to IUnknown. Of course, IUnknown by itself isn't very useful, but a snap-in can use it to check for the existence of other interfaces it recognizes by using the QueryInterface method.
The sample snap-ins I wrote look for IRichDocContextan essentially brain-dead interface I invented that has only two methods: GetRichEditControl and SetStatusText. My new version of WordPad implements IRichDocContext, of course, but the snap-ins it hosts don't know that they're integrated with WordPadthey just know that they're talking to an application that supports IRichDocContext. In your own application, you can expose whatever interfaces you want, but since the entry-point to those interfaces is IUnknown, your snap-ins can implement the exact same ISnapIn interface as the ones I've provided.
One of the most important design considerations when writing an extension component is to determine how it will make its existence known to host applications. In other words, if you deploy your application tomorrow and a compatible snap-in is installed on the computer a year from now, how will your application know about it? One method would be for the snap-in to place information about itself in a predefined section of your application's registry settings. Following this logic, a WordPad-compatible snap-in would place information about itself in the HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\
Applets\WordPad\SnapIns registry section. Each time it was executed, WordPad would check the registry to see if a new snap-in had been added.
While this approach would probably be adequate for snap-ins that are only designed to work with a single application, it suffers from scalability problems. Using this technique, whenever a new snap-in was installed, it would have to determine what compatible applications were already on the machine and place information about itself in the registry section of each application. As a result, a snap-in would have to know an unhealthy amount of information about each of its containers. Furthermore, every time you installed a new application, you'd have to reinstall each of its snap-ins or the application wouldn't know they existed.
The widely accepted method for solving this problem
has been to place one or more specially-named empty registry keys beneath the CLSID entry of the component as a way of indicating that the component supports a certain category of interfaces. To find its compatible components, an application scans through the list of components found in the registry, looking for CLSID entries marked with
the keys that correspond to the interfaces it requires.
For example, Figure 3 shows the CSLID registry entry
for the Microsoft Calendar Control. You'll notice that
it contains two empty keys: Control and Programmable.
Because these keys exist, an ActiveX control container
like Visual Basic 4.x knows it can host that control.
|Figure 3: The Microsoft Calendar Control Registry Entry|
This approach is clearly a much better solution than the one I described earlier because it does not require a control to know anything about its containers. But while this approach has worked fairly well for traditional ActiveX controls, it still has a serious flaw when applied to snap-ins. Suppose you added a special keynamed SnapIn, of courseto the CLSID entry for your snap-in, indicating that it supports the ISnapIn interface. This information alone would be insufficient because a host application must also know which snap-ins handle the specific interfaces it exposes. As a result, you'd also have to assign special registry keys for each type of exposed application interface. For example, since WordPad exposes an interface named IRichDocContext, it would have to look for all components marked with two keys: one named SnapIn and the other named something like SupportsRichDocument. If this categorization method were widely used, there would eventually be name collisions in the snap-in category descriptions. Perhaps a software developer in New Jersey would develop an application with an interface called IAmARichDoctor, but also name its corresponding registry key SupportsRichDocument. Or even worse, some guy in Vernal, Utah, would tweak the ISnapIn interface but still use the word SnapIn as its registry keyword. Pretty soon you'd end up with a real mess.
Fortunately, with the release of the ActiveX Platform SDK, there's a new specificationcomponent categoriesthat solves these problems. Similar to the special keys used to classify traditional ActiveX controls, component categories are registry entries that describe the type of functionality an ActiveX component supports. However, there are several improvements over the old model.
Figure 4 Component Categories
Instead of using human-readable names, component categories are defined using globally unique identifiers called category identifiers, or CATIDs. For example, the CATIDs registered on my machine are shown in Figure 4. Because a CATID is guaranteed to be unique, there's no fear of name collisions between different categories. Each CATID has one or more locale-specific text descriptions associated with it, stored in a well-known location in the registry. This makes it easier for control containers to support multiple languages.
In addition to describing the categories it implements, an ActiveX control can also describe the categories it requires from its container. This extra information will help solve a problem that has frustrated ActiveX control developers in the past. Previously, even when a developer created a control that provided all of the required interfaces, he or she might get variedand often undesiredresults due to the different way each control container (Visual Basic 4.x or 5.0, Microsoft Excel, Internet Explorer 3.0, Visual C++ Test
Container, and so on) interacts with the control. Since a developer can now define the exact set of interfaces that
the ActiveX control requires its container to implement, these inconsistencies should eventually disappear. Unfortunately, backward compatibility with old containers will still be a problem.
There is a system-provided COM object called the Component Category Manager that gives you two interfacesICatRegister and ICatInformationthat you can use to store and retrieve category information from the registry. No more parsing the registry by hand to find the controls that implement a certain category! While you'll need to refer to the online help for specific information on how to use those interfaces, here's how easy it is to create instances of them:
HRESULT hr = CoCreateInstance(
hr = CoCreateInstance(
Snap-In Component Categories
As I mentioned earlier, I came up with a simple interface called ISnapIn that provides just enough information to allow you to integrate a COM object with an application. Naturally, I defined a component category called CATID_
ISnapIn that classifies an ActiveX component as a snap-in. While most component categories indicate support for
several interfaces, registering your component as a member of the CATID_ISnapIn category simply means you guarantee that your object implements ISnapIn. In fact, I'll confess that the GUID I used to identify the CATID_ISnapIn category is the same one that identifies the ISnapIn interface, as you can see in Figure 5. For the WordPad sample, I also defined a category named CATID_IRichDocContext that applies only to containers supporting my IRichDocContext interface.
In production-quality code, you should not use the same GUID to identify both the CATID and its associated interfaces. However, since the code I've provided is just a sample, what I've done is probably OK. Furthermore, even though the number of possible component categories is infinite, you should avoid defining a new component category if there is an existing category that can be used instead. This suggestion is consistent with the idea that controls and containers should be designed with optimum interoperability in mind. As with regular ActiveX controls, if you are writing production-quality snap-ins or snap-in containers, you should collaborate with other vendors when defining new component categories to ensure that they meet the common requirements of your market. Microsoft has said it plans
to provide an up-to-date list of the component categories developed by itself and Microsoft vendor partners at http://www.microsoft.com. At press time the exact URL had not been defined, but I expect it will be in the near future. In the meantime, a search of the site for "component categories" may be helpful.
Come On Up to My Pad
Before I delve into the details of my sample snap-ins, let me explain the enhancements I made to WordPad so that it supports them. When making these changes, I had two goals in mind. First, I wanted to change as few routines as possible. In the spirit of information hidinga key concept in object-oriented designI wanted the application to know as little about snap-ins as possible. I'll also admit that I didn't want to have to take the time to learn about the inner-workings of WordPadI just wanted to sneak into the code, add the snap-in logic, and get out! My secondary goal was to encapsulate the snap-in code so that it would be easily reusable. If I've done my job well, you'll be able to take the code I added to WordPad and slip it into your own application with little effort.
What I ended up with was two classes, CSnapInFrame and CSnapIn, that implement the functionality required
to be a snap-in container. An overview of those classes appears in Figure 6. Adding them to the WordPad code that ships with Visual C++ was fairly easy. Since the WordPad project contains many files, I created a SnapIn subdirectory beneath Project Root in which I placed all of the reusable code. Then, I added those files to the project.
I replaced all references to CFrameWnd in mainfrm.h and mainfrm.cpp so that CMainFrame was derived from CSnapInFrame. I created a simple interface, IRichDocContext, that WordPad uses to make its context information available to the snap-ins. The header file for that interface is shown in Figure 7. I added the header file to the project
and added the following code to the CMainFrame class definition:
If you've used OLE with MFC, you'll immediately recognize this as the way to expose a COM interface. I don't have the space to explain what these macros do and how they work, so if you've never done this type of thing before, I recommend you read (and reread) the MFC help topic entitled "TN038: MFC/OLE IUnknown Implementation." It is sufficient to say that those macros create an embedded class within CMainFramecalled XContextObj, incidentallythat implements the IRichDocContext interface. They also create a member variable named m_xContextObj that represents an instance of the interface. Thus, somewhere in the source code for CMainFrame, I had to implement the GetRichEditCtrl and SetStatusText methods (see Figure 8). As expected, my GetRichEditCtrl interface method returns the window handle of the rich edit control used to display the WordPad document:
HWND FAR EXPORT CMainFrame::XContextObj::GetRichEditCtrl()
CRichEditView* pView =
Your applications probably won't use IRichDocContextat least I sure hope not, because it is admittedly a kludgebut you can still follow this model.
Since I wrote CSnapInFrame to be reusable, it knows nothing about the IRichDocContext interface. As a result, it has two pure virtual functionsGetSnapInContext and GetSupportedCategoriesthat I had to implement in CMainFrame:
IUnknown* CMainFrame::GetSnapInContext(int nMsg)
int CMainFrame::GetSupportedCategories(GUID** ppCatIDs)
static CATID CatIDs;
CatIDs = CATID_IRichDocContext;
*ppCatIDs = CatIDs;
In the GetSnapInContext function, although the code actually returns the address of m_xContextObjan IRichDocContext pointer, in other wordsthe return value is cast into a pointer to IUnknown. This way, the CSnapInFrame class can pass the host interface along to the snap-in without getting its hands dirty. Thanks to COM, when a snap-in gets the IUnknown pointer, it can simply call QueryInterface to get back the pointer to IRichDocContext. Thus, your application can return a pointer to IDontKnow or IDontCare, but you won't have to change CSnapInFrame.
The GetSupportedCategories virtual function is the
way an application specifies which container categories it implements. Again, since CSnapInFrame is not application-specific, this function has no default implementation; it must be implemented by any class derived from CSnapInFrame. WordPad only implements one snap-in container category, CATID_IRichDocContext, but your application may expose several. In that case, you'd point ppCatIDs
(see the previous code) to an array containing the CATIDs you implement and return the number of elements in that array.
To notify each snap-in whenever the user changes the WordPad document, I added the following code to CWordPadView::OnEditChange:
CMainFrame* pFrame = DYNAMIC_DOWNCAST(CMainFrame,
As you can see, when the user changes the text in the rich edit control, the application routes that message to the frame window. In turn, CSnapInFrame calls the OnStateChange interface method of each snap-in. The frequency of communication between WordPad and its snap-ins makes it possible for the word count snap-in to keep an accurate count of the number of words in the document, which it displays at the bottom of the screen. Incidentally, you should be aware that if you call OnStateChange too often (especially if your snap-ins perform a large number of calculations each time they are called), your application's responsiveness will suffer.
To avoid interfering with the registry settings of the real WordPadit is, after all, an application that ships with Windows 95 and Windows NT 4.0I changed the enhanced version's relative registry path from Microsoft\Windows\
CurrentVersion\Applets\WordPad to Microsoft\Microsoft Systems Journal\WordPad. This was more than a matter of politeness, incidentally. As it turns out, WordPad uses MFC's CDockState class to store and retrieve the position and state of its toolbars. Since my version of WordPad has an additional toolbar, it adds more information to the registry than the original WordPad can handle.
Finally, I changed the project settings so that the output directory for all build configurations is set to a single subfolder. I did this because the data files WordPad uses to convert between document typesnamely, Word 6 and rich text formatmust be located in the same directory as the application. Rather than place duplicate versions of those files in several different directories (Debug, DebugU, Release, and ReleaseU), I have the linker place the output files in a single directory regardless of the configuration. Since those files are quite large, this change will make a significant difference in the size of the file you have to download to get the sample code!
While it took several steps to wire things up, I was able to add snap-in functionality to WordPad with minimal
code changes. In fact, except for the two snippets of code I described above, all of the changes were specific to CMainFrame. Figure 8 shows the new functions I added to that class.
How CSnapInFrame Works
While I'll let you sort through the nitty gritty implementation details of CSnapInFrame at your leisure (see Figure 9), here's a high-level overview of what it does.
At create time, CSnapInFrame always creates a snap-in toolbar, regardless of whether or not it finds any compatible snap-ins. If it finds none, the empty toolbar is hidden from view when the frame window is activated. I had to do this because CDockState does not respond gracefully to a missing toolbar. Consider this perilous scenario: the user installs a snap-in and then runs the host application. When the application terminates, it stores the state of the snap-in toolbar in the registry using CDockState. Later, the user uninstalls the snap-in. The next time the application is executed, CSnapInFrame does not create a snap-in toolbar because there are no snap-ins to display. Consequently, CDockState crashes the application while trying to restore the state of a toolbar that has not been created!
Immediately after creating the snap-in toolbar, CSnapInFrame builds an array of CSnapIn objects used to keep track of each snap-in. It does this by traversing the list of compatible snap-ins using the ICatInformation interface. Incidentally, each snap-in is actually loaded into memory by the CSnapIn constructor using a call to CoCreateInstance. The CSnapIn class stores the pointer to ISnapIn so that it can be used in later communication with that component.
If any snap-ins exist, CSnapInFrame calls its AddMenuEntries function, which adds a command to the View menu that lets the user toggle the visibility of the snap-in toolbar. It also creates a Tools menu, where the menu command for each snap-in is placed. Since AddMenuEntries is a virtual function, you can easily override it if you want your menu to behave differently. Each CSnapIn object adds its snap-in's toolbar button and menu item to the application window. As I've mentioned previously, a hidden snap-in may not provide that information, so CSnapIn is written to handle that case as well.
Because the tool-tip text and status bar message for each snap-in do not reside in the resource module for the application, CSnapInFrame has to perform some sleight-of-hand to display them properly. This is because MFC expects to find those resources in the same module as all of the other resources used by the application. To get around this, CSnapInFrame overrides the GetMessageString and OnToolTipText functions and twiddles the application's global resource handle (using a call to AfxSetResourceHandle) before and after calling the base-class implementation of those functions. This approach works fine, but it's not thread-safe. If your application calls AfxGetResourceHandle from simultaneous threads, you'll need to add thread synchronization to your code to make sure that other threads are properly blocked while the primary thread is processing GetMessageString or OnToolTipText.
CSnapInFrame has three functionsOnSnapIn, OnUpdateSnapInUI, and OnStateChangedthat make calls to the ISnapIn interface functions OnCommand, IsEnabled, and OnStateChanged, respectively. Refer to Figure 9 for details.
My Three Snap-Ins
As I mentioned at the outset, I wrote three simple snap-insa word counter, a duplicate word remover, and an autocorrect toolbut don't expect to see them for sale anytime soon! I've provided them just to give you an idea of the kinds of snap-ins you can build yourself. Unfortunately, unlike the CSnapInFrame and CSnapIn classes, most of the sample snap-in code is specific to its integration with an IRichDocContext host application. Thus, you won't be able to copy and paste large chunks of the code into your own snap-in projects.
I wrote the snap-ins using a combination of ATL and MFC. Before beginning this project, I often wondered why anyone would want to mix those two frameworks. Now I have an answer: ATL makes working with COM interfaces a painless experience, but it really can't compare with MFC when it comes to developing dialog boxes. So, I used ATL for everything but the autocorrect preferences dialog box (see Figure 10).
Figure 10 Autocorrect Preferences
Each snap-in exposes an implementation of the ISnapIn interface, which has three methods that get called by the host application in response to user action: OnStateChanged, IsEnabled, and OnCommand (refer to Figure 1). Each snap-in handles those functions differently. The word-count snap-in counts the number of words in the document every time its OnStateChanged method is called. It has no menu item or toolbar button, so its OnCommand and IsEnabled methods are never called. The duplicate word snap-in scans the document for repeated words whenever the user selects it from the menu, but its OnStateChanged method does nothing; its IsEnabled function returns false when the WordPad document is empty. The autocorrect snap-in replaces spelling errors
in the document in its OnStateChanged method and displays a settings dialog box whenever its OnCommand method is called.
It seems to me that the ideal way to register the snap-in component categories would be to derive my own class from CComModule and override its RegisterClassHelper and UnregisterClassHelper functions. However, I was disappointed to find that neither of those functions is virtualat least, they weren't at the time of this writing. Of course, I'll probably get email from you COM experts out there telling me that the best way to do it is with registry scripting. Since I haven't figured out how to do that yet, my approach was to write three helper functionsRegisterComponentCategory, RegisterClassReqCategory, and RegisterClassImplCategory (see Figure 11)that you may find useful in your own code. They act as general-purpose wrappers around the calls to the interface methods of ICatRegister. With the help of those functions, adding component category registration to the DllRegisterServer function was
T("Snap-Ins that support the
_ATL_OBJMAP_ENTRY* pEntry = _Module.m_pObjMap;
while (pEntry->pclsid != NULL)
return _Module.RegisterServer(FALSE) // No typelib
The autocorrect snap-in allows users to maintain a persistent list of commonly misspelled words (and their replacements, of course) by storing that information in a file. However, I had trouble deciding whether the file should be specific to each host application or shared among all of them. I finally decided to associate a wordlist with its host so each application using the autocorrect snap-in has its own file. I probably should have developed an additional interface that the application uses to tell the snap-in where to store the file, but I simply use the same path name as the host application and change its file extension, like so:
TCHAR* pFileExt = _tcschr(sFileName, _T('.'));
_tcscpy(pFileExt, _T(".acf")); // auto-correct file
I've shown you how to use simple COM objects, called snap-ins, to extend the functionality of your application. I've discussed some of the features of an ideal snap-in and I showed you an implementation that uses my homegrown ISnapIn interface. Feel free to modify and improve that interface to meet your needs. Just remember to use a different CLSID in case someone else does the same thing. Hopefully, I've given you several ideasand some nifty sample codethat will help you along the way. Incidentally, if you happen to develop a super-duper WordPad-compatible snap-in, I'd love to hear about it.
© 1997 Microsoft Corporation. All rights reserved. Legal Notices.