Second Life of a Hungarian SharePoint Geek

August 3, 2010

Creating a simple Internet Explorer add-in that interacts with the HTML events and content of the current document

Filed under: IE add-in — Tags: — Peter Holpar @ 23:07

One of my favorite area of interest is to create applications that help users in their work via integrating content from different systems. Since significant amount of content is available on web servers, most of the users run web browsers to achieve the required information.

When you have to integrate web content with your application there are two main directions. Either you integrate the web browser control into your application or just vice versa, you push your application into the browser itself, typically by creating a browser add-in.

Of course, you can create Java applets, ActiveX controls, Silverlight or Flash applications to make your web content richer, but that is another story.

Since unfortunately there is no out-of-the-box tool in Visual Studio that would make  creation of IE add-ins easier, one should either create the add-in using C++ or get a free or commercial wrapper to work with managed code as described by Christophe Geers in this post in more details.

For my example I chose a commercial product, Add-in Express for Internet Explorer because it provides all of the features I need in a very convenient way. You can find some introductory and more advanced texts, how-to articles and tutorials about the product on their site as well.

I created the sample toolbar using an earlier version of the product (Add-in Express 2008 .NET for Internet Explorer) I had access to and Visual Studio 2008. Hopefully the same code will work with the 2010 version of Add-in Express that supports Visual Studio 2010 as well.

In this post I will show you a simple example for Internet Explorer add-in that interacts with the content and events of the browser. For the sake of the sample I chose two features you might be familiar with from Google Toolbar:

  • Show highlighted text in the search box: This feature displays the selected text from the HTML page to a text box. When the user clicks on the Go button, the text is submitted to Google.
  • WordTranslator: When the user holds the mouse pointer over a word, a pop-up is displayed with the selected word. In this case I omit the actual translation. User can switch this feature off by clicking on the Disable link button in the pop-up, or using the Popup Enabled check box.

See the following images for illustration the add-in in action:

image

image

Let’s see the most interesting challenges and codes of the implementation.

One of the key points in the add-in to capture and handle HTML events, for example when the selection changed, the mouse is moving, down or up and a key is down. First it seemed to be trivial but it turned out that when you handle an event it somehow hides other HTML events to be fired, so standard IE functionality was broken. Fortunately I found this post from Rick Strahl. The article describes the exact symptoms I experienced and led me to the correct track.

  1. private void AttachEvents()
  2. {
  3.     mshtml.DispHTMLDocument document = HTMLDocument as mshtml.DispHTMLDocument;
  4.  
  5.     if (document != null)
  6.     {
  7.         DHTMLEventHandler SelectionChangedHandler = new DHTMLEventHandler(HTMLDocument);
  8.         SelectionChangedHandler.Handler += new DHTMLEvent(this.onselectionchanged);
  9.         document.onselectionchange = SelectionChangedHandler;
  10.  
  11.         DHTMLEventHandler MouseUpHandler = new DHTMLEventHandler(HTMLDocument);
  12.         MouseUpHandler.Handler += new DHTMLEvent(this.onmouseup);
  13.         document.onmouseup = MouseUpHandler;
  14.  
  15.         DHTMLEventHandler MouseMoveHandler = new DHTMLEventHandler(HTMLDocument);
  16.         MouseMoveHandler.Handler += new DHTMLEvent(this.onmousemove);
  17.         document.onmousemove = MouseMoveHandler;
  18.  
  19.         DHTMLEventHandler MouseDownHandler = new DHTMLEventHandler(HTMLDocument);
  20.         MouseDownHandler.Handler += new DHTMLEvent(this.onmousedown);
  21.         document.onmousedown = MouseDownHandler;
  22.  
  23.         DHTMLEventHandler KeyDownHandler = new DHTMLEventHandler(HTMLDocument);
  24.         KeyDownHandler.Handler += new DHTMLEvent(this.onkeydown);
  25.         document.onkeydown = KeyDownHandler;
  26.  
  27.         ResetTimer();
  28.  
  29.     }
  30. }

More about the ResetTimer method later.

The DHTMLEvent delegate and DHTMLEventHandler are in fact the same you find on Rick’s page.

  1. ///
  2. /// Generic HTML DOM Event method handler.
  3. ///
  4. public delegate void DHTMLEvent(IHTMLEventObj e);
  5.  
  6. ///
  7. /// Generic Event handler for HTML DOM objects.
  8. /// Handles a basic event object which receives an IHTMLEventObj which
  9. /// applies to all document events raised.
  10. ///
  11. public class DHTMLEventHandler
  12. {
  13.     public DHTMLEvent Handler;
  14.     HTMLDocument Document;
  15.  
  16.     public DHTMLEventHandler(HTMLDocument doc)
  17.     {
  18.         this.Document = doc;
  19.     }
  20.  
  21.     [DispId(0)]
  22.     public void Call()
  23.     {
  24.         Handler(Document.parentWindow.@event);
  25.     }
  26.  
  27. }

