Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Component-Based Development with Visual C#

0.00/5 (No votes)
20 Nov 2002 1  
Chapter 8: Creating Front Ends with the WebBrowser Component
Sample Image - 1930110286.gif
Author Ted Faison
Title Component-Based Development with Visual C#
PublisherWiley
PublishedApril 2002
ISBN 0-7645-4914-6
Price US 49.99
Pages 1008

In This Chapter

  • Creating a simple Web browser
  • Using ActiveX components in managed code
  • Exposing COM interfaces from managed code
  • Customizing the WebBrowser component

Chapter 8: Creating Front Ends with the WebBrowser Component

A billion here, a billion there — sooner or later it adds up to real money.
— Former U.S. Senator Everett Dirksen on public finance

Senator Dirksen’s concept of real money may have been slightly different from yours or mine, but one thing is certain: If you fail to take advantage of existing components when building the front end of your application, you’ll find your development costs adding up to real money.

The Microsoft WebBrowser Component

One of the most powerful reusable UI components I know of is the WebBrowser ActiveX component used by Internet Explorer. Microsoft designed it to be extremely flexible in terms of the kinds of content it can display. Obviously, it can display HTML documents, but it may come as a surprise to some of you that it can also display other common file types such as Word, Excel, PowerPoint, TXT, RTF, GIF, JPEG, XML, PDF, and others. WebBrowser achieves this incredible flexibility by using an embedded ActiveX component to render its data. In the case of HTML documents, the component is MsHtml. For more complex documents like Word or Excel, WebBrowser acts as an Active Document host and embeds Word or Excel in the client area to handle the rendering.

Since WebBrowser was designed specifically to be hosted by a container application, it has a lot of features you can control. Microsoft uses the component in several different applications, and customizes its look and feel to blend in with the rest of the host application. Examples of hosts include Windows Explorer, Outlook, and even the VS .NET Start Page. In this chapter, I’ll show you how to create a Windows application that hosts the WebBrowser component. Later, since customization is very important to many of you, I’ll create a second Windows application showing how to access the various customization features that WebBrowser makes available.

I’ll call the first browser application MyWebBrowser and give it a tool bar, to show how to control some of the common functions of WebBrowser. As I said earlier, WebBrowser can handle more than just HTML files. Figures 8-1 through 8-6 show MyWebBrowser displaying different types of files.

Figure 8-1: Using MyWebBrowser to display an HTML page

Figure 8-2: Using MyWebBrowser to display a Word document

Figure 8-3: Using MyWebBrowser to display an Excel spreadsheet

Figure 8-4: Using MyWebBrowser to display a Powerpoint document

Figure 8-5: Using MyWebBrowser to display an XML document

Figure 8-6: Using MyWebBrowser to display a PDF document

These screen shots hopefully give you some sense of how flexible WebBrowser really is. WebBrowser and its Active Document subcomponents do all the work behind the scenes. If you can find a more powerful reusable UI component than WebBrowser, let me know.

Not only does WebBrowser display a zillion file types, it also lets you control the way documents are presented. In the case of HTML documents, there is a whole slew of behaviors that can be changed, such as:

  • Changing the context menu

  • Hiding scrollbars

  • Disabling text selection

  • Removing the 3D border

  • Using flat scrollbars

  • Forcing in-place navigation

I’ll show you how to make these and other kinds of customizations to MsHtml, but first, I’ll get you familiar with WebBrowser by showing you a simple application called MyWebBrowser that can be used as a simple mini-browser without any customizations. After describing MyWebBrowser, I’ll show you a customized browser in an application called MyCustomWebBrowser. This application will use COM interfaces and callbacks to control the way MsHtml works.

Designing MyWebBrowser

Since almost all the functionality you need is already built into WebBrowser, the design of an application based on Windows Forms that embeds the component is fairly trivial. Figure 8-7 shows the salient parts of the class diagram for MyWebBrowser.

Figure 8-7: The class diagram for MyWebBrowser

Like I said, in this first example, I won’t add any special customizations to MsHtml or WebBrowser, because doing so requires utilizing COM interoperability features. After showing a simple example, I’ll show you how to use COM interfaces to customize many MsHtml features.

Developing MyWebBrowser

Enough said about the glories of WebBrowser and MsHtml. They’re built-in, they’re good, they’re wonderful. Great. Let’s put them to work in a real program. MyWebBrowser is a simple Windows application that has just a single form, called MainForm, which hosts the WebBrowser component and demonstrates not only how to use it, but more generally how to access ActiveX components from managed code.

I created MyWebBrowser with the New Project Wizard, choosing Windows Application as the project type. I renamed the main form class from Form1 to MainForm, and set the project name to MyWebBrowser. The next step was to add the WebBrowser ActiveX component to MainForm, a task that deserves a separate explanation.

Importing the WebBrowser ActiveX Component

There is no WebBrowser component in the Toolbox when you install VS .NET, so you have to import a .NET version of the WebBrowser into your project. As with just about everything in life, the import process can be done the easy way or the hard way.

The easy way

The easy way is with the Customize Toolbox dialog box. First select the Windows Forms page on the Toolbox and then right-click on the Toolbox and choose the Customize Toolbox command on the pop-up menu. Select the COM Components tab, scroll down to the Microsoft Web Browser item, and check its checkbox, as shown in Figure 8-8.

Figure 8-8: Importing the ShDocVw.WebBrowser control into the VS .NET Toolbox

The WebBrowser will show up on the Windows Forms tab of the Toolbox with the name Explorer, as shown in Figure 8-9.

Figure 8-9: The WebBrowser component after installing it on the Toolbox

Now drop an instance of WebBrowser on MainForm. When the component is dropped, VS .NET performs the following tasks.

  1. It launches an import process that creates the two files AxInterop.SHDocVw.dll and Interop.SHDocVw.dll.

  2. It puts the two files in your project’s bin\Debug or bin\Release directory (depending on whether your project configuration is set to Debug or Release).

  3. It adds the two files to the References node of your project in the Solution Explorer.

  4. It adds an instance of AxSHDocVw.AxWebBrowser to your form.

A nice piece of work, saving you precious time and money — perhaps not billions, but hey, no one said life is fair.

In case you’re wondering, much of the process was performed under the covers by a command-line utility called aximp, described later. At this point the Solution Explorer looks like Figure 8-10.

Figure 8-10: The Solution Explorer, showing the newly imported files

That was the easy way to import the WebBrowser component. The hard way is for those of you that like typing (and I know there are a lot of you out there).

The hard way

I always get a kick out of watching people open DOS boxes and furiously type in long commands that could be replaced with a couple of mouse clicks. Force of habit is a powerful thing. But using command-line utilities for the import process is not always a bad thing, because they create the necessary import files without adding anything to the Toolbox. Although you may want to have a useful component like WebBrowser in the Toolbox, you don’t necessarily want every ActiveX component you’ll ever use cluttering up the precious Toolbox space.

In any case, there are two command-line utilities available for converting COM types into .NET-compatible types that can be referenced in a VS .NET project. Which one to use depends on how you want to use the imported components.

Using TlbImp

The lowest level command-line utility for ActiveX importing is TlbImp. You’ll want to use this utility when importing ActiveX components that won’t be used in a Windows Form. TlbImp reads a file containing COM Type Library information — which can be a .tlb, .dll, .odl, or other file type — and produces a DLL containing .NET-compatible metadata. The DLL must then be added to the References node of your project.

For example, to use shdocvw.dll (containing the WebBrowser component, for those of you that jumped into this section without reading the previous ones) in the MyWebBrowser project, you would open a Command-Prompt box, go to the folder C:\Program Files\Microsoft.NET\FrameworkSDK\Bin, and then type the command:

tlbimp c:\winnt\system32\shdocvw.dll /out:C:\MyWebBrowser\bin\Debug\Interop.shdocvw.dll

You can inspect the .NET metadata in the DLL generated by TlbImp with a standard .NET tool like ildasm. Open a Command-Line box and go to the folder containing the newly generated Shdocvw.dll file and then type the command:

"C:\Program Files\Microsoft.NET\FrameworkSDK\Bin\ildasm" shdocvw.dll

Figure 8-11 shows some of the contents of the metadata file displayed by ildasm.

Figure 8-11: Inspecting the metadata in the DLL produced by running TlbImp on c:\winnt\system32\shdocvw.dll

The metadata makes it possible for your code to interact with the unmanaged code of COM components using exactly the same syntax you would use with native C# components.

Using AxImp

If you plan to use an ActiveX component in a Windows Form, as I did with WebBrowser, you’ll need to use the AxImp command-line utility instead of TlbImp. The reason is this: To use an ActiveX component in a Windows Form, a wrapper class must also be generated. All components dropped in Windows Forms are required to be derived from the common base class System.Windows.Forms.Control. The utility AxImp creates the wrapper for you.

To create the wrapper for WebBrowser, open a Command-Line box, go to the folder where the executable code of your project will go. For the project MyWebBrowser, the folder will be MyWebBrowser\bin\debug or MyWebBrowser\bin\release. Type the following command:

"C:\Program Files\Microsoft.NET\FrameworkSDK\Bin\aximp"  c:\winnt\system32\shdocvw.dll

The utility will generate two files, called SHDocVw.dll and AxSHDocVw.dll. The first file contains the .NET metadata that describes the COM types contained in c:\winnt\system32\shdocvw.dll. The file is identical to the one produced by TlbImp in the previous section (because AxImp internally calls TlbImp to generate it). The second file is a .NET assembly containing the wrapper classes that allow you to use the ActiveX component in standard Windows Forms. Figure 8-12 shows the contents of AxSHDocVw.dll, as viewed with ildasm:

Figure 8-12: The wrapper class created for SHDocVw

Basically, AxImp creates a new class derived from System.Windows.Forms.AxHost, which is derived from System.Windows.Forms.Control. This new class acts as a .NET wrapper for the ActiveX component.

Runtime Callable Wrappers (RCW)

