How to Bring up a Find Dialog in the WebBrowser Control using Managed C++

Figure 1 - Find Dialog for the IE WebBrowser Control
 
Introduction
 
Although this may sound like a trivial thing to do, bringing up the FindDialog in a WebBrowser control requires COM (short for complicated). The reason that it requires COM is because theWebBrowser control shipped with Visual Studio.NET 2005 is really a wrapper around the existing internet explorer on your machine. Microsoft didn't change IE to be pure .NET, so you are kind of stuck with accessing the control with COM Callable Wrappers (yuck). Much of this article was deciphered from a Microsoft knowledge base article on the topic of implementing the finddialog in Managed C++. The Microsoft knowledge base article only applies to the previous version of .NET. (2003). This article will show you how to access the FindDialog using the new features in .NET 2005.
 
Marshaling through the IOleCommandTarget
 
In order to access various functions in Internet Explorer, you need to pick the correct COM Interface with the method you want to invoke.  You then need to marshal that interface through .NET into your application. The find dialog inside the WebBrowser control is accessed from an interface called IOleCommandTarget by calling the Exec method on this interface.
 
The COM wrapper for this interface and the various structures required are shown in listing 1 below. The first few definitions in listing 1 are marshaled structures that you need to pass into the Exec method of the IOleCommandTarget interface. The last definition is the wrapper around the IOleCommandTarget itself.  It is renamed to IOleCommandTargetWrapper because C++ seems to get all of these redefinition compilation errors if you name it IOleCommandTarget. (C# doesn't have these strange issues). The Exec method takes a GUID representing the command group, a command id, an optional execution parameter, an object in and an object out. 
 
Note that the GUID is passed in with a interior_ptr. What is an interior pointer (interior_ptr)? When you are working with managed objects, they are managed on the heap by the garbage collector. Although the garbage collector serves as an internal memory manager for all of your objects, it tends to move the objects around on the heap. Therefore a static unmanaged reference to the object may not be valid somewhere along the road. An interior_ptr is a type of reference that dynamically changes as your object moves around, so you can always be sure that this "smart pointer" is always pointing to your object. A similar type of reference is the tracking reference (%). This allows you to pass values by reference so that the pointer pointing to them is also tracked on the heap as the garbage collector moves them. You can think of the tracking reference (%) as a ref in C#. Managed objects in and out of the Exec interface method are marshaled using tracking references.
 
Listing 1 - Marshaling the IOleCommandTarget COM Interface
  1. #pragma once  
  2.   
  3. using namespace System::Runtime::InteropServices;  
  4. using namespace System;  
  5.   
  6. public ref struct OLECMDTEXTWrapper  
  7. {  
  8.   public:  
  9.   
  10.       unsigned int cmdtextf;  
  11.       unsigned int cwActual;  
  12.       unsigned int cwBuf;  
  13.       char rgwz;  
  14. };  
  15. public ref struct OLECMDWrapper  
  16. {  
  17.   public:  
  18.       long cmdID;  
  19.       System::UInt64 cmdf;  
  20. };  
  21.       // Constants for the commands that are named earlier  
  22.       public enum class MiscCommandTargetWrapper  
  23.       {  
  24.             Find = 1,  
  25.             ViewSource,  
  26.             Options  
  27.       };  
  28.   
  29. // Interop definition for IOleCommandTarget. You need this attribute to marshal the wrapper as a COM   
  30. // interface  
  31. [ComImport,   
  32. Guid("b722bccb-4e68-101b-a2bc-00aa00404770"),  
  33. InterfaceType(ComInterfaceType::InterfaceIsIUnknown)]  
  34. public interface class IOleCommandTargetWrapper  
  35. {   
  36.       //IMPORTANT: The order of the methods is important here.   
  37.       // Since you are doing early binding, the order of the methods  
  38.       //here MUST match the order of their vtable layout (which is determined  
  39.       //by their layout in IDL). The interop calls key off the vtable ordering,  
  40.       //not off the symbolic names, and therefore, if you switched these method  
  41.       //declarations and tried to call the Exec() function on an IOleCommandTarget  
  42.       //interface from your application, it would translate into a call to  
  43.       //QueryStatus() instead.  
  44.   
  45.       void QueryStatus(Guid* pguidCmdGroup, System::UInt32 cCmds,   
  46.      [MarshalAs(UnmanagedType::LPArray, SizeParamIndex=1)] interior_ptr<OLECMD> prgCmds,  
  47.             interior_ptr<OLECMDTEXT> CmdText);  
  48.       void Exec(interior_ptr<System::Guid> pguidCmdGroup, long nCmdId, long nCmdExecOpt, Object^% pvaIn,  
  49.                        Object^% pvaOut);  
  50.   
  51. };   
Implementing the IOleCommandTarget through the WebBrowser Control
 
Now that we have wrapped the IOleCommandTarget COM interface, we can access its powerful set of functions. We need to know the command guid in order to access the find dialog, and luckily this is provided to us in the Microsoft KnowledgeBase. We also take advantage of the enumeration for miscellaneous command target options (MiscCommandTargetWrapper) to specifically call upon the find dialog. The command execute option is the default (zero). The objects in and out of the function are created simply as place holders and use a managed object created with a default constructor.
 
As seen in Listing 2, we access the IOleCommandTarget COM interface through the IHTMLDocument2 COM interface. In order to access the IHTMLDocument2, we need to go through the DomDocument property, which is a property for the unmanaged interface of the HtmlDocument in the WebBrowser. 
 
Listing 2 - Bringing up the Find Dialog from the WebBrowser Control
  1. System::Object^ o = gcnew System::Object();  
  2. Guid *cmdGuid = new Guid("ED016940-BD5B-11CF-BA4E-00C04FD70816");  
  3. ((IOleCommandTargetWrapper^)((mshtml::IHTMLDocument2^)webBrowser1->Document->DomDocument))  
  4.       ->Exec(cmdGuid, (System::UInt32)MiscCommandTargetWrapper::Find, 0, o, o);   
Note: Accessing the COM interfaces inside of the DomDocument, requires a reference to the mshtml NET assembly inside the project shown in figure 2.
 

Figure 2 - Adding the mshtml reference to your project.

One last thing you may need to do. I added a class interface attribute to the form containing the WebBrowser indicating that we were using the IOleCommandTarget as a COM event source (see listing 3).

Listing 3 - COM Class Interface attribute added to the form.

  1. [ClassInterface(ClassInterfaceType::None),ComSourceInterfaces(IOleCommandTargetWrapper::typeid)]        
  2. public ref class HtmlExampleForm : public System::Windows::Forms::Form  
Conclusion
 
Hopefully, in the future, Microsoft will add all of the functionality of IE to the WebBrowser control methods and the Document property methods. This way, the user doesn't have to jump through hoops to get to the underlying COM functionality in the browser. In the meantime, try to stay COM and keep on plugging away using the power of .NET.