Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Create Binary Behaviors for IE with .NET


Create Binary Behaviors for IE with .NET

Starting with version 5.0, Internet Explorer (IE) has provided an ability to customize the way HTML elements behave and display. In fact, it is possible to define completely custom elements with their own unique rendering behavior. For example, if you desire 2D drawings or 3D graphics in your HTML page, writing your own binary behavior gives you the power of GDI+ or DirectX for controlling the rendering of your elements. IE exposes its own device context and lets you draw directly on it.

IE exposes this capability through a set of COM interfaces, and developers of binary behavior have become familiar with such interfaces as IElementBehavior and IHTMLPainter for hooking up their behavior objects to IE. With the advent of .NET and COM Interop, it is now possible to implement binary behavior entirely in .NET — and with much greater ease. .NET reduces development hassle particularly through the transparency of COM Interop, which reduces the number of interfaces that a developer must explicitly implement. (Event handling is made especially intuitive.) Compared with the ATL-based sample in the Platform SDK, writing a .NET binary behavior is a snap.

In the new world of .NET web development, why would anyone feel compelled to write IE binary behaviors? There are at least two reasons. First, there is legacy code. In a typical example, an HTML application is already deployed, but then requires 2D drawings. Second, HTML is not going away, and IE provides a powerful and easy-to-use rendering engine. IE (MSHTML) may continue to be hosted in other applications, especially if element rendering is easily customizable through binary behaviors. .NET has so simplified the writing of binary behaviors that we may actually see an increased use of this IE feature.

The Interfaces

In terms of COM, there are two components involved in writing a binary behavior: the behavior's class factory and the element behavior itself. The factory involves implementing two interfaces: IElementBehaviorFactory and IElementNamespaceFactory. IElementBehaviorFactory provides the FindBehavior() method, which hands IE an instantiation of the element behavior. IElementNamespaceFactory allows the factory to introduce new element names into the namespace of your HTML page through the create() method. That makes only two methods to implement for the factory.

The behavior component must also implement just two interfaces: IElementBehavior and IHTMLPainter. IElementBehavior has three methods to implement, providing the basic functionality of a binary behavior (without rendering): Init(), Detach(), and Notify(). Init() is called during initialization of the behavior object and is generally used to cache references back to interfaces implemented by IE (such as IElementBehaviorSite). The method Notify() is called when the element is ready for use (i.e., when the document is loaded). At this point, a reference to the element can be obtained, event-handling can be attached, and rendering can be triggered.

The second essential interface, IHTMLPainter, controls element rendering through four methods, though only these two are significant: GetPainterInfo() and Draw(). GetPainterInfo() allows the behavior to specify how rendering will be performed. This is where the drawing engine (e.g., GDI+, DirectX) is specified, as well as various rendering flags (e.g., transparency, clipping, or transformation). The real work of rendering the element's behavior is done in Draw(). Draw() is handed an HDC (device context), which can be used, for example, by GDI+ to define a Graphics object for vector drawing. The two remaining methods, onresize() and HitTestPoint(), are trivial for most basic rendering behaviors. onresize() should just invalidate the element's region, triggering repainting. HitTestPoint() is used only if you desire to identify where specifically in your element an event (e.g., mouse click) has occurred — in case some areas of your element should be distinguished from others. (You must set the HTMLPAINTER_HITTEST in GetPainterInfo() for this method to be called.)

So, all in all there are only four interfaces with five or six methods to implement, most of which are boiler-plate code. Well, there is one last interface, which is peripheral to the functionality of the behavior: IObjectSafety. The behavior's class factory should implement IObjectSafety in order to declare to IE that this component is safe to run. It has but two methods, which at most set some flags: GetInterfaceSafetyOptions() and SetInterfaceSafetyOptions(). So much is well known to writers of binary behaviors.

Defining the Interfaces for .NET

Most of the interfaces we need are available in the primary interop assembly for MSHTML, which comes with Visual Studio .NET. Just add an assembly reference to the Microsoft.mshtml assembly, and the many COM interfaces of MSHTML are accessible as .NET interfaces. At this point, the above factory interfaces, as well as IElementBehavior and IHTMLPainter, are ready to use.

IObjectSafety, on the other hand, is not supplied in the PIA and must be manually included for the sake of your behavior factory. (The behavior object itself does not need this.) The interface looks like this:

// IObjectSafety interface.
[
ComImport,
Guid("CB5BDC81-93C1-11CF-8F20-00805F2CD064"),
InterfaceType(
    ComInterfaceType.InterfaceIsIUnknown)
]
public interface IObjectSafety
{
    int GetInterfaceSafetyOptions(
         ref Guid riid, 
         out int pdwSupportedOptions,
         out int pdwEnabledOptions);
    int SetInterfaceSafetyOptions(
          ref Guid riid, 
          int dwOptionsSetMaks,
          int dwEnabledOptions);
}

Notice that the methods are defined so as to return their HRESULT, rather than transforming errors into exceptions. As a result, we will need the PreserveSig attribute on our methods; for example:

// IObjectSafety method.
[return:MarshalAs(UnmanagedType.Error)]
[PreserveSig]
public int GetInterfaceSafetyOptions(
    ref Guid riid, 
    out int pdwSupportedOptions, 
    out int pdwEnabledOptions)
{
    // INTERFACESAFE_FOR_UNTRUSTED_CALLER     // and _DATA.
    pdwSupportedOptions = 0x00000001 | 0x00000002;
    pdwEnabledOptions = 0x00000001 | 0x00000002;
    return 0; // S_OK.
}

So far, so good. At this point, it would appear that we have all the interfaces in hand, but this is not quite so. It turns out that two interfaces defined in the Microsoft.mshtml PIA need to be manually adjusted so as to marshal method parameters properly. For some method parameters, we need to preserve the COM-based semantics, which might not translate well into the expected .NET type. For example, when the semantics of the COM type are overloaded by using NULL as a meaningful value, we will have trouble if we marshal this as an Object reference. This is where the transparency of Interop can be tricky and requires special attention.

In particular, the IElementBehavior interface is faulty in the Notify() method. The default signature is:

void Notify(System.Int32 lEvent, ref 			System.Object pVar);

The second parameter, though, corresponds to a pointer to VARIANT and so may be NULL. When Notify() is called with NULL, an exception will occur. To adjust for these semantics, the methods must be defined this way:

void Notify(
    System.Int32 lEvent, 
    System.IntPtr pVar); 	// Not: System.Object pVar.

This is a classic scenario in which we need to use IntPtr in order to allow for the original NULL semantics of the pointer. (See .NET and COM, by Adam Nathan, Pearson Education 2002, p. 264.)

The corrected IElementBehavior interface is, therefore, as follows:

// Corrected IElementBehavior.
[
ComImport,
Guid("3050F425-98B5-11CF-BB82-00AA00BDCE0B"),
InterfaceType(
    ComInterfaceType.InterfaceIsIUnknown)
]
public interface IElementBehavior
{
    void Init(
        mshtml.IElementBehaviorSite 
        pBehaviorSite);
    void Notify(
        System.Int32 lEvent, 
        System.IntPtr pVar); // CORRECTED.
    void Detach();
}

We need to place this and other corrected interfaces in a new namespace (such as mshtml2) in order to distinguish them from their synonyms in Microsoft.mshtml. Since we will be using interfaces from both mshtml and mshtml2 namespaces, we will need to qualify our interfaces with the new namespace explicitly.

This correction now affects IElementBehaviorFactory, whose FindBehavior() method returns an IElementBehavior. Since we want this to return a corrected IElementBehavior and not the mshtml.IElementBehavior, we need to adjust this interface as well (trivially):

// Corrected IElementBehaviorFactory.
[
ComImport,
Guid("3050F429-98B5-11CF-BB82-00AA00BDCE0B"),
InterfaceType(
    ComInterfaceType.InterfaceIsIUnknown)
]
public interface IElementBehaviorFactory
{
    // CORRECTED METHOD: Return corrected     // interface.
    mshtml2.IElementBehavior 
        FindBehavior(System.String bstrBehavior,
                     System.String bstrBehaviorUrl,
                  mshtml.IElement 		   BehaviorSitepSite);
}

Finally, we need to make a similar adjustment in the IHTMLPainter interface. The Draw() method is given to us by mshtml as:

void Draw(mshtml.tagRECT rcBounds, 
        mshtml.tagRECT rcUpdate, 
        System.Int32 lDrawFlags,
        ref mshtml._RemotableHandle hdc, 
        System.IntPtr pvDrawObject);

However, the handle to device context (hdc) provided by IE (and as used in GDI+) is ultimately just a pointer. We use the HDC to create a Graphics object using the static method Graphics.FromHdc(System.IntPtr hdc). This fact gives away the answer for the correction to make. We simply need to revise this parameter type to IntPtr. The newly defined interface now appears thus:

// Corrected IHTMLPainter.
[
ComImport,
Guid("3050F6A6-98B5-11CF-BB82-00AA00BDCE0B"),
InterfaceType(
    ComInterfaceType.InterfaceIsIUnknown)
]
public interface IHTMLPainter
{
    // CORRECTED METHOD: Use IntPtr, 
    // not ref _RemotableHandle hdc.
    void Draw(mshtml.tagRECT rcBounds, 
              mshtml.tagRECT rcUpdate,
              System.Int32 lDrawFlags, 
              System.IntPtr hdc,
              System.IntPtr pvDrawObject);
    void onresize(mshtml.tagSIZE size);
    void GetPainterInfo(
        out _HTML_PAINTER_INFO pInfo);
    void HitTestPoint(
        mshtml.tagPOINT pt, 
        out System.Int32 pbHit,
        out System.Int32 plPartID);
}

As this interface is not referred to in other mshtml interfaces we will need, this is the last correction we need. We are now ready to implement.

For all these interfaces that we have just defined, the Guid attribute provides the COM interface ID (IID) expected for the given interface. That is, these GUIDs are not arbitrary, but are the IIDs used when IE calls QueryInterface().

Implementing the Behavior

As an example, let us implement a simple line-drawing element, which uses the vector graphics of GDI+ to draw a line segment between two points given by left/top pixel locations. Making the line element a binary behavior means that the drawing takes place when IE renders the HTML document, giving us control to draw on a transparent background and to preserve z-index ordering. To do this, we define a Line class that implements our two mshtml2 interfaces, IElementBehavior and IHTMLPainter. Let us design our line element to have four attributes (x1, y1, x2, y2) to describe the two end points of the line. We will also provide a single, string-based property (coords) by which we can specify all four coordinates in one string. For variety, let us also provide stroke width and stroke color attributes. Our Line class, therefore, will have to maintain state information for all these attributes, in addition to caching some housekeeping information for drawing and communicating with IE. The class starts out as in Listing 1.

After the element constructor is called, Init() is executed, which caches the behavior site interface, allowing us to call IE for more information. At various points during the loading of the HTML document, IE calls Notify(), each time with a different lEvent flag. (Actually, only calls with the FIRST and DOCUMENTREADY event flags occur in this example.) The crucial call for us is when the document has been loaded, when lEvent equals BEHAVIOREVENT_DOCUMENTREADY. At this point we know that the document, element, and parent windows are available, and references to these can be saved for future use. Now is also the time we can initialize our element with any default attribute values of our choosing. In our case, we opt to force our line element to have absolute positioning style, which will save us the trouble of setting this style in our HTML to enable drawing to absolute pixel locations.

Most importantly, we can now hook up event handlers for all the events to which we wish our element to respond. For more interactive elements, one might wish to respond to onclick, onmouseenter, or onmouseleave — note the many events offered by HTMLElementEvents_Event. In our case, we only need onpropertychange, which will allow us to accept DHTML-based changes to the attribute values of our line. Since the COM-callable wrapper (CCW) provides default implementations for IDispatch and IEventSink, we are saved this hassle and can simply hook up event handlers in the C# way.

Keep in mind the difference between HTML attributes and object properties. Element attributes are maintained in the HTML DOM. When script alters the state of attribute data in the DOM, our underlying object is also informed via the onpropertychange event. Properties, on the other hand, are maintained directly by the object. Hence, attributes are available in HTML tags, while properties are available only in script. In our example, Line captures the initial attribute values (from the HTML tag) in Notify() and further changes in HTMLElement_OnPropertyChange(). The property coords, however, is simply implemented as a C# property. (When end points are modified through the coords property, the new coordinates must be copied into the x1, y1, x2, and y2 attributes.) COM Interop handles the work of implementing automation (IDispatch) through its CCW. This difference presents a design decision: Which qualities of your element should be available in the HTML tag, and which only in script?

Once the element has been notified that the document is loaded, it is time to initiate drawing by informing IE that the element is in need of repainting. Calling InvalidateRegion() in the paint site interface provided by IE will do just this. (It would also be possible to call InvalidateRect(), but this method expects a ref to mshtml.tagRECT, which cannot be null. Yet, we need to pass in NULL to invalidate the entire element. We could redefine this interface to provide the InvalidateRect() signature we need, but InvalidateRegion() will also work.) Hence, we call this method every time we change the line drawing, in DoLineDraw(), as seen in Listing 2. When the element region is invalidated, IE will begin to make calls to Draw() on mshtml2.IHTMLPainter.

IE will, in fact, call Draw() more than once in drawing the element, as it paints the element rectangle in successive horizontal bands. For this reason, it is best to prepare an off-screen bitmap image before element invalidation, so that successive calls to Draw() deal with an unchanging graphic. When IE calls Draw(), the drawing requirements dictated by IE are obtained through GetDrawInfo(), and any necessary image transformation and clipping can then be applied before rendering with Graphics.DrawImage().

The interesting work of actually constructing the line segment is done in our Compose() and UpdateElement() methods. Compose() creates the bitmap (System.Drawing.Bitmap) representation of the line, where the end points are made relative to the bounds of the bitmap rectangle. UpdateElement() then sets the element dimensions to suit this bitmap and locates the element at the correct pixel location. The element and the bitmap are made slightly wider (by xoffset) and taller (by yoffset) than required by the minimum-size rectangle enclosing the line, because we must provide sufficient room for the stroke width of a vertical or horizontal line (right on the edge).

Of particular importance is the need to free the bitmap on each draw using the Dispose() method. Otherwise, the garbage collector permits large, unused bitmaps to accumulate, and with each garbage collection even larger accumulations are allowed.

Adding More Behaviors

It is easy to add behaviors in this component. First, a behavior class must be defined that implements mshtml2.IElementBehavior and mshtml2.IHTMLPainter. A new GUID is given this class to identify it for COM. Second, the factory class must supply the element namespace with a tag string naming the custom element and return a reference to an instantiation of the behavior. The tag string (element name) is provided via IElementNamespaceFactory.create(), which informs an HTML document how to refer to the element (in the namespace to which the factory will be assigned). In order to return the behavior reference, the factory method FindBehavior() merely constructs a behavior of the requested name and returns its reference. The code sample illustrates these steps in its second behavior, which can draw an ellipse.

Sample HTML Page

Included with the source code is a sample HTML file, which employs our binary behaviors to draw a tethered ellipse around any chosen paragraph (see Figure 1). The <object> element instantiates the binary behavior class factory, using the factory's class ID as defined in our C# code. The <?import> tag directs HTML processing to query the behavior class factory for the implementation of any element whose name is in the specified namespace (behaviors). We can now introduce our custom elements by the names we specified in C#, provided we prefix them with the namespace designation (e.g., <behaviors:line>). Two custom elements are given, drawing a colored ellipse with a line segment. Each paragraph in the document responds to its onmouseover event (when the mouse hovers over the paragraph) and calls OnMouseOver(). The binary behaviors are then invoked to shape the colored ellipse around the given paragraph, with the colored line segment tethering the ellipse to the origin of the client window.

Conclusion

.NET COM Interop takes the headache out of getting binary behaviors to work in Internet Explorer. Several standard interfaces are automatically implemented by the default CCW. Event handling and properties are transparently handled in the normal C# way. Some interfaces supplied with the MSHTML primary interop assembly needs manual adjustment, but the guts of the behavior can be implemented easily in C#. Binary behaviors are another case where .NET makes working with COM easier.


Scobie P. Smith is a software consultant for Infinitiv, a research and development firm providing custom development services and offering specialties in .NET, linguistic processing, and data mining. He is currently completing a doctorate from Harvard in ancient Near Eastern languages. Contact Scobie at [email protected].


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.