When you instantiate a COM type in your C# code, using code like this:

AxSHDocVw.AxWebBrowser axWebBrowser1 = new AxSHDocVw.AxWebBrowser();

there is more going on than meets the eye. What actually happens is this: The compiler looks at the metadata describing the class AxWebBrowser, in the AxSHDocVw assembly generated by TlbImp or AxImp. Using this metadata, it creates something called a Runtime Callable Wrapper (RCW), which is a proxy component that on one side is callable from your managed code and on the other side deals with the unmanaged COM code of the WebBrowser ActiveX component. The RCW acts as an invisible bridge between your code and the ActiveX code, as shown in Figure 8-13.

Figure 8-13: The Runtime Callable Wrapper as a proxy for COM components

The RCW manages all those pesky COM details that you don’t want to deal with, such as reference counting, deleting the component when it is no longer used, marshalling parameters in method calls, and so on. If you create multiple instances of the same COM component in your code, all the instances will share a single RCW. Keep in mind that all this RCW business is generally completely transparent to you. It’s there to help you, making it as easy to access an ActiveX component as any other managed component.

Adding a toolbar

Before I completely lose track of where I am, let me finish describing the code for MyWebBrowser. First I’ll discuss the toolbar buttons. Most UIs containing WebBrowser support some means for navigating, whether it’s with toolbar buttons, menu commands, or some other element.

I added a toolbar to MyWebBrowser by dropping a Toolbox component of type ToolBar on the main form. In the ToolBar’s properties, I clicked on the Collections field and added seven buttons with the following functions:

  • Back

  • Forward

  • Stop

  • Refresh

  • Home

  • Search

  • Print

To make the buttons look like those used by Internet Explorer, I used a screen capture utility to get the IE button images, and then saved them as bitmap files in the folder containing the source code for MyWebBrowser. I set them all to have a green background. I added an ImageList component to the form and added all the bitmap images to it. I set the ImageList TransparentColor property to green, so the green areas of the bitmaps would be transparent and assume the color of the button faces. Then I set the ToolBar’s ImageList property to reference the ImageList component. For each toolbar button, I set the following properties: ToolTipText, Text, and ImageIndex. To the right of the toolbar buttons, I dropped a TextBox control. To make the TextBox always stretch to the right side of the form, I set its Anchor property to (Top, Left, Right). Figure 8-14 shows the finished toolbar.

Figure 8-14: The finished toolbar

Notice the flat look of the toolbar, with the buttons showing no borders. This look was achieved by setting the toolbar’s Appearance property to Flat. Next, I added some event handling code for the toolbar buttons, as shown in Listing 8-1.

Listing 8-1:: The Toolbar Event Handler

protected void toolBar1_ButtonClick(object sender,
             System.WinForms.ToolBarButtonClickEventArgs e)
{
  Cursor.Current = Cursors.WaitCursor;
  try 
  {
    if (e.button == toolBarButtonBack)
      axWebBrowser1.GoBack();
    else if (e.button == toolBarButtonForward)
      axWebBrowser1.GoForward();
    else if (e.button == toolBarButtonStop) 
    {
      axWebBrowser1.Stop();
      toolBarButtonStop.Enabled = false;
    }
    else if (e.button == toolBarButtonSearch)
      axWebBrowser1.GoSearch(); 
    else if (e.button == toolBarButtonPrint)
      PrintPage();
    else if (e.button == toolBarButtonRefresh)
    {
      object REFRESH_COMPLETELY = 3;
      axWebBrowser1.Refresh2(ref REFRESH_COMPLETELY); 
    }
    else if (e.button == toolBarButtonHome)
      axWebBrowser1.GoHome();
  }
  finally 
  {
    Cursor.Current = Cursors.Default;
  }
}

The method WebBrowser.GoSearch() navigates to the Search site, which can be set up in Internet Explorer by clicking the Search toolbar button and selecting Customize in the Search pane. By default, WebBrowser uses the search engine at http://search.msn.com.

The method WebBrowser.GoHome() navigates to the Home site set up in Internet Explorer with the Tools @@> Internet Options dialog box, on the General tab.

Adding support for printing

All of the buttons except Print are handled by calling a method in the embedded WebBrowser component. As noted earlier, there is no Print() method, so printing is handled differently, with the code shown in Listing 8-2.

Listing 8-2:: Printing the HTML Page Displayed in the WebBrowser

private bool IsPrinterEnabled()
{
  int response = 
    (int) axWebBrowser1.QueryStatusWB(SHDocVw.OLECMDID.OLECMDID_PRINT);
  return (response & (int) SHDocVw.OLECMDF.OLECMDF_ENABLED) != 0 ? 
    true : false;
}
private void PrintPage()
{
  object o = "";
  // constants useful when printing

  SHDocVw.OLECMDID Print = SHDocVw.OLECMDID.OLECMDID_PRINT;
  // use this value to print without prompting

  // SHDocVw.OLECMDEXECOPT PromptUser =

  //  SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_PROMPTUSER;

  SHDocVw.OLECMDEXECOPT DontPromptUser =
    SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER;
  
  if (!IsPrinterEnabled() ) return;
  // print without prompting user

  axWebBrowser1.ExecWB(Print, DontPromptUser, ref o, ref o);
  // to prompt the user with printer settings

  // axWebBrowser1.ExecWB(Print, PromptUser, ref o, ref o);

}

The last two lines in the listing show two ways to print: The first way prints silently, the second way prints after prompting the user for printer settings. I created two constants called DontPromptUser and PromptUser, which are derived from ShDocVw enumerated values. When you call WebBrowser.ExecWB(), you simply pass the constant indicating whether you want the method to display a Print dialog box or not.

Adding navigation support

To navigate to a Web site, you call the Navigate() or Navigate2() method of the WebBrowser. The first method handles navigating to ordinary URLs, including local files. The second method extends the first by supporting navigation to items in the Windows Desktop and My Computer folders. To protect the rest of the application from errors caused by invalid URLs or network problems, I wrapped the call to WebBrowser.Navigate() in a try block in a method called GotoURL(), shown in Listing 8-3.

Listing 8-3:: Setting the URL of the Document to Load

public void GotoURL(String theURL) 
{
  try 
  {
    Cursor.Current = Cursors.WaitCursor;
    Object o = null;
    axWebBrowser1.Navigate(theURL, ref o, ref o, ref o, ref o);
  } 
  finally {
    Cursor.Current = Cursors.Default;
  }
}

To allow the user to type a URL in the TextBox, I added an event handler for the TextBox that calls GotoURL() as shown in Listing 8-4.

Listing 8-4:: The TextBox Event Handler

protected void textBoxAddress_KeyDown (object sender,
                                       System.WinForms.KeyEventArgs e)
{
  if (e.KeyCode == Keys.Return) 
    GotoURL(textBoxAddress.Text);
}

To display an hourglass cursor while a page is being loaded, I set the cursor in the WebBrowser’s BeforeNavigate2() handler as shown in Listing 8-5.

Listing 8-5:: Setting the Cursor to an Hourglass during Navigation

protected void axWebBrowser1_BeforeNavigate2 (object sender,
               AxSHDocVw.DWebBrowserEvents2_BeforeNavigate2Event e)
{
  toolBarButtonStop.Enabled = true;
  Cursor.Current = Cursors.WaitCursor;
}

Once a page is loaded, I need make sure the cursor is eventually restored to an arrow pointer. Navigation commands can end in one of two ways: If the page can’t be loaded, the NavigateError handler is called. If the page was loaded, the NavigateComplete handler is called. I created a simple NavigateError handler, as shown in Listing 8-6.

Listing 8-6:: The NavigateError Handler that Restores the Mouse Cursor

private void axWebBrowser1_NavigateError(object sender,
             AxSHDocVw.DWebBrowserEvents2_NavigateErrorEvent e)
{
  Cursor.Current = Cursors.Default;
  toolBarButtonStop.Enabled = false;
  toolBarButtonHome.Enabled = true;
  toolBarButtonSearch.Enabled = true;
  toolBarButtonRefresh.Enabled = true;
}

If navigation to a site is successful, the NavigateComplete2 handler is called, if available. I created a NavigateComplete2 handler, as shown in Listing 8-7.

Listing 8-7:: The NavigateComplete2 Handler

private void axWebBrowser1_NavigateComplete2(object sender, 
        AxSHDocVw.DWebBrowserEvents2_NavigateComplete2Event e)
{
  Cursor.Current = Cursors.Default;
  toolBarButtonStop.Enabled = false;
  toolBarButtonHome.Enabled = true;
  toolBarButtonSearch.Enabled = true;
  toolBarButtonRefresh.Enabled = true;
  // update the URL displayed in the address bar

  String s = e.uRL.ToString();
  textBoxAddress.Text = s;
  // update the list of visited URLs

  int i = urlsVisited.IndexOf(s);
  if (i >= 0)
    currentUrlIndex = i;
  else 
    currentUrlIndex = urlsVisited.Add(s);
  // enable / disable the Back and Forward buttons

  toolBarButtonBack.Enabled = (currentUrlIndex == 0) ? false : true;
  toolBarButtonForward.Enabled = 
    (currentUrlIndex >= urlsVisited.Count-1) ? false : true;
  // set the state of the Print button

  toolBarButtonPrint.Enabled = IsPrinterEnabled();
}

In the NavigateComplete2 handler, I displayed the current URL in the TextBox and updated the list of visited URLs. Each time the user goes to a new URL, I store it in an ArrayList. I used the list to support enabling and disabling the Back and Forward buttons. When the user clicks the Back button, I retrieve the previous URL in the list and navigate to it. When the user navigates all the way back to the first URL in the list, I disable the Back button. Similarly, when the user navigates forward to the end of the list, I disable the Forward button.

Disabling the Back and Forward buttons is more than just a cosmetic exercise: If you call the WebBrowser’s GoBack() or GoForward() method when there is no previous or next URL to navigate to, the component will raise an exception. If you don’t catch the exception, an error message will be displayed.

