Build a Managed BHO and Plug into the Browser

来源:互联网 发布:日本传统音乐知乎 编辑:程序博客网 时间:2024/06/09 14:00
With the amount of time we spend touring the Internet for information, enhancing the browsing experience has never been more important. Internet Explorer has an extensibility model supporting a variety of browser plug-ins. You have probably seen and used them yourself: the Google toolbar, the Babelfish Translator, or the ever-popular variety of popup blockers. Plug-ins such as these may come in the form of basic Browser Helper Objects (BHOs) or other browser extensions such as Explorer Bars and docking Tool Bands. In any case, they enrich the browsing experience in some way by integrating otherwise cumbersome or elusive functionality. They do this by implementing a required set of predefined COM interfaces and by communicating with interfaces exposed by IE's Web browser object model.

Many a BHO has been implemented using lightweight ATL components in C++, but for those who have fully embraced C# and spend the majority of time writing managed code, the thought of revisiting COM and ATL probably seems a lousy idea. Fortunately, the COM interoperability support provided by the .NET Framework gives us a way to build 100 percent managed components and to control how they are exposed to COM. In this article I will show you how to create a managed BHO that responds to browser events and discuss the interoperability features that make it possible.

Here We Are...Talking COM Again

Prior to .NET I fancied myself a pretty good COM developer, with years of experience developing components with C++, ATL, MFC and VB. For that reason, COM interoperability was a subject near and dear to me in the early days of .NET. But, as time passed, more and more of my .NET development became pure, and COM became a distant memory. But, every once in a great while, for example, while I was porting a legacy ATL BHO to C#, I find myself summoning from past COM experiences.

In the case of the BHO, I had to gather interface identifiers to import existing COM interfaces and review the rules of reference counting ...yet again. The simplest form of BHO is a component that, once registered, is loaded by IE and can both communicate with IE's object model and hook into its useful browsing events. A BHO does not need to expose a visible user interface to be effective. IE loads any registered BHOs and provides them with reference to its container site (the Web browser class in this case), and from there BHOs decide what events to subscribe to and what parts of the IE object model to communicate with.

This requires two-way communication between managed and unmanaged code. In the sample application the managed component, Observer.dll, is registered so COM clients can consume it, and the .NET runtime dynamically generates a COM Callable Wrapper (CCW) that exposes COM interfaces to those clients. When you set the project properties option to register the assembly for COM interoperability, the IDE invokes regasm.exe to insert registry settings for the managed component, but it also invokes tlbexp.exe to generate a type library for the assembly. In fact the CCW is generated at runtime, but we can inspect the type library to see how assembly types will be marshaled to COM clients. The CCW exposes IUnknown and IDispatch interfaces for the assembly, in addition to any coclass and interface definitions it encapsulates (see Figure 1).

The sample application includes a BHO
Figure 1: The sample application includes a BHO that runs within each IE process and communicates with a remote Webservice. The Web service is also invoked by a singleton Windows client, loaded through the IE toolbar, to display a list of active session browsing statistics.

Since the Observer assembly will be using the IE object model, a reference to the COM component SHDocVw.dll is added to the project. This generates a Runtime Callable Wrapper (RCW) that gives us access from managed code to IE's Web browser class and related object and event interfaces (also shown in Figure 1). In the Observer project the generated interop assembly (RCW) is located in Interop.SHDocVw.dll.

For this article, I'll now move past the basics of COM and .NET wrapper classes and focus on the specific implementationrequirements of a BHO along with some extended features of COM interoperability that are leveraged to implement its functionality.       

You're Browsing...Who's Listening?

BHOs, like other browser extensions, must be registered in a particular section of the Windows registry. If you double-click on the observerbho.reg file supplied with the sample Observer project, you will see the following entry was added to the registry (to see the registry, run regedit.exe):

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\{0CD00297-9A19-4698-AEF1-682FBE9FE88D}