We attach the event handlers by calling the AttachEvents method both in the OnConnect and DocumentComplete events of the toolbar to ensure the cases when the add-in is first loaded into the browser and when a new page is loaded into the browser. If we do not re-attach the event handlers each time a new page is loaded, handlers would refer to a page not existing any more in the browser and events won’t be triggered as expected.

I found that referencing the HTMLDocumentObj when there is no page loaded yet into the browser throws an exception, so I wrapped in into a try / catch block.

  1. public mshtml.HTMLDocument HTMLDocument
  2. {
  3.     get
  4.     {
  5.         try
  6.         {
  7.             return (this.HTMLDocumentObj as mshtml.HTMLDocument);
  8.         }
  9.         catch
  10.         {
  11.             Trace.TraceError("Error getting HTMLDocumentObj");
  12.             return null;
  13.         }
  14.     }
  15. }

When the selection is changed, we set the text of the text box back to the original, empty value:

  1. private void onselectionchanged(mshtml.IHTMLEventObj e)
  2. {
  3.     SelectionText.Text = _defaultText;
  4.     e.returnValue = true;
  5. }

When the mouse button is up we try to get the current selection and interpret it as text. If it is successful, we copy the value into the text box.

  1. private void onmouseup(mshtml.IHTMLEventObj e)
  2. {
  3.     mshtml.IHTMLSelectionObject currentSelection = HTMLDocument.selection;
  4.  
  5.     if ((currentSelection != null) && (currentSelection.type == "Text"))
  6.     {
  7.         mshtml.IHTMLTxtRange range = currentSelection.createRange() as mshtml.IHTMLTxtRange;
  8.         if (range != null)
  9.         {
  10.             SelectionText.Text = range.text;
  11.         }
  12.     }
  13.     e.returnValue = true;
  14. }

By pressing the Go button, the text (if not empty) in the text box will be submitted to Google.

  1. private void GoButton_Click(object sender, EventArgs e)
  2. {
  3.     if (SelectionText.Text.Trim() != String.Empty)
  4.     {
  5.         HTMLDocument.url = "http://www.google.com/search?q=" + HttpUtility.UrlEncode(SelectionText.Text);
  6.     }
  7. }

The event handler for the mouse movement is responsible for tracking the mouse position in the document and for getting the word in the mouse position. It also starts and stops the timer that commands the pop-up window.

  1. private void onmousemove(mshtml.IHTMLEventObj e)
  2. {
  3.     Trace.TraceInformation("onmousemove: x: {0}, y: {1}", e.screenX, e.screenY);
  4.  
  5.     if ((this.Visible) && (this.PopUpEnabled.Checked))
  6.     {
  7.         // we might display a new window as a result of the mouse move that
  8.         // cause the onmousemove event to fire again
  9.         // to avoid the endless circular events we check
  10.         // the location of the mouse pointer
  11.         if (!MousePosition.IsInRange(_selectedWordLocation, POINTER_RANGE))
  12.         {
  13.             ClosePopUpWindow();
  14.  
  15.             mshtml.IHTMLBodyElement body = (mshtml.IHTMLBodyElement)HTMLDocument.body;
  16.             if (body != null)
  17.             {
  18.                 mshtml.IHTMLTxtRange range = body.createTextRange();
  19.                 range.moveToPoint(e.clientX, e.clientY);
  20.                 range.expand("word");
  21.                 if (range != null)
  22.                 {
  23.                     _selectedWord = range.text.Trim();
  24.                     if (String.IsNullOrEmpty(_selectedWord))
  25.                     {
  26.                         Trace.TraceInformation("Stop Timer");
  27.                         _timer.Stop();
  28.                     }
  29.                     else
  30.                     {
  31.                         Trace.TraceInformation("Selected word: {0}", _selectedWord);
  32.  
  33.                         if (!_timer.Enabled)
  34.                         {
  35.                             Trace.TraceInformation("Start Timer");
  36.                             _timer.Start();
  37.                             _selectedWordLocation = MousePosition;
  38.                         }
  39.                     }
  40.  
  41.                 }
  42.             }
  43.         }
  44.     }
  45.     e.returnValue = true;
  46. }

Note, that instead of checking for equality we check for a distance around the original location using an extension method of Point. This is necessary because the mouse move event is not triggered on every minor mouse movement so the actual mouse location might slightly differ from the location we experience in the mouse move event handler. Checking for equality would cause the pop-up display extremely unlikely in the case of a large screen resolution.

  1. public static bool IsInRange(this Point point1, Point point2, int distance)
  2. {
  3.     Trace.TraceInformation("Point1: {0}", point1);
  4.     Trace.TraceInformation("Point2: {0}", point2);
  5.     Trace.TraceInformation("Distance limit: {0}", distance);
  6.  
  7.     double actualDistance = Math.Sqrt(Math.Pow((point1.X – point2.X), 2) + Math.Pow((point1.Y – point2.Y), 2));
  8.     Trace.TraceInformation("Actual distance: {0}", actualDistance);
  9.  
  10.     return (actualDistance < distance);
  11. }