The last feature to add is one that will make MyWebBrowser go to the Home page when it is run. All you need to do is call the method axWebBrowser1.GoHome() in the constructor for MainForm.

The complete code

The next section will deal with ways to customize the WebBrowser component. Before I move to this new topic, I’ve included the full code for MyWebBrowser in Listing 8-8.

Listing 8-8:: The Code for MyWebBrowser

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
namespace MyWebBrowser
{
  public class MainForm : System.Windows.Forms.Form
  {
    private System.Windows.Forms.Panel panel1;
    private System.Windows.Forms.ToolBar toolBar1;
    private System.Windows.Forms.ImageList imageList1;
    private System.Windows.Forms.ToolBarButton toolBarButtonBack;
    private System.Windows.Forms.ToolBarButton toolBarButtonForward;
    private System.Windows.Forms.ToolBarButton toolBarButtonStop;
    private System.Windows.Forms.ToolBarButton toolBarButtonRefresh;
    private System.Windows.Forms.ToolBarButton toolBarButtonHome;
    private System.Windows.Forms.ToolBarButton toolBarButtonSearch;
    private System.Windows.Forms.ToolBarButton toolBarButtonPrint;
    private AxSHDocVw.AxWebBrowser axWebBrowser1;
    private System.Windows.Forms.TextBox textBoxAddress;
    private System.ComponentModel.IContainer components;
    ArrayList urlsVisited = new ArrayList();
    int currentUrlIndex = -1;  // no sites visited initially

    
    public MainForm()
    {
      InitializeComponent();
      toolBarButtonBack.Enabled = false;
      toolBarButtonForward.Enabled = false;
      toolBarButtonStop.Enabled = false;
      toolBarButtonRefresh.Enabled = false;
      toolBarButtonHome.Enabled = false;
      toolBarButtonSearch.Enabled = false;
      toolBarButtonPrint.Enabled = false;
      
      axWebBrowser1.GoHome();
    }
    protected override void Dispose( bool disposing )
    {
      // standard wizard-created code

    }
    private void InitializeComponent()
    {
      // standard wizard-created code

    }
    [STAThread]
    static void Main() 
    {
         Application.Run(new MainForm());
    }
    private void toolBar1_ButtonClick(object sender,
            System.Windows.Forms.ToolBarButtonClickEventArgs e)
    {
      Cursor.Current = Cursors.WaitCursor;
      try 
      {
        if (e.Button == toolBarButtonBack)
          axWebBrowser1.GoBack();
        else if (e.Button == toolBarButtonForward)
          axWebBrowser1.GoForward();
        else if (e.Button == toolBarButtonStop) 
        {
          axWebBrowser1.Stop();
          toolBarButtonStop.Enabled = false;
        }
        else if (e.Button == toolBarButtonSearch)
          axWebBrowser1.GoSearch(); 
        else if (e.Button == toolBarButtonPrint)
          PrintPage();
        else if (e.Button == toolBarButtonRefresh)
        {
          object REFRESH_COMPLETELY = 3;
          axWebBrowser1.Refresh2(ref REFRESH_COMPLETELY); 
        }
        else if (e.Button == toolBarButtonHome)
          axWebBrowser1.GoHome();
      }
      finally 
      {
        Cursor.Current = Cursors.Default;
      }
    }
    
    private bool IsPrinterEnabled()
    {
      int response = 
        (int) axWebBrowser1.QueryStatusWB(SHDocVw.OLECMDID.OLECMDID_PRINT);
      return (response & (int) SHDocVw.OLECMDF.OLECMDF_ENABLED) != 0 ?
             true : false;
    }
    private void PrintPage()
    {
      object o = "";
      // constants useful when printing

      SHDocVw.OLECMDID Print = SHDocVw.OLECMDID.OLECMDID_PRINT;
      // use this value to print without prompting

      // SHDocVw.OLECMDEXECOPT PromptUser =

      //  SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_PROMPTUSER;

      SHDocVw.OLECMDEXECOPT DontPromptUser =
        SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER;
  
      if (!IsPrinterEnabled() ) return;
      // print without prompting user

      axWebBrowser1.ExecWB(Print, DontPromptUser, ref o, ref o);
      // to prompt the user with printer settings

      // axWebBrowser1.ExecWB(Print, PromptUser, ref o, ref o);

    }
    
    public void GotoURL(String theURL) 
    {
      try 
      {
        Cursor.Current = Cursors.WaitCursor;
        Object o = null;
        axWebBrowser1.Navigate(theURL, ref o, ref o, ref o, ref o);
      } 
      finally 
      {
        Cursor.Current = Cursors.Default;
      }
    }
    private void textBoxAddress_KeyDown(object sender,
            System.Windows.Forms.KeyEventArgs e)
    {
      if (e.KeyCode == Keys.Return) 
        GotoURL(textBoxAddress.Text);
    }
    private void axWebBrowser1_BeforeNavigate2(object sender,
            AxSHDocVw.DWebBrowserEvents2_BeforeNavigate2Event e)
    {
      toolBarButtonStop.Enabled = true;
      Cursor.Current = Cursors.WaitCursor;
    }
    private void axWebBrowser1_NavigateComplete2(object sender,
            AxSHDocVw.DWebBrowserEvents2_NavigateComplete2Event e)
    {
      Cursor.Current = Cursors.Default;
 
      toolBarButtonStop.Enabled = false;
      toolBarButtonHome.Enabled = true;
      toolBarButtonSearch.Enabled = true;
      toolBarButtonRefresh.Enabled = true;
      // update the URL displayed in the address bar

      String s = e.uRL.ToString();
      textBoxAddress.Text = s;
      // update the list of visited URLs

      int i = urlsVisited.IndexOf(s);
      if (i >= 0)
        currentUrlIndex = i;
      else 
        currentUrlIndex = urlsVisited.Add(s);
      // enable / disable the Back and Forward buttons

      toolBarButtonBack.Enabled = (currentUrlIndex == 0) ? false : true;
      toolBarButtonForward.Enabled = 
        (currentUrlIndex >= urlsVisited.Count-1) ? false : true;
      // set the state of the Print button

      toolBarButtonPrint.Enabled = IsPrinterEnabled();
    }
    private void axWebBrowser1_NavigateError(object sender,
            AxSHDocVw.DWebBrowserEvents2_NavigateErrorEvent e)
    {
      Cursor.Current = Cursors.Default;
      toolBarButtonStop.Enabled = false;
      toolBarButtonHome.Enabled = true;
      toolBarButtonSearch.Enabled = true;
      toolBarButtonRefresh.Enabled = true;
    }
  }
}

In the next section, I’ll switch gears and get into advanced topics with COM Interop programming. If you’re not experienced in COM, you may want to skip the rest of the chapter entirely.

Creating a Customized Web Browser

MyWebBrowser has all the basic browser features, such as navigation, printing, and so on. It also has features you may want to change, such as how accelerator keys are handled or what commands are available on the context menu. To make these kinds of changes, things get a bit more complicated with WebBrowser, and you are forced to get your hands dirty with some COM interoperability programming. In this and the following sections, I’ll create another Windows application called MyCustomWebBrowser that demonstrates how to develop a fully customized WebBrowser. To start the project, I just copied the entire MyWebBrowser solution into a new folder and renamed it to MyCustomWebBrowser.

Customizing the WebBrowser component is more complicated than one might think. The problem is this: Most of the customizable features of MsHtml depend on COM callbacks that must be handled by the host window — MainForm in this case. Callbacks are methods exposed through COM interfaces, so you have to make the necessary interfaces available to MsHtml. It would have been nice if MsHtml had exposed a list of properties for all its customizable features, so you could change a feature with a simple line of code like this:

AxWebBrowser.Use3Dborders = false;  // this won�t work

That would have been way too simple! Besides, each time the parent WebBrowser loads a new HTML page, it creates a new instance of MsHtml, so even if you could set the properties using the previous code, the new MsHtml component wouldn’t be affected. You might be thinking, “Why didn’t Microsoft store the customizable properties of MsHtml in the parent WebBrowser component? This way you could set them once, and have WebBrowser automatically reapply them each time it created a new MsHtml component”. For better or for worse, WebBrowser doesn’t want anything to do with presentation of content. It was designed to act as a host for rendering components. It handles other aspects of Web browsing, like navigation. WebBrowser is an Active Document host, and it delegates all details of presentation to the hosted Active Document rendering component (MsHtml in this case).

The moral of the story is this: MainForm needs to implement a number of COM interfaces to support MsHtml customizing. Figure 8-15 shows the class diagram for MainForm.

Figure 8-15: The class diagram for a fully customized WebBrowser host Windows Form

Let’s take a look at what the COM interfaces are used for. The first step in any type of WebBrowser customization is to establish a component as the controlling host. By default, the WebBrowser has no controlling host. To set up MainForm as the new controlling host, you need to call WebBrowser through its OleClient interface as shown in Listing 8-9.

Listing 8-9:: Setting Up MainForm as the Controlling Host of WebBrowser

object obj = axWebBrowser1.GetOcx();
IOleObject oc = obj as IOleObject;
oc.SetClientSite(this);

These three deceptively simple lines of code are critical. The method GetOcx() retrieves the IUnknown interface of the native COM object wrapped by axWebBrowser1. The second line implicitly issues a QueryInterface, seeking an IOleObject interface from the WebBrowser COM object. You can use the as operator like this to query for any type of interface. If the interface is not available, a null will be returned. Once the IOleObject interface is obtained from WebBrowser, the method SetClientSite() is called to set MainForm as the controlling host. Objects passed as a parameter to SetClientSite() must implement the IOleClientSite, ergo the presence of IOleClientSite in the Class Diagram previously shown in Figure 8-15.

Importing and wrapping COM interfaces

MyCustomWebBrowser implements the two COM interfaces IOleClientSite and IDocHostUIHandler, and calls methods of the interface IOleObject. Unfortunately there are no type libraries published by Microsoft that contain these interfaces, meaning you have to get your hands dirty with a bit of COM programming to work around the problem. As it turns out, most of the COM interfaces that Microsoft uses in its components are not published as type libraries. You’ll either find them as .idl files, or in some cases as C++ header files.

IOleObject and IOleClientSite

You sometimes have to do a bit of digging to locate the file that declares an interface. In the case of IOleClientSite and IOleObject, I found them both declared in the file oleidl.h, which is in the following folder:

C:\Program Files\Microsoft Visual Studio.NET\Vc7\PlatformSDK\include

I created C# interfaces to wrap the two COM interfaces, as shown in Listings 8-10 and 8-11.

Listing 8-10:: The Interface that Wraps the COM Interface IOleObject

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace MyCustomWebBrowser
{
  [ComImport,
  Guid("00000112-0000-0000-C000-000000000046"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown) ]
  public interface IOleObject
  {
    void SetClientSite(IOleClientSite pClientSite);
    void GetClientSite(IOleClientSite ppClientSite);
    void SetHostNames(object szContainerApp, object szContainerObj);
    void Close(uint dwSaveOption);
    void SetMoniker(uint dwWhichMoniker, object pmk);
    void GetMoniker(uint dwAssign, uint dwWhichMoniker, object ppmk);
    void InitFromData(IDataObject pDataObject, bool 
                      fCreation, uint dwReserved);
    void GetClipboardData(uint dwReserved, IDataObject ppDataObject);
    void DoVerb(uint iVerb, uint lpmsg, object pActiveSite, 
                uint lindex, uint hwndParent, uint lprcPosRect);
    void EnumVerbs(object ppEnumOleVerb);
    void Update();
    void IsUpToDate();
    void GetUserClassID(uint pClsid);
    void GetUserType(uint dwFormOfType, uint pszUserType);
    void SetExtent(uint dwDrawAspect, uint psizel);
    void GetExtent(uint dwDrawAspect, uint psizel);
    void Advise(object pAdvSink, uint pdwConnection);
    void Unadvise(uint dwConnection);
    void EnumAdvise(object ppenumAdvise);
    void GetMiscStatus(uint dwAspect,uint pdwStatus);
    void SetColorScheme(object pLogpal);
  };
}

Listing 8-11:: The Interface that Wraps the COM Interface IOleClientSite

using System;
using System.Runtime.InteropServices;
namespace MyCustomWebBrowser
{
  [ComImport,
  Guid("00000118-0000-0000-C000-000000000046"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown) ]
  public interface IOleClientSite
  {
    void SaveObject();
    void GetMoniker(uint dwAssign, uint dwWhichMoniker, object ppmk);
    void GetContainer(object ppContainer);
    void ShowObject();
    void OnShowWindow(bool fShow);
    void RequestNewObjectLayout();
  }
}

A few words of explanation are due regarding the attributes used on the interface declarations. The ComImport attribute is used to tag an interface as an existing COM interface that is being implemented in managed code. A requirement for a ComImported interface is that there must also be a Guid attribute providing the interface’s Globally Unique ID. The InterfaceType attribute indicates the basic type of the interface. The choices are:

  • InterfaceIsDual. This interface supports both early and late binding.

  • InterfaceIsIDispatch. This interface supports the IDispatch interface.

  • InterfaceIsIUnknown. This interface just derives directly or indirectly from IUnknown.

The value ComInterfaceType.InterfaceIsIUnknown is by far the most common type of interface.

I don’t have enough space here to describe all the methods exposed by the interfaces in the last two listings. For the purpose of this discussion, the only method that matters is IOleObject.SetClientSite().

When creating wrapper interfaces that will be called from native COM code, it is obviously critical that the GUID you specify matches the original one used by COM. It is also essential that the wrapper interface implements all the original COM methods, and that all methods have the same signature as the original method.

Using ICustomDoc

As stated earlier, the first step in customizing WebBrowser is to establish MainForm as the controlling host. Calling IOleObject.SetClientSite() is the way to do this. If you’re not interested in working with WebBrowser, but only with MsHtml, there is another way to support customizations, without the need to use IOleObject or IOleClientSite.

Since customizing MsHtml is a fairly common operation, the component was provided with an interface called ICustomDoc that you can call to specify an IDocHostUIHandler object, which will act as a customizer. When MsHtml is about to interact with the user is some way, it checks to see if an IDocHostUIHandler object is available. If so, it calls its methods to find out how to behave. I’ll talk about these methods a bit later. For the moment, what counts is how MainForm would use MsHtml’s ICustomDoc interface. Listing 8-12 shows the details.

Listing 8-12:: Setting a Component as the Customizer for MsHtml

object obj = mshtml.GetOcx();  // assume mshtml was created earlier

ICustomDoc doc = obj as ICustomDoc;
doc.SetUIHandler(this);  // �this� must implement IDocHostUIHandler

Keep in mind that the interface ICustomDoc is only available on MsHtml — not on WebBrowser — so the code shown in the listing requires you to have a reference to an instance of MsHtml. There are many applications that use MsHtml by itself to render HTML pages. For example, it is increasingly common to see forms and dialog boxes containing HTML controls instead of Windows controls. By embedding MsHtml on a form, you have easy access to the powerful HTML renderer contained in MsHtml.

IDocHostUIHandler

This is the most important interface MainForm needs, in terms of customizing MsHtml. In the following sections, I’ll give you a detailed description about what methods the interface has, what parameters the methods take, when MsHtml calls the methods, and what effect they have on the user interface.

Before I get ahead of myself (and I’m not talking about an out-of-body experience), let me show you how to import the interface into your managed code. As it turns out, IDocHostUIHandler references a fairly large number of other OLE interfaces and enumerations, so it isn’t as easy to import as other interfaces.

Since I didn’t have days or weeks to create the necessary wrapper classes and interfaces for all the items referenced in IDocHostUIHandler, I cheated and used a tool to help me. Borland’s Delphi product has a very powerful and easy-to-use Type Library editor. The types used by IDocHostUIHandler are mostly defined in the file mshtmhst.idl, available on the Web and in the following folder:

C:\Program Files\Microsoft Visual Studio.NET\Vc7\PlatformSDK\include

But cutting and pasting code from the .idl file into the Delphi Type Library editor, I was able create a new type library file that I could use with TypImp. I’ll spare you the gory details of my cutting and pasting adventures. Suffice it to say that I created a file called MsHtmlCustomization.tlb, which I saved in the folder “MyCustomWebBrowser\Type Libraries”. Using TlbImp, I easily converted the file into a .dll that I could use with MyCustomWebBrowser. All I had to do is use the Add Reference Wizard to import the .dll and then add the following statement to class MainForm:

using MsHtmlCustomization;

In case you’re curious, Figure 8-16 shows Delphi’s Type Library editor with the interfaces and other items the library contains.

Figure 8-16: The Delphi Type Library editor, which is a convenient tool for rapidly creating type libraries

Figure 8-17 shows what the imported assembly MsHtmlCustomization.dll looks like with ildasm.

Figure 8-17: Exploring the MsHtmlCustomization contents with ILDASM

As you can see from the figure, MsHtmlCustomization contains a number of interfaces that are useful when customizing MsHtml, including ICustomDoc and IDocHostShowUI. Listing 8-13 shows how IDocHostUIHandler would look if it were declared in C#.

Listing 8-13:: The C# Implementation of IDocHostUIHandler

namespace MsHtmlCustomization
{
  [ComImport,
  Guid("BD3F23C0-D43E-11CF-893B-00AA00BDCE1A"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown) ]
  public interface IDocHostUIHandler : IUnknown
  {
    public void ShowContextMenu(
           MsHtmlCustomization.ContextMenuTarget dwContext, 
           ref MsHtmlCustomization.POINT pPoint, 
           MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
           object HTMLTagElement); 
    public void GetHostInfo(
           ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo);
    public void ShowUI(int dwID, object pActiveObject,
           MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
           object pFrame, object pDoc);
    public void HideUI();
    public void UpdateUI();
    public void EnableModeless(int fEnable);
    public void OnDocWindowActivate(int fActivate);
    public void OnFrameWindowActivate(int fActivate);
    public void ResizeBorder(ref MsHtmlCustomization.RECT prcBorder, 
                int pUIWindow, int fFrameWindow) {}
    public void TranslateAccelerator(ref MsHtmlCustomization.MSG lpMsg,
                ref MsHtmlCustomization.UUID pguidCmdGroup, int nCmdID);
    public void GetOptionKeyPath(ref int pchKey, int dw);
    public MsHtmlCustomization.IDropTarget 
           GetDropTarget(MsHtmlCustomization.IDropTarget pDropTarget);
    public object GetExternal();
    public int TranslateUrl(int dwTranslate, int pchURLIn);
    public MsHtmlCustomization.IdataObject
           FilterDataObject(MsHtmlCustomization.IDataObject pDO);
  }
}

Besides IDocHostUIHandler, there is another COM interface that is useful in customizing MsHtml: IDocHostShowUI. Although the limited space available prevents me from describing this interface in detail, I did include it in MsHtmlCustomization. Listing 8-14 shows how the class would look if declared in C#.

Listing 8-14:: The C# Implementation of IDocHostShowUI

namespace MsHtmlCustomization
{
  [ComImport,
  Guid("C4D244B0-D43E-11CF-893B-00AA00BDCE1A"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown) ]
  public interface IDocHostShowUI : IUnknown
  {
    public void ShowMessage(int hwnd, ref int lpstrText, 
                            ref int lpstrCaption, uint dwType, 
                            ref int lpstrHelpFile, uint dwHelpContext,
                            out int lpResult);
                                  
    public void ShowHelp(uint hwnd, ref int pszHelpFile, 
                         uint uCommand, uint dwData, 
                         MsHtmlCustomization.POINT ptMouse, 
                         out object pDispatchObjectHit);                       
  }
}

This interface is used to control how message boxes and help windows are handled by MsHtml. As you can see, the interface only supports two methods. In order for IDocHostShowUI methods to be called back from MsHtml, the host component (MainForm in this case) needs to implement the interfaces IOleClientSite and IOleDocumentSite. To keep my discussion as short as possible, my example MyCustomWebBrowser doesn’t support IOleDocumentSite or IDocHostShowUI. Using the guidelines shown for supporting IOleObject, IOleClientSite, and IDocHostUIHandler, you should be able to add support for IDocHostShowUI if you need it.

Returning values from methods invoked through a COM interface

When you import a COM method into managed code, the imported method’s signature is different from the original one. For example, Listing 8-15 shows a native COM method and its equivalent C# method.

Listing 8-15:: A COM Method and Its Equivalent C# Method

// the IDL for a COM method

HRESULT ShowContextMenu([in]      ContextMenuTarget dwContext, 
                        [in, out] POINT* pPOINT, 
                        [in]      IOleCommandTarget* pCommandTarget, 
                        [in]      IDispatch* HTMLTagElement);
// the C# declaration of a COM method

public void ShowContextMenu(
            MsHtmlCustomization.ContextMenuTarget dwContext, 
            ref MsHtmlCustomization.POINT pPoint, 
            MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
            object HTMLTagElement); 

The C# method uses a more friendly notation that is much easier to deal with. A potential problem with the new C# method is the absence of an HRESULT return value. While COM methods are expected to return an HRESULT to indicate success or failure, the translated C# methods don’t appear to give you access to the HRESULT. As you can see, the C# version of the method returns void, while the IDL version returns an HRESULT.

When you implement COM interfaces using managed code, there are times when you need to set the HRESULT value returned. For example, IDocHostUIHandler.ShowContextMenu uses the HRESULT to return an important value to the caller. Normally the Runtime Callable Wrapper class takes care of the HRESULT automatically like this: If a managed method throws an exception, the value S_FALSE is returned to COM; otherwise, the value S_OK is returned.

If you want to directly control the value returned back to COM, using HRESULT to return a parameter, you need to throw a special exception called COMException. When you throw this type of exception, the RCW intercepts it and uses its parameter as the HRESULT returned to COM. Listing 8-16 shows you how you use COMException.

Listing 8-16:: Using COMException to Return HRESULT Values to COM

const int Ok = 0;
const int Error = 1;
// use one of the following statements

throw new COMException("", Ok);    // HRESULT returned: S_OK

throw new COMException("", Error);  // HRESULT returned: S_FALSE

Most OLE and COM methods use the HRESULT to signal success or failure of execution. In those few cases in which the HRESULT is used to return a parameters, just throw a COMException.

Common customizations

Although you can customize most user interface features of MsHtml, some customizations are in high demand, so I’ll discuss them in this separate section to make them easier to find in the book. For the other types of customization, you’ll need to look through the later sections of the chapter that discuss the individual callback methods that MsHtml invokes.

Removing the vertical scroll bar

This is probably the number one customization that developers ask for. When MsHtml displays a page, a vertical scroll bar is always shown, regardless of whether the page is completely visible or not. If the page fits on the screen, the vertical scroll bar appears with no thumb, as shown in Figure 8-18.

Figure 8-18: The vertical scroll bar that MsHtml shows by default on all HTML pages

Internet Explorer also exhibits this behavior, which is to be expected, since it also uses MsHtml internally to display HTML content. No matter how much you resize the window, the vertical scroll bar always appears. One way to remove the scroll bar, if you have control over the content displayed, involves setting an attribute of the <body> element of the page. Using this approach, you don’t need to write any programming code at all. Just add the scroll attribute to the page’s <body> tag like this:

<BODY scroll="NO">

That’s it. The page will then appear with no scroll bars, as shown in Figure 8-19.

Figure 8-19: Displaying HTML pages with no vertical scroll bar

The problem with this approach is that it requires you to change the HTML code of the page displayed — something that is not always feasible. A better way to remove the vertical scrollbar is by using one of the IDocHostUIHandler callbacks. When MsHtml interacts with the user interface, it frequently makes calls to these callback methods to see how to proceed. By implementing the method GetHostInfo(), you can disable the vertical scroll bar. The nice thing about this programmatic customization is that it doesn’t require you to make any changes to the content of the HTML page being displayed. Listing 8-17 shows how the GetHostInfo() method needs to look to turn off the vertical scroll bar.

Listing 8-17:: Programmatically Hiding the Vertical Scroll Bar

public void GetHostInfo(ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo)
{
  theHostUIInfo.dwFlags |= DOCHOSTUIFLAG.DOCHOSTUIFLAG_SCROLL_NO;
}

The code does hide the vertical scroll bar, but is not quite sufficient to prevent users from scrolling. How could they scroll without scroll bars? All they have to do is click the mouse somewhere in the HTML page and drag it off the end of the window. MsHtml thinks the user wants to select text and scrolls to the right or downwards. To disable this behavior, you need to add the DOCHOSTUIFLAG_DIALOG flag to the value returned from GetHostInfo(), as shown in Listing 8-18.

Listing 8-18:: Preventing Users from Scrolling an HTML Document

public void GetHostInfo(ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo)
{
  // turn two flags on

  theHostUIInfo.dwFlags |= (DOCHOSTUIFLAG.DOCHOSTUIFLAG_SCROLL_NO |
                            DOCHOSTUIFLAG.DOCHOSTUIFLAG_NO3DBORDER |
                            DOCHOSTUIFLAG.DOCHOSTUIFLAG_DIALOG);
}

In case you’re wondering, the name DOCHOSTUIFLAG_DIALOG comes from the fact that the flag is generally used to display an HTML page inside an ordinary dialog box. In such a case, you normally don’t want users to be able to scroll around the dialog box contents.

The GetHostInfo() method enables you to control a wide variety of other features. For details, see the section titled “GETHOSTINFO”, later in this chapter. Keep in mind that if you disable the vertical scroll bar, users won’t be able to scroll downwards on HTML pages even if the documents extend beyond the bottom of the window.

Customizing the context menu

Another common request from developers is a way to hide or customize the context menu that appears when users right-click on an HTML page. Figure 8-20 shows the default context menu shown by MsHtml.

Figure 8-20: The default MsHtml context menu

The items listed in the menu depend on what type of element was right-clicked in the HTML page. The most popular reason for hiding the context menu is to prevent users from choosing the View Source command to gain access to the HTML code of the page. The proper way to control MsHtml’s context menu is through the ShowContextMenu() callback. This callback gives you complete control over the context menu, enabling you to do the following:

  • Hide the context menu completely.

  • Display your own context menu.

Let’s take a look at each of these options. To hide the context menu altogether, just return S_OK as the HRESULT from ShowContextMenu(), as shown in Listing 8-19.

Listing 8-19:: Preventing MsHtml’s Context Menu from Appearing

public void ShowContextMenu(
            MsHtmlCustomization.ContextMenuTarget dwContext, 
            ref MsHtmlCustomization.POINT pPoint, 
            MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
            object HTMLTagElement) 
{
  const int Ok = 0;
  throw new COMException("", Ok); // returns HRESULT = S_OK;

}

That was easy. Returning S_OK from the callback is your way of telling MsHtml that your host code handled the context menu and MsHtml is to take no further action. Whether your callback code displayed a context menu of its own or not is important to MsHtml.

To show your own context menu, you can use the pPoint parameter to get the position at which the user right-clicked the mouse. Using a standard .NET Framework ContextMenu component, you can show your custom menu at the same location where MsHtml would have displayed the default menu. Listing 8-20 shows an example.

Listing 8-20:: Replacing the MsHtml Context Menu with a Custom Menu

public void ShowContextMenu(
            MsHtmlCustomization.ContextMenuTarget dwContext, 
            ref MsHtmlCustomization.POINT pPoint, 
            MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
            object HTMLTagElement) 
{
  // show our custom context menu

  Point p = new Point(pPoint.x, pPoint.y);
  p = PointToClient(p); 
  myCustomContextMenu.Show(this, p);
  // tell MsHtml that we handled the context menu ourselves

  const int Ok = 0;
  throw new COMException("", Ok);
}

The parameter pPoint passed to ShowContextMenu() is the location right-clicked by the user. The point is in screen coordinates, so you need to call PointToClient to convert it to client coordinates. I created a simple menu with one command labeled Print. Figure 8-21 shows how the context menu appears on the browser.

Figure 8-21: Displaying a custom context menu

In the ShowContextMenu() callback, you may want to add logic that displays a custom menu under certain conditions, and the standard MsHtml menu in others. To make MsHtml show its own default context menu, throw a COMException as shown in Listing 8-21.

Listing 8-21:: Telling MsHtml to Use Its Own Default Context Menu

public void ShowContextMenu(
            MsHtmlCustomization.ContextMenuTarget dwContext, 
            ref MsHtmlCustomization.POINT pPoint, 
            MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
            object HTMLTagElement) 
{
  // tell MsHtml to show its default context menu

  const int Error = 1;
  throw new COMException("", Error); // returns HRESULT = S_FALSE;

}

Preventing new windows from being opened

Another default behavior of MsHtml that you may want to disable is allowing a new browser window to be opened with an accelerator key. The built-in behavior of MsHtml is to open the current document in a new window when the Ctrl-N accelerator key is pressed. To disable this behavior, all you need to do is add some code to a callback method. Any time an accelerator key is pressed (while MsHtml has the input focus), it will call the callback method TranslateAccelerator(), passing a message containing the key code for the accelerator key. The value returned in the HRESULT from the callback tells MsHtml what to do. The value S_OK tells MsHtml to handle the accelerator. If you don’t want MsHtml to process the accelerator, return the value S_FALSE. Listing 8-22 shows how to prevent all accelerators from being processed.

Listing 8-22:: Disabling All Accelerator Keys

public void TranslateAccelerator(
            ref MsHtmlCustomization.MSG lpMsg, 
            ref MsHtmlCustomization.UUID pguidCmdGroup, 
            int nCmdID) 
{
  // squelch all accelerators

  const int Error = 1;
  throw new COMException("", Error); // HRESULT = S_FALSE;

}

If you only want to disable certain accelerators, you need to use the lpMsg parameter to see which keys were pressed. Listing 8-23 shows how to disable only the Ctrl-N accelerator.

Listing 8-23:: Disabling Only the Ctrl-N Accelerator

public void TranslateAccelerator(
            ref MsHtmlCustomization.MSG lpMsg, 
            ref MsHtmlCustomization.UUID pguidCmdGroup, 
            int nCmdID) 
{
  const int WM_KEYDOWN = 0x0100;
  const int VK_CONTROL = 0x11;
  if (lpMsg.message != WM_KEYDOWN)
    // don't disable

    throw new COMException("", 1); // returns HRESULT = S_FALSE

  lpMsg.wParam &= 0xFF; // get the virtual keycode

  if (lpMsg.wParam == 'N')
  if (GetAsyncKeyState(VK_CONTROL) < 0)
    // disable the Ctrl-N accelerator

    throw new COMException("", 0); // returns HRESULT = S_OK

  // allow everything else

  throw new COMException("", 1); // returns HRESULT = S_FALSE

}

The code makes use of the native Windows API method GetAsyncKeyState() to get the status of the Ctrl key. The method is imported using the code shown in Listing 8-24.

Listing 8-24:: Importing the Windows API Method GetAsyncKeyState()

[DllImport("User32.dll")]
public static extern short GetAsyncKeyState(int vKey);

I’ve taken you through some of the most common types of customization you can apply to MsHtml, but there are many more. In the following sections, I’ll describe them in terms of the callback methods that are exposed to MsHtml through the IDocHostUIHandler interface.

IDocHostUIHandler methods in detail

In the following sections, I’ll describe the various methods of the IDocHostUIHander interface. The methods appear in alphabetical order.

The IDocHostUIHandler methods are called when the WebBrowser component is showing an HTML document using MsHtml. If a different document type is loaded, such as a Word or PDF file, IDocHostUIHandler methods won’t necessarily be called.

EnableModeless

This method has the signature:

public void EnableModeless(int fEnable);

EnableModeless() is called at various times by MsHtml to tell you to disable any modeless dialogs you might have in your host component. For example, when MsHtml is about to display an error message, it calls EnableModeless with fEnable set to 0. After the user closes the error message, MsHtml calls EnableModeless() again with fEnable set to 1, telling you that it’s OK to enable any modeless dialog boxes you may have. Listing 8-25 is a simple example that just logs calls to EnableModeless() to the Trace output window.

Listing 8-25:: A Simple Example with EnableModeless()

using System.Diagnostics;
public void EnableModeless(int fEnable) {
  int i = fEnable;
  Trace.WriteLine("EnableModeless: fEnable= " + i);
}

FilterDataObject

This method has the signature:

public MsHtmlCustomization.IDataObject
       FilterDataObject(MsHtmlCustomization.IDataObject pDO); 

The DataObjects in question are those used typically with Clipboard operations. At various times, MsHtml may call FilterDataObject to let the host component see what type of data is about to be handled. To prevent handling, return the value null. To allow handling, return the pDO object, as show in Listing 8-26.

Listing 8-26:: Allowing All Data Types to Be Handled

public MsHtmlCustomization.IDataObject
       FilterDataObject(MsHtmlCustomization.IDataObject pDO) 
{
  return pDO;
}

GetDropTarget

This method has the signature:

public MsHtmlCustomization.IdropTarget
       GetDropTarget(MsHtmlCustomization.IDropTarget pDropTarget) 

During Drag and Drop operations, when the user drops an object on a target object, MsHtml calls GetDropTarget(). Using this method, you can supply an alternative target. It is unusual to change the drop target. Listing 8-27 shows how to just accept the default target.

Listing 8-27:: Accepting the Default Drop Target

public MsHtmlCustomization.IdropTarget
       GetDropTarget(MsHtmlCustomization.IDropTarget pDropTarget) 
{
  return pDropTarget;
}

GetExternal

This method has the signature:

public object GetExternal();

MsHtml calls this method to obtain the IDispatch interface of the host component (MainForm, in my case). If the host doesn’t implement IDispatch, it must return null. Listing 8-28 shows how you would return the IDispatch if your host did implement it.

Listing 8-28:: Returning the IDispatch Interface

public object GetExternal() 
{
  return this as IDispatch;
}

If you need to implement the IDispatch interface in a class, the managed code wrapper for IDispatch is available in the file StdOle.dll in the folder:

C:\Program Files\Microsoft.NET\Primary Interop Assemblies

GetHostInfo

This is an important customization callback. The method has the signature:

public void GetHostInfo(
            ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo);

The parameter passed in is a struct that looks like the code in Listing 8-29.

Listing 8-29:: The Equivalent C# Code for the DOCHOSTUIINFO struct

public struct DOCHOSTUIINFO
{
  public uint cbSize;
  public uint dwFlags;
  public uint dwDoubleClick;
  public uint pchHostCss;
  public uint pchHostNS;
};

cbSize is the length of the struct in bytes. dwFlags is the most important field, and is described later in detail. dwDoubleClick is where you specify what to show in MsHtml when the user double-clicks the mouse. Possible values are shown in Listing 8-30.

Listing 8-30: The Values that Can Be Assigned to dwDoubleClick

public enum DOCHOSTUIDBLCLK: uint
{
  DOCHOSTUIDBLCLK_DEFAULT         = 0,
  DOCHOSTUIDBLCLK_SHOWPROPERTIES  = 1,
  DOCHOSTUIDBLCLK_SHOWCODE        = 2
};

Table 8-1 describes the values.

Table 8-1
Allowed Values for DOCHOSTUIINFO.dwDoubleClick

Value Meaning

DOCHOSTUIDBLCLK_DEFAULT

Perform the default double-click action.

DOCHOSTUIDBLCLK_SHOWPROPERTIES

Show the properties of the double-clicked item.

DOCHOSTUIDBLCLK_SHOWCODE

Show the code for the double-clicked item.


Before you get too excited about controlling the double-click action with GetHostInfo(), let me inform you that this feature appears to have been disabled in MsHtml.

Getting back to my description of DOCHOSTUIINFO, the field pchHostCss references the Cascading Style Sheet (CSS) used to layout the current HTML page.

The field pchHostNS references a semicolon-delimited list of namespaces used on the page.

By far the most useful field of DOCHOSTUIINFO is dwFlags, which defines a fairly long list of flags you can control to change MsHtml interface elements. Table 8-2 describes each flag.

Table 8-2
Flags Available in DOCHOSTUIINFO.dwFlags

Flag Name Effect When Property Is Set

DOCHOSTUIFLAG_DIALOG

Prevents the user from selecting text. If you don’t want users to be able to scroll content by dragging the mouse, use this flag.

DOCHOSTUIFLAG_DISABLE_HELP_MENU

Disables the right mouse button pop-up menu.

DOCHOSTUIFLAG_NO3DBORDER

Disables the 3D border around the HTML document displayed.

DOCHOSTUIFLAG_SCROLL_NO

Turns off both vertical and horizontal scroll bars. Users will then only be able to see the part of the HTML document that fits in the window. They will still be able to scroll the window by dragging the mouse off the document. To prevent this last behavior, include the flag DOCHOSTUIFLAG_DIALOG.

DOCHOSTUIFLAG_DISABLE_SCRIPT_INACTIVE

Disables all scripts from being run while a page is being loaded.

DOCHOSTUIFLAG_OPENNEWWIN

Forces WebBrowser to open a new Internet Explorer window if a link is clicked.

DOCHOSTUIFLAG_FLAT_SCROLLBAR

Disables the 3D look on all scroll bars. If no scroll bars are visible, the property has no effect.

DOCHOSTUIFLAG_DIV_BLOCKDEFAULT

When the user edits the HTML text on the screen and presses the Enter key, this property makes MsHtml insert a <DIV> tag in the HTML code, rather than the default <P> tag.

DOCHOSTUIFLAG_ACTIVATE_CLIENTHIT_ONLY

Tells MsHtml to take the input focus only when the user clicks the mouse in the client area. By default, the component will become focused even if the user clicks a non-client area, such as a scroll bar.

DOCHOSTUIFLAG_OVERRIDEBEHAVIORFACTORY

Disables DHTML behaviors IE 5 and later. (For a discussion of behaviors, see http://msdn.microsoft.com/library/periodic/period99/HTMLbehaviors.htm.)

DOCHOSTUIFLAG_CODEPAGELINKEDFONTS

This flag was added only to provide a common look and feel across the two Microsoft products, Outlook Express and Internet Explorer. It applies to Outlook Express 4 and Internet Explorer 5 (or later versions). You’ll probably never use this flag.

DOCHOSTUIFLAG_URL_ENCODING_DISABLE_UTF8

Disables the use of UTF8 character coding for URLs that have characters that are not in the UTF8 set. By default, MsHtml will always try to use UTF8. Applies to IE 5 and later.

DOCHOSTUIFLAG_URL_ENCODING_ENABLE_UTF8

Forces the use of UTF8 character coding with URLs that have characters that are not in the UTF8 set. By default, MsHtml will always try to use UTF8. Applies to IE 5 and later.

DOCHOSTUIFLAG_ENABLE_FORMS_AUTOCOMPLETE

Enables the AutoComplete feature for Forms, which by default is enabled. The value set for this property will be ignored if the user has disabled AutoComplete for Forms in Internet Explorer. To disable AutoComplete for Forms in IE, select the menu command Tools @@> Internet Options, switch to the Content tab, click the AutoComplete button and uncheck the Forms checkbox.


You can combine multiple flags, as shown in Listing 8-31.

Listing 8-31:: Setting Multiple Flags in GetHostInfo()

public void GetHostInfo(
            ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo)
{
  // turn three flags on

  theHostUIInfo.dwFlags |= 
       (DOCHOSTUIFLAG.DOCHOSTUIFLAG_SCROLL_NO  |
        DOCHOSTUIFLAG.DOCHOSTUIFLAG_NO3DBORDER |
        DOCHOSTUIFLAG.DOCHOSTUIFLAG_DISABLE_SCRIPT_INACTIVE);
 }

You can set any flags you need, by combining them together with a Boolean OR operation.

GetOptionKeyPath

This method is called to retrieve the Registry path to use for storing user preferences. It is rarely used.

HideUI

If the host component draws UI elements, such as toolbars or menus, related to the MsHtml state, you need to make sure your UI elements are shown only at the proper time. When the HideUI() callback occurs, you need to hide those elements. At some later time, MsHtml will call the ShowUI() method to let you restore the hidden elements back on the screen.

OnDocWindowActivate

This method has the signature:

public void OnDocWindowActivate(int fActivate);

The method is called at various times by MsHtml to tell you when the HTML document being displayed is activated or deactivated. A document is basically considered activated when it has the input focus. Windows shows the currently active window with a different caption bar color from inactive windows. The reason MsHtml calls to inform you when the Active status changes is to allow your host component to make any necessary changes to its UI.

OnFrameWindowActivate

This method has the signature:

public void OnFrameWindowActivate(int fActivate);

The method is called at various times by MsHtml to tell you when the top-level frame window containing MsHtml is activated or deactivated. Use this callback method to change any UI elements in the host component that appear differently when the window is activated versus deactivated.

ResizeBorder

This method has the signature:

public void ResizeBorder(ref MsHtmlCustomization.RECT prcBorder, 
                         int pUIWindow, int fFrameWindow);

This method is useful if your host component allows MsHtml to become inplace-activated. When an embedded component becomes inplace-activated, the host normally draws a hatched border around the embedded component. Inplace-activation used to be considered a really cool technology that used menu-merging and OLE-nested documents. Outside of products like Microsoft Office, inplace-activation just isn’t used that much, so the requirement to support ResizeBorder() is somewhat unusual.

ShowContextMenu

This method has the signature:

public void ShowContextMenu(
            MsHtmlCustomization.ContextMenuTarget dwContext, 
            ref MsHtmlCustomization.POINT pPoint, 
            MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
            object HTMLTagElement); 

This is commonly used callback method, because it allows you to customize how pop-up menus behave with MsHtml. You basically have three options:

  • Allow MsHtml to display its context menu.

  • Disable context menus altogether.

  • Hide MsHtml’s menu and display your own.

To allow MsHtml to show its normal context menu, return the value S_FALSE by throwing a COMException as shown in Listing 8-32.

Listing 8-32:: Allowing MsHtml to Show Its Default Context Menu

public void ShowContextMenu(
            MsHtmlCustomization.ContextMenuTarget dwContext, 
            ref MsHtmlCustomization.POINT pPoint, 
            MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
            object HTMLTagElement) 
{
  const int Error = 1;
  throw new COMException("", Error); // returns HRESULT = S_FALSE

}

To prevent MsHtml from showing its context menu, return the value S_OK by throwing a COMException as shown in Listing 8-33.

Listing 8-33:: Disabling Context Menus Altogether

public void ShowContextMenu(
            MsHtmlCustomization.ContextMenuTarget dwContext, 
            ref MsHtmlCustomization.POINT pPoint, 
            MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
            object HTMLTagElement) 
{
  const int Ok = 0;
  throw new COMException("", Ok); // returns HRESULT = S_OK

}

To show your own custom menu, use a ContextMenu component in the callback and return the value S_OK by throwing a COMException as shown in Listing 8-34.

Listing 8-34: Disabling Context Menus Altogether

public void ShowContextMenu(
            MsHtmlCustomization.ContextMenuTarget dwContext, 
            ref MsHtmlCustomization.POINT pPoint, 
            MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
            object HTMLTagElement) 
{
  Point p = new Point(pPoint.x, pPoint.y);
  p = PointToClient(p); 
  myCustomContextMenu.Show(this, p);
  const int Ok = 0;
  throw new COMException("", Ok); // return HRESULT = S_OK, so MsHtml

                                  // doesn�t display its own menu

}

Notice the call to PointToClient(). This call is necessary, because the pPoint is in screen coordinates, while ContextMenu.Show() requires client coordinates. By setting up your own ContextMenu, you can customize the pop-up menu in any way you want.

ShowUI

If the host component (MainForm in my case) needs to draw UI elements such as toolbars or menus related to MsHtml, this callback tells you it’s okay to show them. At some later time, MsHtml will call the HideUI() and UpdateUI() methods where you should be prepared to hide or refresh your UI elements. You don’t need to handle ShowUI() unless your UI elements are related to MsHtml’s state.

TranslateAccelerator

When the user presses an accelerator key, such as Ctrl-O or Ctrl-P, MsHtml calls TranslateAccelerator() to see what you want to do. This method has the signature:

public void TranslateAccelerator(
            ref MsHtmlCustomization.MSG lpMsg, 
            ref MsHtmlCustomization.UUID pguidCmdGroup, 
            int nCmdID); 

Accelerator keys are keys that activate menu commands. Table 8-3 shows the important accelerator keys supported by MsHtml.

Table 8-3
Main Accelerator Keys Supported by MsHtml

Accelerator Description

Ctrl-N

Opens the current HTML document in a new WebBrowser window.

Crtl-P

Displays a Print dialog box for printing the HTML document.

Ctrl-A

Selects the entire contents of the HTML document.

Crtl-F

Displays a Find dialog box for searching the HTML document.

F5, Ctrl-F5

Refreshes the currently loaded HTML document.


The accelerators most often disabled are Ctrl-N and Ctrl-P. Listing 8-35 shows how to disable them.

Listing 8-35:: Disabling the Ctrl-N and Ctrl-P Accelerators

public void TranslateAccelerator(
            ref MsHtmlCustomization.MSG lpMsg, 
            ref MsHtmlCustomization.UUID pguidCmdGroup, 
            int nCmdID) 
{
  const int Ok = 0;
  const int Error = 1;
  const int WM_KEYDOWN = 0x0100;
  const int VK_CONTROL = 0x11;
  if (lpMsg.message != WM_KEYDOWN)
    // allow message

    throw new COMException("", Error); // returns HRESULT = S_FALSE

  if (GetAsyncKeyState(VK_CONTROL) >= 0)
    // Ctrl key not pressed: allow message

    throw new COMException("", Error); // returns HRESULT = S_FALSE

  // disable the Ctrl-N and Ctrl-P accelerators

  lpMsg.wParam &= 0xFF; // get the virtual keycode

  if ( (lpMsg.wParam == 'N') || ((lpMsg.wParam == 'P')) )
    throw new COMException("", Ok); // returns HRESULT = S_OK

  // allow everything else

  throw new COMException("", Error); // returns HRESULT = S_FALSE

}

TranslateUrl

This method is called when the user clicks a hyperlink in an HTML document. Before passing the link’s URL to the WebBrowser, MsHtml calls TranslateUrl() to allow the host component to modify the URL.

UpdateUI

If you draw UI elements that are based on the state of MsHtml, this callback tells you when to refresh those elements. This callback will occur anytime a significant state occurs in MsHtml.

The Complete Code

I’ve shown you lots of bits and pieces of code. Listing 8-36 shows the complete code for MainForm of MyCustomWebBrowser.

Listing 8-36:: The Complete Code for MainForm of MyCustomWebBrowser

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Runtime.InteropServices;
using System.Diagnostics;
using MsHtmlCustomization;
namespace MyCustomWebBrowser
{
  public class MainForm : System.Windows.Forms.Form,
                            IOleClientSite,
                            IDocHostUIHandler 
  {
    private System.Windows.Forms.Panel panel1;
    private System.Windows.Forms.ToolBar toolBar1;
    private System.Windows.Forms.ImageList imageList1;
    private System.Windows.Forms.ToolBarButton toolBarButtonBack;
    private System.Windows.Forms.ToolBarButton toolBarButtonForward;
    private System.Windows.Forms.ToolBarButton toolBarButtonStop;
    private System.Windows.Forms.ToolBarButton toolBarButtonRefresh;
    private System.Windows.Forms.ToolBarButton toolBarButtonHome;
    private System.Windows.Forms.ToolBarButton toolBarButtonSearch;
    private System.Windows.Forms.ToolBarButton toolBarButtonPrint;
    private AxSHDocVw.AxWebBrowser axWebBrowser1;
    private System.Windows.Forms.TextBox textBoxAddress;
    private System.ComponentModel.IContainer components;
    ArrayList urlsVisited = new ArrayList();
    private System.Windows.Forms.ContextMenu myCustomContextMenu;
    private System.Windows.Forms.MenuItem menuItemPrint;
    int currentUrlIndex = -1;  // no sites visited initially

    
    public MainForm()
    {
      InitializeComponent();
      // tell WebBrowser that we are its host

      object obj = axWebBrowser1.GetOcx();
      IOleObject oc = obj as IOleObject;
      oc.SetClientSite(this);
      toolBarButtonBack.Enabled = false;
      toolBarButtonForward.Enabled = false;
      toolBarButtonStop.Enabled = false;
      toolBarButtonRefresh.Enabled = false;
      toolBarButtonHome.Enabled = false;
      toolBarButtonSearch.Enabled = false;
      toolBarButtonPrint.Enabled = false;
      
      axWebBrowser1.GoHome();
    }
    protected override void Dispose( bool disposing )
    {
      // standard wizard-generated code

    }
    private void InitializeComponent()
    {
      // standard wizard-generated code...

      this.axWebBrowser1 = new AxSHDocVw.AxWebBrowser();
      this.panel1 = new System.Windows.Forms.Panel();
      this.textBoxAddress = new System.Windows.Forms.TextBox();
      this.myCustomContextMenu = new System.Windows.Forms.ContextMenu();
      this.menuItemPrint = new System.Windows.Forms.MenuItem();
      
      // 

      // axWebBrowser1

      // 

      this.axWebBrowser1.Dock = System.Windows.Forms.DockStyle.Fill;
      this.axWebBrowser1.Enabled = true;
      this.axWebBrowser1.Location = new System.Drawing.Point(0, 28);
      this.axWebBrowser1.OcxState =
                 ((System.Windows.Forms.AxHost.State)
                 (resources.GetObject("axWebBrowser1.OcxState")));
      this.axWebBrowser1.Size = new System.Drawing.Size(394, 245);
      this.axWebBrowser1.TabIndex = 1;
      this.axWebBrowser1.NavigateError += new
           AxSHDocVw.DWebBrowserEvents2_NavigateErrorEventHandler(
                                  this.axWebBrowser1_NavigateError);
      this.axWebBrowser1.NavigateComplete2 += new  
           AxSHDocVw.DWebBrowserEvents2_NavigateComplete2EventHandler(
                                  this.axWebBrowser1_NavigateComplete2);
      this.axWebBrowser1.BeforeNavigate2 += new 
           AxSHDocVw.DWebBrowserEvents2_BeforeNavigate2EventHandler(
                                  this.axWebBrowser1_BeforeNavigate2);
      // more wizard-generated code...

    }
    [STAThread]
    static void Main() 
    {
      Application.Run(new MainForm());
    }
    private void toolBar1_ButtonClick(object sender,
         System.Windows.Forms.ToolBarButtonClickEventArgs e)
    {
      Cursor.Current = Cursors.WaitCursor;
      try 
      {
        if (e.Button == toolBarButtonBack)
          axWebBrowser1.GoBack();
        else if (e.Button == toolBarButtonForward)
          axWebBrowser1.GoForward();
        else if (e.Button == toolBarButtonStop) 
        {
          axWebBrowser1.Stop();
          toolBarButtonStop.Enabled = false;
        }
        else if (e.Button == toolBarButtonSearch)
          axWebBrowser1.GoSearch(); 
        else if (e.Button == toolBarButtonPrint)
          PrintPage();
        else if (e.Button == toolBarButtonRefresh)
        {
          object REFRESH_COMPLETELY = 3;
          axWebBrowser1.Refresh2(ref REFRESH_COMPLETELY); 
        }
        else if (e.Button == toolBarButtonHome)
          axWebBrowser1.GoHome();
      }
      finally 
      {
        Cursor.Current = Cursors.Default;
      }
    }
    
    private bool IsPrinterEnabled()
    {
      int response = 
        (int) axWebBrowser1.QueryStatusWB(SHDocVw.OLECMDID.OLECMDID_PRINT);
      return (response & (int) SHDocVw.OLECMDF.OLECMDF_ENABLED) != 0 ? 
        true : false;
    }
    private void PrintPage()
    {
      object o = "";
      // constants useful when printing

      SHDocVw.OLECMDID Print = SHDocVw.OLECMDID.OLECMDID_PRINT;
      // use this value to print without prompting

      // SHDocVw.OLECMDEXECOPT PromptUser =

      //  SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_PROMPTUSER;

      SHDocVw.OLECMDEXECOPT DontPromptUser =
        SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER;
  
      if (!IsPrinterEnabled() ) return;
      // print without prompting user

      axWebBrowser1.ExecWB(Print, DontPromptUser, ref o, ref o);
      // to prompt the user with printer settings

      // axWebBrowser1.ExecWB(Print, PromptUser, ref o, ref o);

    }
    
    public void GotoURL(String theURL) 
    {
      try 
      {
        Cursor.Current = Cursors.WaitCursor;
        Object o = null;
        axWebBrowser1.Navigate(theURL, ref o, ref o, ref o, ref o);
      } 
      finally 
      {
        Cursor.Current = Cursors.Default;
      }
    }
    private void textBoxAddress_KeyDown(object sender,
                 System.Windows.Forms.KeyEventArgs e)
    {
      if (e.KeyCode == Keys.Return) 
        GotoURL(textBoxAddress.Text);
    }
    private void axWebBrowser1_BeforeNavigate2(object sender,
            AxSHDocVw.DWebBrowserEvents2_BeforeNavigate2Event e)
    {
      toolBarButtonStop.Enabled = true;
      Cursor.Current = Cursors.WaitCursor;
    }
    private void axWebBrowser1_NavigateComplete2(object sender,
            AxSHDocVw.DWebBrowserEvents2_NavigateComplete2Event e)
    {
      Cursor.Current = Cursors.Default;
 
      toolBarButtonStop.Enabled = false;
      toolBarButtonHome.Enabled = true;
      toolBarButtonSearch.Enabled = true;
      toolBarButtonRefresh.Enabled = true;
      // update the URL displayed in the address bar

      String s = e.uRL.ToString();
      textBoxAddress.Text = s;
      // update the list of visited URLs

      int i = urlsVisited.IndexOf(s);
      if (i >= 0)
        currentUrlIndex = i;
      else 
        currentUrlIndex = urlsVisited.Add(s);
      // enable / disable the Back and Forward buttons

      toolBarButtonBack.Enabled = (currentUrlIndex == 0) ? false : true;
      toolBarButtonForward.Enabled = 
        (currentUrlIndex >= urlsVisited.Count-1) ? false : true;
      // set the state of the Print button

      toolBarButtonPrint.Enabled = IsPrinterEnabled();
    }
    private void axWebBrowser1_NavigateError(object sender,
            AxSHDocVw.DWebBrowserEvents2_NavigateErrorEvent e)
    {
      Cursor.Current = Cursors.Default;
      toolBarButtonStop.Enabled = false;
      toolBarButtonHome.Enabled = true;
      toolBarButtonSearch.Enabled = true;
      toolBarButtonRefresh.Enabled = true;
    }
    
    // implement IOleClientSite methods

    void IOleClientSite.SaveObject() {}
    void IOleClientSite.GetMoniker(uint dwAssign, 
                                   uint dwWhichMoniker, 
                                   object ppmk) {}
    void IOleClientSite.GetContainer(object ppContainer) 
    {
      ppContainer = this;
    }
    void IOleClientSite.ShowObject() {}
    void IOleClientSite.OnShowWindow(bool fShow) {}
    void IOleClientSite.RequestNewObjectLayout() {}
    // implement IDocHostUIHandler methods

    public void ShowContextMenu(
                MsHtmlCustomization.ContextMenuTarget dwContext, 
                ref MsHtmlCustomization.POINT pPoint, 
                MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
                object HTMLTagElement) 
    {
      // use this code to show a custom menu

      const int Ok = 0;
      Point p = new Point(pPoint.x, pPoint.y);
      p = PointToClient(p); 
      myCustomContextMenu.Show(this, p);
      throw new COMException("", Ok); // HRESULT = S_OK

      // use this code to let MsHtml shows its menu

      // const int Error = 1;

      // throw new COMException("", Error); // HRESULT = S_FALSE

    }
    public void GetHostInfo(
                ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo)
    {
      // turn two flags on

      theHostUIInfo.dwFlags |= (DOCHOSTUIFLAG.DOCHOSTUIFLAG_SCROLL_NO |
                                DOCHOSTUIFLAG.DOCHOSTUIFLAG_NO3DBORDER);
    }
    
    public void ShowUI(int dwID, object pActiveObject,
                MsHtmlCustomization.IOleCommandTarget pCommandTarget, 
                object pFrame, object pDoc) {}
    public void HideUI() {}
    public void UpdateUI() {}
    
    public void EnableModeless(int fEnable) {
      int i = fEnable;
      Trace.WriteLine("EnableModeless: fEnable= " + i);
    }
    public void OnDocWindowActivate(int fActivate) {}
    public void OnFrameWindowActivate(int fActivate) {}
    
    public void ResizeBorder(ref MsHtmlCustomization.RECT prcBorder, 
                int pUIWindow, int fFrameWindow) {}
    
    
    public void TranslateAccelerator(
                ref MsHtmlCustomization.MSG lpMsg, 
                ref MsHtmlCustomization.UUID pguidCmdGroup, 
                int nCmdID) 
    {
      const int Ok = 0;
      const int Error = 1;
      const int WM_KEYDOWN = 0x0100;
      const int VK_CONTROL = 0x11;
      
      if (lpMsg.message != WM_KEYDOWN)
        // allow message

        throw new COMException("", Error); // returns HRESULT = S_FALSE

      if (GetAsyncKeyState(VK_CONTROL) >= 0)
        // Ctrl key not pressed: allow message

        throw new COMException("", Error); // returns HRESULT = S_FALSE

      // disable the Ctrl-N and Ctrl-P accelerators

      lpMsg.wParam &= 0xFF; // get the virtual keycode

      if ( (lpMsg.wParam == 'N') || ((lpMsg.wParam == 'P')) )
        throw new COMException("", Ok); // returns HRESULT = S_OK

      // allow everything else

      throw new COMException("", Error); // returns HRESULT = S_FALSE

    }
    
    public void GetOptionKeyPath(ref int pchKey, int dw) {}
    public MsHtmlCustomization.IdropTarget
           GetDropTarget(MsHtmlCustomization.IDropTarget pDropTarget) 
    {
      return pDropTarget;
    }
    public object GetExternal() 
    {
      return null;
    }
    public int TranslateUrl(int dwTranslate, int pchURLIn) {return 0;}
    
    public MsHtmlCustomization.IDataObject
           FilterDataObject(MsHtmlCustomization.IDataObject pDO) 
    {
      return pDO;
    }
    
    [DllImport("User32.dll")]
    public static extern short GetAsyncKeyState(int vKey);
  }
}

Summary

By the sheer number of ways WebBrowser can be customized, you must realize by now how important this component is, not only for Microsoft, but also for your own applications. Once you import the proper COM interfaces into your managed code, WebBrowser is your friend. Moreover, it’s already installed on practically every Windows machine on the planet, is flexible beyond words, and is a pleasure to use.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here