Each subkey beneath the Browser Helper Objects key is identified by a GUID that matches a registered BHO component class. Of course, that means that the component must also be registered for COM, and you can test this by compiling the Observer project, which creates the following registry entry:

HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0CD00297-9A19-4698-AEF1-682FBE9FE88D}

This CLSID key provides information about the programmatic identifier and class loader required to load the BHO component when a client invokes it: Observer.BrowserMonitor and mscoree.dll, respectively. Once loaded, IE will look for the component to have implemented the IObjectWithSite COM interface. This interface is a lightweight alternative to IOleObject, for non-embedded objects to communicate with their host container. But, to expose this predefined COM interface from managed code, we must define the interface as shown in Figure 2.

[ComImport(), Guid("fc4801a3-2ba9-11cf-a229-00aa003d7352")]

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]

public interface IObjectWithSite

{

void SetSite([In,

MarshalAs(UnmanagedType.IUnknown)]

object pUnkSite);

void GetSite(ref Guid riid,

[MarshalAs(UnmanagedType.IUnknown)]

out object ppvSite);

}

Figure 2: Importing a COM interface means providing an exact definition with matching GUID. IObjectWithSite is an externally defined COM interface shown here defined in C#.

As with COM objects, COM interfaces are also identified in the Windows registry by a GUID. In order for IE to gain access to the observer BHO through its IObjectWithSite interface, the interface must be defined with this GUID. For managed components, rather than including a header as we would in a C++ application, we can define the interface in C# and use the ComImportAttribute to mark the interface as externally defined. The interface must define interface members exactly as they appear in the actual interface definition, in the same order. When the ComImportAttribute is applied, the compiler also expects a GuidAttribute to be present to specify a matching GUID. In this case, the GUID for IObjectWithSite as it is registered.

NOTE: When ComImportAttribute is applied to a class definition, the class need not include members since those will be supplied by the CCW. In this respect, ComImportAttribute allows us to provide friendly names to externally defined COM objects.

By default, objects are marshaled as COM variants, but the methods exposed by IObjectWithSite define an IUnknown interface pointer that is passed to SetSite and an interface pointer returned by GetSite in the second parameter:

HRESULT SetSite(IUnknown* pUnkSite);
HRESULT GetSite(REFIID riid, void** ppvSite);

The MarshalAsAttribute can be used to tell the .NET Framework how to marshal types between the CCW and the COM client that invokes it. As shown in Figure 2 the UnmanagedType enumeration includes an IUnknown element that is applied for this purpose.      

A BHO COM Wrapper - Made to Order

Now that we have the interface well defined, let's take a look at the BHO class that implements it, BrowserMonitor. A partial view of BrowserMonitor is shown in Figure 3.

[GuidAttribute("181C179B-7CC9-4457-8C1D-4B45E7C8589D")]

[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)]

public interface IObserver

{

}

[ClassInterfaceAttribute(ClassInterfaceType.None)]

[GuidAttribute("0CD00297-9A19-4698-AEF1-682FBE9FE88D")]

[ProgIdAttribute("Observer.BrowserMonitor")]

public class BrowserMonitor: IObserver, IObjectWithSite

{

public void SetSite(object pUnkSite)

{

if (m_pIWebBrowser2!=null)

Release();

if (pUnkSite==null)

return;

m_pIWebBrowser2 =

pUnkSite as SHDocVw.IWebBrowser2;

string sHostName = m_pIWebBrowser2.FullName;

if (!(sHostName.ToUpper().EndsWith("IEXPLORE.EXE")))

{

Release();

return;

}

m_hwndBrowser = m_pIWebBrowser2.HWND;

SHDocVw.DWebBrowserEvents2_Eventev =

m_pIWebBrowser2as SHDocVw.DWebBrowserEvents2_Event ;

if (ev != null)

{

ev.BeforeNavigate2 += new

SHDocVw.DWebBrowserEvents2_BeforeNavigate2EventHandler(

BeforeNavigate2);

ev.DocumentComplete += new

SHDocVw.DWebBrowserEvents2_DocumentCompleteEventHandler(

DocumentComplete);

ev.OnQuit += new

SHDocVw.DWebBrowserEvents2_OnQuitEventHandler(OnQuit);

ev.NavigateComplete2 += new

SHDocVw.DWebBrowserEvents2_NavigateComplete2EventHandler(

NavigateComplete2);

}

else

{

Release();

return;

}

}

public void GetSite(ref System.Guid riid,

out object ppvSite)

{

ppvSite=null;

if (m_pIWebBrowser2 !=null)

{

IntPtr pSite = IntPtr.Zero;

IntPtr pUnk =

Marshal.GetIUnknownForObject(m_pIWebBrowser2);

Marshal.QueryInterface(pUnk,ref riid,out pSite);

Marshal.Release(pUnk);

Marshal.Release(pUnk);

if (!pSite.Equals(IntPtr.Zero))

{

ppvSite = pSite;

}

else

{

Release();

Marshal.ThrowExceptionForHR(E_NOINTERFACE);

}

}

else

{

Release();

Marshal.ThrowExceptionForHR(E_FAIL);

}

}

// more Observer methods

}

Figure 3: A partial listing of the BrowserMonitor BHO object (defined in the Observer assembly) showing the implementation of IObjectWithSite members.

The BrowserMonitor will be registered as a coclass with a GUID and programmatic identifier (ProgID), so a few attributes have been applied to the class tailoring how the runtime exposes it to COM. By default, a new GUID will be generated each time we compile and register the Observer assembly's types, so the GuidAttribute is applied to ensure that the same GUID is always used. Following COM versioning rules, this GUID need only be altered when the object's interface is altered or implementation is drastically altered. The GUID for BrowserMonitor matches the BHO registry setting discussed earlier.

By default, the runtime generates a CCW with a default class interface for each publicly exposed class. This behavior is consistent with how Visual Basic 6.0 (and earlier) exposed types to COM; however, this means that all public methods are aggregated and exposed in a single default interface. It is traditionally preferred to create well-defined interfaces for each coclass andgroupmembers in the appropriate interface that can be separately versioned and accessed through QueryInterface.

To suppress the automatic generation of a class interface the ClassInterfaceAttribute is applied with enum parameter ClassInterfaceType.None. In addition, although symbolic in this case, I created an empty custom dual interface for the class called IObserver and apply this interface to the BrowserMonitor in lieu of the class interface. The InterfaceTypeAttribute makes it possible to create IDispatch, IUnknown, or dual interfaces, the latter of which is preferred. The ProgIdAttribute is used to override the default generated progid which would normally be generated assemblyname.classname. In the case of this assembly, the default actually matches the attribute setting, but is kept for good measure.

So, now that the Observer class is registered as a BHO, and the class properly implements the IObjectWithSite interface, IE will load the BHO and invoke SetSite to initiate communications. Next let's see what interesting things we can do with the BHO.      

Hooking the Web Browser

After a BHO is loaded you have a golden opportunity during SetSite() to store a reference to the browser instance that loaded it and subscribe to useful events. When we implement SetSite and GetSite, we must also consider the rules we're asked to follow, per the interface definition.

The implementation of SetSite should store a reference to the browser site, unless a null reference is passed, which means we are being asked to release our hold on a previously stored reference. The first few lines of the SetSite method shown in Figure 2 demonstrates this. Once we have established there is a valid reference to the Web browser, the following code casts that reference to the IWebBrowser2 interface of the object:

m_pIWebBrowser2 = pUnkSite as SHDocVw.IWebBrowser2;

Remember that when we hold a reference to a COM object, we are supposed to increment the reference count? Well, the CCW handles this when we assign the reference as shown. In fact, under the hood, this safe cast to IWebBrowser2 using the as keyword invokes the appropriate calls to QueryInterface to find the IWebBrowser2 interface (if present) exposed by the pUnkSite parameter.