On a timer tick we check if the mouse pointer is still on the original location where it was when the timer was started. If it is and there is a word at the mouse position we display the pop-up.

  1. private void timer_Tick(object sender, EventArgs e)
  2. {
  3.     Trace.TraceInformation("timer_Tick");
  4.  
  5.     _timer.Stop();
  6.  
  7.     mshtml.DispHTMLDocument document = HTMLDocument as mshtml.DispHTMLDocument;
  8.  
  9.     // we display the popup only if the document is completly rendered
  10.     // this is to avoid annoying popups during next page load
  11.     // that are displayed when you click on a link and leave the mouse at the same position
  12.  
  13.     // MousePosition.Equals(_selectedWordLocation) is not adequate
  14.     // see extension functions for the method
  15.  
  16.     if ((MousePosition.IsInRange(_selectedWordLocation, POINTER_RANGE)) && ((document != null) && (document.readyState == "complete")))
  17.     {
  18.         Trace.TraceInformation("Displaying PopUp");
  19.  
  20.         // if there is an existing instance we should close that first
  21.         ClosePopUpWindow();
  22.         _puf = new PopUpForm(_selectedWord, this);
  23.         _puf.Location = _selectedWordLocation;
  24.         _puf.Show();
  25.  
  26.         // reset focus to keep highligted (e.g. selected) text higlighted
  27.         if (document != null)
  28.         {
  29.             document.focus();
  30.         }
  31.     }
  32. }

The code of the PopUpForm is fairly simple. Note, that the Disable link button directly set the value of the related check box of the toolbar.

  1. public partial class PopUpForm : Form
  2. {
  3.     private SelectionToolBar _selectionToolBar;
  4.  
  5.     public PopUpForm(String selectedWord, SelectionToolBar selectionToolBar)
  6.     {
  7.         InitializeComponent();
  8.         SelectedWord.Text = selectedWord;
  9.         _selectionToolBar = selectionToolBar;
  10.     }
  11.  
  12.     private void DisablePopUp_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
  13.     {
  14.         _selectionToolBar.PopUpEnabled.Checked = false;
  15.         Close();
  16.     }
  17. }

When the mouse button or any key is down we reset the timer and hides the pop-up. Note, that we do the same in the DocumentComplete event handler as well (not shown here).

  1. private void onkeydown(mshtml.IHTMLEventObj e)
  2. {
  3.     Trace.TraceInformation("onkeydown");
  4.     ClosePopUpWindow();
  5.     ResetTimer();
  6.     e.returnValue = true;
  7. }
  8.  
  9. private void onmousedown(mshtml.IHTMLEventObj e)
  10. {
  11.     Trace.TraceInformation("onmousedown");
  12.     ClosePopUpWindow();
  13.     ResetTimer();
  14.     e.returnValue = true;
  15. }

See the following methods for details of the calls:

  1. private void ClosePopUpWindow()
  2. {
  3.     if (_puf != null)
  4.     {
  5.         _puf.Close();
  6.         _puf = null;
  7.     }
  8. }
  9.  
  10. private void ResetTimer()
  11. {
  12.     if (_timer != null)
  13.     {
  14.         _timer.Stop();
  15.     }
  16.     _timer = new Timer();
  17.     _timer.Interval = TIMER_LIMIT;
  18.     _timer.Tick += new EventHandler(timer_Tick);
  19. }

As you can see, the code contains a lot of trace method to make it easier to check the working the add-in using DebugView.

You can found the sample solution here. If you would like to work with that it requires the Add-in Express for IE to be installed in your environment.

The solution was tested on Windows Server 2008 / IE7 and Windows Server 2008 R2 / IE8.

Tips for working with Add-in Express:

  • Although the Add-in Express solution may contain a setup project I found that after running the setup at the first time to register the add-in, it is more comfortable to use a post-build command like this to deploy the new .dll to the application folder on each build:
  • copy $(TargetPath) "C:\Program Files\Default Company Name\SelectionAddOnSetup" /y
  • I suggest you to close all instances of IE before building to avoid file locks.
  • Sometimes it happens that despite you close all the IE windows, the target files are locked by a process so the build fails. In this case it is the quickest to kill Windows Explorer process from Task Manager and restart a new instance.

Known issues of the sample toolbar:

  • The toolbar might be not displayed the first time in IE if there is other toolbars. In this case hide other toolbars first and try to disable and enable the add-in again.
  • Sometimes only the title of the toolbar is displayed when you enable it. In this case check that the Lock the Toolbars setting is off in IE.
  • The sample implementation does not handle HTML frames (including IFrames).
  • If the user switches the input focus using the Alt + Tab keys (or Shift + Alt + Tab) the pop-up is displayed over the other application.
  • The Popup Enabled setting is not shared between IE windows and not persisted between browsing sessions.
About these ads

1 Comment »

  1. [...] HTML events. My idea was to enable users to select web page text in the browser, similar like in my former post about IE add-ins, but in this case the application should “read” the selected text instead of simply copying it [...]

    Pingback by Speech enable your web pages using Silverlight, WCF and the Managed Speech API « Second Life of a Hungarian SharePoint Geek — January 9, 2011 @ 23:58


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

The Shocking Blue Green Theme. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

Join 54 other followers

%d bloggers like this: