Implementing Edit Designers: The Basics

This tutorial and its accompanying sample explain how to implement the IHTMLEditDesigner interface, which is used to customize the behavior of the MSHTML Editor. It demonstrates how to implement IHTMLEditDesigner, explains how to control the Editor's processing of events with the return values from IHTMLEditDesigner methods, shows how to add edit designers to the MSHTML Editor and how to remove them, describes the use of support interfaces for designer initialization and other tasks, and shows you how to import Mshtml.idl into your Interface Definition Language (IDL) files.

This article is divided into the following sections:

  • Prerequisites and Requirements
  • Introduction
  • The Sample
  • Implementing IHTMLEditDesigner
  • Using S_OK and S_FALSE to control Event Handling
  • Adding and Removing Edit Designers
  • Designer Initialization and Support Interfaces for IHTMLEditDesigner
  • MIDL and Mshtml.idl
  • Conclusion
  • Related topics

Prerequisites and Requirements

To make best use of this tutorial and use its accompanying sample, you need:

Introduction

An edit designer is a COM component that allows you to customize the MSHTML Editor. The only interface an edit designer must implement is IHTMLEditDesigner, which has four methods that act as callback routines. The MSHTML Editor calls these methods in response to events occurring in the Editor. These methods are:

The Sample

The sample for this tutorial implements an IHTMLEditDesigner interface that monitors the calls to the IHTMLEditDesigner methods. It demonstrates the important features of edit designer implementation:

The sample is a simple browser implementation with an address bar, five buttons, and a combo box. It monitors calls to the edit designer methods so that you can see the pattern of interaction between an edit designer and the MSHTML Editor. The specifications are as follows:

  • The Design Mode button switches the browser between design mode and browse mode.
  • The PreHandle S_OK button, when depressed, causes the edit designer to return S_OK from its PreHandleEvent call. When the button is not depressed, the edit designer returns S_FALSE.
  • The PostHandle S_OK button, when depressed, causes the edit designer to return S_OK from its PostHandleEvent call. When the button is not depressed, the edit designer returns S_FALSE.
  • The TransAcc S_OK button, when depressed, causes the edit designer to return S_OK from its TranslateAcceleratior call. When the button is not depressed, the edit designer returns S_FALSE.
  • The list box on the left side records the edit designer methods as they are called, along with an event description. You can double click any item in the list for more detailed information on the event.
  • The check boxes below the list box allow you to prevent some event messages from being recorded in the list box. Note that the check boxes only prevent the list box from recording these events; the events occur in the edit designer regardless of any selection made in the check boxes.

The source code for this sample is included in a Microsoft Visual C++ 6 workspace. It uses ATL to provide COM support, standard implementations of some of the standard interfaces, and "smart" interface pointers that handle their own reference counting. You can use this sample as a structure for building your own implementations of IHTMLEditDesigner. The sample and project source code can be downloaded at the Edit Designer Monitor Sample Source Page.

Structurally, the sample consists of three classes, implementing three interfaces. The classes are:

  • CEdMonitor: this class implements IHTMLEditDesigner along with a supporting interface, IEDMessenger, and is the focus of this article.
  • CBrowserHost: this class implements the application's window—which includes a WebBrowser control, buttons, and text boxes—and implements IBrowserHost to create, show, hide, and destroy the window. The function of most interest here is CBrowserHost::OnButton1. OnButton1 switches a document displayed in the WebBrowser control between design and browse mode. In the course of switching modes, the application instantiates or releases the Edit Designer Monitor's IHTMLEditDesigner interface and adds or removes it from the MSHTML Editor.
  • CExeModule: this class is the server for the executable file. This module exposes the CBrowserHost and CEdMonitor objects. For the purposes of this article, this class only provides support and won't be discussed further.

The sample has been kept simple in order to focus on the implementation structure of edit designers. For this reason, the sample performs minimal error checking and no exception handling. A "real-world" application generally provides more robust error checking and exception handling.

Implementing IHTMLEditDesigner

In principle, implementing IHTMLEditDesigner is quite simple. You need to implement just one interface, IHTMLEditDesigner. No other supporting interfaces or components are required, though in most cases they will be desirable or necessary. A stripped-down class declaration for an edit designer, in which IHTMLEditDesigner is implemented by inheritance, might look like this:

class CMyDesignerImpl : 
    public IHTMLEditDesigner,
    public ISupportingInterface
{
public:
    // IHTMLEditDesigner
    HRESULT PostEditorEventNotify(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);
    HRESULT PostHandleEvent(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);
    HRESULT PreHandleEvent(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);
    HRESULT TranslateAccelerator(DISPID inEvtDispId, IHTMLEventObj *pIEventObj);

    // ISupportingInterface
    HRESULT SetHost(IUnknown* pUnkHost);
    HRESULT SetDoc(IHTMLDocument2* pDoc);

    // IUnknown
    ULONG AddRef();
    HRESULT QueryInterface(REFIID iid, void** ppv);
    ULONG Release();
};

Once implemented, IHTMLEditServices::AddDesigner and IHTMLEditServices::RemoveDesigner enable you to activate and deactivate a designer at will.

In the course of implementing an edit designer, you'll be dealing with the events that are passing through the MSHTML Editor during a design mode session. Each edit designer method gives you access to these events through the method parameters. The first parameter is the DISPID for the event. The second parameter is an IHTMLEventObj interface for the event. From the DISPID and event object, you can determine what the event is, the state of the mouse buttons, the state of keystrokes on the keyboard, mouse cursor position in the screen, and so on.

Edit designer implementation is powerful because it interacts with the Editor's handling of events. At the same time, edit designer implementation can be complex in order to work correctly and efficiently with MSHTML's default event handling. Keep in mind that other edit designers might be attached at the same time as yours. Consider carefully where you want your designer to act—on which event, on which part of what element, in which part of the screen, in what part of the document markup, and so on.

MSHTML provides a variety of tools to help you with edit designer implementation—for instance, it provides the markup, display, selection, and highlight interfaces. Keep in mind that returning S_OK from your method implementation cancels a large part of the MSHTML Editor's functionality, even in the IHTMLEditDesigner::PostHandleEvent method. Most of the Editor's event handling occurs after the IHTMLEditDesigner::PostHandleEvent method call. If you return S_OK from IHTMLEditDesigner::PostHandleEvent, you usually override the Editor's handler; if you return S_OK from IHTMLEditDesigner::PreHandleEvent, you override the default handlers for both the Editor and MSHTML. Most implementations of an edit designer will return S_FALSE from their IHTMLEditDesigner::PreHandleEvent, IHTMLEditDesigner::PostHandleEvent, and IHTMLEditDesigner::TranslateAccelerator methods for all but a few of the method calls.

Using S_OK and S_FALSE to control Event Handling

S_OK and S_FALSE are the important return values for IHTMLEditDesigner::PreHandleEvent, IHTMLEditDesigner::PostHandleEvent, and IHTMLEditDesigner::TranslateAccelerator. Your designer uses them to control how the MSHTML Editor will proceed with event processing after returning from a call to one of these edit designer methods.

S_OK signals the MSHTML Editor that your method has completely handled the event and that the Editor should not proceed with any further processing, including calls to other edit designer methods. These other edit designer methods can be those from your edit designer implementation or from other edit designers that might be attached to the MSHTML Editor at the same time. For instance, returning S_OK from your IHTMLEditDesigner::PreHandleEvent method will prevent any calls to other edit designer IHTMLEditDesigner::PreHandleEvent methods for those edit designers that were added after yours. Returning S_OK from your IHTMLEditDesigner::PreHandleEvent method will also prevent your own IHTMLEditDesigner::PostHandleEvent method being called. Returning S_OK from IHTMLEditDesigner::TranslateAccelerator, which is called before IHTMLEditDesigner::PreHandleEvent, cancels your IHTMLEditDesigner::PreHandleEvent and IHTMLEditDesigner::PostHandleEvent method calls; it also cancels calls to all the methods of all other attached designers.

A return value of S_FALSE from the IHTMLEditDesigner::PreHandleEvent, IHTMLEditDesigner::PostHandleEvent, or IHTMLEditDesigner::TranslateAccelerator methods signals the MSHTML Editor that the method has not completely handled the event. At this point, the MSHTML Editor will continue to call other edit designer methods (both those of other active edit designers and yours) or proceed with the MSHTML Editor's own event processing.