SetSite requires us to return an S_OK HRESULT in all cases (which is why we don't throw any exceptions in this method), but GetSite requires additional logic. GetSite is called to retrieve the last site passed to SetSite, which we should return if we have stored a valid reference. However, if the first parameter to GetSite requests an interface identifier that is not supported on the site reference, an E_NOINTERFACE HRESULT should be returned. If we are holding a null site reference, GetSite should return E_FAIL.

System.Runtime.InteropServices.Marshal is a utility class supplying methods to help us with COM interoperability. Throwing a ComException will return E_FAIL to a COM client, however ThrowExceptionForHR supports the return of any valid HRESULT. I copied the definitions for two predefined HRESULT constants in the BrowserMonitor class for this purpose:

const int E_FAIL = unchecked((int)0x80004005);const int E_NOINTERFACE = unchecked((int)0x80004002);
The Marshal class also supplies a QueryInterface method that is used in GetSite to retrieve the requested interface reference. In order to invoke QueryInterface, the site object (IWebBrowser2 type) had to be cast to IUnknown, as shown in Figure 3. Be warned, you must know your utility functions well. In this case, since GetIUnknownForObject adds a reference to the site object, as does QueryInterface, I decrement the reference count twice to release those additional references before the method call is over.

If you take a look at the interop assembly, Interop.SHDocVw.dll, you will see many interesting types and event interfaces that could be useful to communicate with.

IWebBrowser2 is the primary interface implemented by the Web browser control contained in SHDocVw.dll (see Figure 1). It exposes properties and methods for browser navigation to interact with UI components such as the address bar and to instruct the browser to close. The same Web browser control also exposes a core connection point interface, DWebBrowserEvents2, that includes events related to navigation, UI activities, file downloads and printing.

As shown in Figure 3, SetSite checks the FullName property of the Web browser control to see if the calling container is Internet Explorer (iexplore.exe). This check ensures that we suppress BHO functionality for Windows Explorer, for example, which also loads registered BHOs into each instance. This BHO is otherwise more interested in navigation events, so before SetSite completes we grab the event interface by casting the site object to DWebBrowserEvents2.

The connection point mechanism exposed by COM objects is exposed via delegates in managed code through the RCW. Figure 3 shows how to use the DWebBrowserEvents2_Event class to subscribe to a few key navigation events (BeforeNavigate2, DocumentComplete and NavigateComplete2) and the Quit event to detect when the browser session is closing.

The NavigateComplete2 event is issued once navigation to a requested URL has been complete; however, the page may take time to load, so I hook the DocumentComplete event that notifies listeners that the current page has completed loading. BeforeNavigate2 is issued prior to navigation when a user has requested a new page. The sample BHO uses these events to track navigation URLs and time spent on each URL. This information is sent to the Web service forstorage (see Figure 1).

One interesting point to note is that the Web service proxy that was generated for the BHO project when I added a Web reference to BrowsingServices becomes part of the CCW when the BHO is exposed to COM. This is because the proxy is a public class. To hide this object from the CCW I applied the ComVisibleAttribute to the proxy class. The drawback of this, of course, is that each time I generate a new proxy I have to re-apply the attribute. Similarly, this ComVisibleAttribute can be applied to any public type of member that you want omitted from the runtime-generated COM wrapper.      

A BHO and Then Some

As Figure 1 demonstrates, the sample application is more than just a BHO, although that is the heart of it all and the focus of this article. Rather than keep things simple, I created a solution around the BHO that includes Web services and a singleton Windows client. The BHO uses Web services to remotely store active session browsing data. The Windows client (really, a smart client of sorts) leverages the same Web services to display a list of active session browsing data. In fact, the installation will also add a toolbar to the browser so that you can launch the Windows client from any open browser.

Now you should have a deeper understanding of how BHOs operate, and the power they are able to harness through the container site's object model. In addition, with our short and sweet COM review, you now know how to import COM class and interfaces, control marshalling, and customize the way your components are exposed to COM

 

reference URL:http://www.15seconds.com/issue/040331.htm