IHTMLEditDesigner::PostEditorEventNotify functions differently than the other edit designer methods. Once an event has been completely handled—either as signaled by an S_OK return value from an edit designer method or by passing through all active designer methods and the MSHTML Editor default processing—the MSHTML Editor will call the IHTMLEditDesigner::PostEditorEventNotify methods of each active edit designer. These methods are called in the order that the edit designers were added. The MSHTML Editor ignores the return value from IHTMLEditDesigner::PostEditorEventNotify, which means that it is not possible to cancel calls to IHTMLEditDesigner::PostEditorEventNotify. IHTMLEditDesigner::PostEditorEventNotify will always be called when the MSHTML Editor is finished with an event. IHTMLEditDesigner::PostEditorEventNotify is useful for ensuring that those edit designers attached to the Editor at the same time as yours do not inadvertently prevent your designer from completing its tasks, as they might in some cases by returning S_OK from one of their IHTMLEditDesigner method calls. IHTMLEditDesigner::PostEditorEventNotify gives your designer a final chance to verify that its work has been completed before the event is over.

Adding and Removing Edit Designers

Use IHTMLEditServices::AddDesigner to add an edit designer to the editor. You can obtain an IHTMLEditServices interface pointer through an IServiceProvider interface obtained from the document interface. The following code demonstrates the order of calls needed to switch a document displayed in the WebBrowser control to design mode and attach an IHTMLEditDesigner interface to the MSHTML Editor.

// Error checking omitted for clarity
// Assume m_spWebBrowser is a valid pointer to the IWebBrowser interface
IDispatch* pDisp;
IHTMLDocument2* pDoc;
IServiceProvider* pServProv;
IHTMLEditServices* pEditServ;
IHTMLEditDesigner* pDesigner;

// Get current document in browser
m_spWebBrowser->get_Document(&pDisp);
pDisp->QueryInterface(IID_IHTMLDocument2,
                      (void**)&pDoc);

// Activate Design Mode
pDoc->put_designMode(L"On");

// QI doc for IServiceProvider
pDoc->QueryInterface(IID_IServiceProvider,
                     (void**)&pServProv);

// QS for IHTMLEditServices
pServProv->QueryService(SID_SHTMLEditServices,
                        IID_IHTMLEditServices,
                        (void**)&pEditServ);

// Create an instance of your designer
CoCreateInstance(CSLID_MyDesignerImpl,
                 NULL,
                 NULL,
                 IID_IHTMLEditDesigner,
                 (void**)&pDesigner);

// Add the designer to the editor
pEditServ->AddDesigner(pDesigner);

// Free resources
pDesigner->Release(); // You don't need to save the IHTMLEditDesigner
                      // pointer except for a specific reason
pDisp->Release();
pDoc->Release();
pServProv->Release();
pEditServ->Release();

You don't need to save your designer pointer specifically; the MSHTML Editor keeps its own internal copy. You might, however, want to save your designer pointer if you'll be activating and deactivating the designer during the course of one design mode session. You may also want to query the designer for other interfaces that your application will use. When your application leaves design mode, or on navigation, all designers attached to the MSHTML Editor are released.

To remove an edit designer, you use the IHTMLEditServices::RemoveDesigner method:

// m_pDesigner is a stored IHTMLEditDesigner pointer
pEditServ->RemoveDesigner(m_pDesigner);

Designer Initialization and Support Interfaces for IHTMLEditDesigner

In many circumstances, a designer has an initialization phase that requires access to the hosting application, the document, or elements of the document. A designer might also need to act on the document or host at different points during a design mode session. For example, a spell checking component would need to parse the document to make an initial spelling check and highlight misspelled words when it starts. It would also need mechanisms to add words to the dictionary, load a custom dictionary, and perform other tasks. The IHTMLEditDesigner interface contains only the callback routines needed by the Editor, which means you'll have to accomplish this initialization and continuing work in other ways. Your component could, for instance, implement another interface or interfaces to perform the tasks it needs to do. Your project could define an IMyDesigner interface implemented by your component along with IHTMLEditDesigner. This interface could have whatever methods you need to initialize and carry out the work the designer is to perform. Here's a hypothetical IDL definition for IMyDesigner:

import "mshtml.idl"

[
    object,
    uuid(B46852D9-F442-45A3-833D-007E1F0031FE),
    helpstring("IMyDesigner Interface"),
    pointer_default(unique)
]
interface IMyDesigner : IUnknown
{
    HRESULT SetHost([in] IUnknown* pUnk);
    HRESULT SetDoc([in] IHTMLDocument2* pDoc);
    HRESULT AttachToBody([in] IHTMLElement* pBodyElem);
    HRESULT AddWordToDictionary([in] BSTR bstrWord);
    HRESULT IgnoreWord([in] BSTR bstrWord);
    HRESULT GetSpellingSuggestions([in] IDispatch *pRange,
                                   [out] SAFE_ARRAY *pSuggestions);
    HRESULT Detach();
};

When deactivating a designer, there might be a need to add methods to your interface definition if deactivation affects the browser or hosting application. For example, you might want a method to disable an "Add Word to Dictionary" button or to remove highlighting of spelling errors. In many circumstances, resource deallocation could be accomplished in the designer's implementation of Release, if you are willing to implement Release. This will entail more work, however, than relying on a default release implementation like that provided by ATL's CComObjectRootEx.

Another possibility for adding functionality to a designer is to implement IOleCommandTarget in your designer component. Implementing IOleCommandTarget would enable you to define your own custom commands to execute with IOleCommandTarget::Exec.

MIDL and Mshtml.idl

The example interface in the previous section contains an import directive to bring in Mshtml.idl. However, if you attempt to compile this interface definition using the Mshtml.idl file supplied with the Windows Internet Explorer public headers and libraries, the compilation will fail. At present, the MIDL compiler is unable to process Mshtml.idl when it's imported this way. This failure occurs whenever an IDL imports another IDL that has circular references in its library block. The Mshtml.idl supplied with the Internet Explorer public headers and libraries defines almost all of its interfaces in the library block, and most of them take other MSHTML interfaces as method arguments. Fortunately, there are some strategies you can use to work around the issue.

One very good solution is to create a restructured copy of Mshtml.idl specifically to import into other .idl files. It is very easy: make a copy of Mshtml.idl and remove the library declaration block at the beginning of the library.

// Begin cut
[
    lcid(0x0000),    // lcid =0; locale independent.
    helpstring("Microsoft HTML Object Library"),
    version(4.00),  //  WARNING: This version must match VERSION in Dllreg.cxx.
    uuid(3050f1c5-98b5-11cf-bb82-00aa00bdce0b)
]

library MSHTML
{
    #ifndef _MAC
    importlib("stdole2.tlb");
    #else
    importlib("mstdole.tlb");
    #endif

    import "ocidl.idl";
    #include <olectl.h>
// End cut
// Preserve all definitions in the library block

    // Forward definitions:
    interface IHTMLEventObj;
    enum BEHAVIOR_EVENT;
    enum BEHAVIOR_EVENT_FLAGS;
    .
    .
    .

Now, find the right curly bracket that corresponds to the left curly bracket removed in the cut shown above. It should be at or near the end of the file. One way to find it is to compile mshtml.idl and note the line where the compile error occurs.

You can place this IDL in your include directory for Internet Explorer headers with the name Mshtml.idl. Rename your original Mshtml.idl file to something else to preserve it. This new IDL will provide the interface definitions needed to create components with proper type-checking; it is for import only to other IDL files. It will not compile by itself.

A second work-around is the one taken by the edit designer samples. The samples' source code must be as portable as possible, and their design cannot assume that the system on which they're built has a restructured copy of Mshtml.idl in its include directories. Therefore, it cannot import Mshtml.idl to its .idl file. The sample solves the problem by using IDispatch or IUnknown pointers in its method arguments wherever an interface from Mshtml.idl would appear.

[
    object,
    uuid(B46852D9-F442-45A3-833D-007E1F0031FE),
    helpstring("IEDMessenger Interface"),
    pointer_default(unique)
]
interface IEDMessenger : IDispatch
{
    HRESULT SetReturnValueForMethod([in] DESIGNER_METHOD_FLAG lMethod, 
                                    [in] HRESULT hrMethodReturn);
    HRESULT SetHost([in] IUnknown* pUnk);
    HRESULT SetDoc([in] IDispatch* pDispDoc);
    HRESULT AttachToBody([in] IDispatch* pDispElem);
};

This work-around avoids the need to import Mshtml.idl into an IDL. It has two disadvantages. The first is that the work-around necessitates extra calls to QueryInterface and Release to convert an interface to IUnknown or IDispatch for the method call and then back to the original type in the method implementation.

// Sample preparation for calls to IEDMessenger methods
// Assume m_pEdMess, pDoc, and pBodyElem are valid IEDMessenger, IHTMLDocument2,
// and IHTMLElement interface pointers

IDispatch* pDocDisp;
IDispatch* pBodyElemDisp;

// Query for IDispatch pointers
pDoc->QueryInterface(IID_IDispatch, (void**)&pDocDisp);
pBodyElem->QueryInterface(IID_IDispatch, (void**)&pBodyElemDisp);

// Call IEDMessenger methods
m_pEdMess->SetDoc(pDocDisp);
m_pEdMess->AttachToBody(pBodyElemDisp);

// Release Resources
pDocDisp->Release();
pBodyElemDisp->Release();

// Implementation of IEDMessenger methods
// m_pDoc and m_pBodyElem are member 
// variables of the CEDMessenger class

HRESULT CEDMessenger::GetDoc(IDispatch* pDisp)
{
    HRESULT hr;
    hr = pDisp->QueryInterface(IID_IHTMLDocument2, (void**)&m_pDoc);
    return hr;
}

HRESULT CEDMessenger::AttachToBody(IDispatch* pDisp)
{
    HRESULT hr;
    hr = pDisp->QueryInterface(IID_IHTMLElement, (void**)&m_pBodyElem);
    return hr;
}

A second and more serious disadvantage about this solution is that using IUnknown and IDispatch pointers circumvents good type-checking practices. Whenever practical, create a restructured Mshtml.idl for your development environment so that you can preserve type integrity in your code.

As a variation on this last solution, use VARIANT or VARIANT pointer arguments wherever an interface from Mshtml.idl would appear.

[
    object,
    uuid(B46852D9-F442-45A3-833D-007E1F0031FE),
    helpstring("IAnnotator Interface"),
    pointer_default(unique),
    dual
]
interface IAnnotator : IDispatch
{
    [id(1)] HRESULT AttachAnnotator(VARIANT* pVar);   
};

Notice that this interface is declared as a dual interface in the bracketed attribute block. In other words, IAnnotator is derived from IDispatch instead of IUnknown. When combined with a proper implementation of IDispatch (ATL's IDispatchImpl can provide this support automatically), the methods of this interface will be accessible from script. This interface declaration, like the last work-around, loses its type-checking, and will require slightly more work in the method implementations to extract the interface pointers.

// Implementation of IAnnotator method
// m_pDoc is a member variable of the CAnnotator class

HRESULT IMyDesigner::SetDoc(VARIANT* pVar)
{
    HRESULT hr;
    hr = pVar->pdispVal->QueryInterface(IID_IHTMLDocument2, (void**)&m_pDoc);
    return hr;
}

When the interface is used in C++ by a client to the component, the parameters passed in must be converted to IDispatch pointers and wrapped by the VARIANT data type for the interface method call.

// Sample preparation for calls to IAnnotator methods
// Assume pDoc and pAnno are a valid IHTMLDocument2 and
// IAnnotator interface pointers

VARIANT* pVar;

// Query for IDispatch pointer
pDoc->QueryInterface(IID_IDispatch, (void**)&pVar->pdispVal);

// Call IAnnotator method
pAnno->AttachAnnotator(pVar);

// Release Resources
pVar->pdispVal->Release();

In spite of these issues, this interface gains considerably in portability because it exposes the component for use on a Web page or in another scripting environment.

Conclusion

This tutorial has given the basic information you need to implement the IHTMLEditDesigner interface. The next tutorial will demonstrate a more realistic application of edit designers, an annotation tool that allows you to edit comments embedded in a document.

Conceptual

Introduction to MSHTML Editing

Activating the MSHTML Editor

Modifying Documents in Edit Mode

Using the MSHTML Editor's Extra Features

Using Editing Glyphs

Implementing IHTMLEditHost

About Edit Designers

Implementing Edit Designers 2: The Annotator Sample