Search code examples
c#internet-explorerdispose

Close application from captive IE session


My question is: "Can this be done better?" and if so, how? Any ideas?

We need to start a captive IE session from within an "invisible" C# .NET 3.5 application, and quit both the IE session and the "parent" application after processing a certain request.

I've been mucking around with this problem for the last week or so... and this morning I've finally reached what I think is a robust solution; but I'm a bit of a C# noob (though I've been a professional programmer for 10 years), so I'm seeking a second or third opinion; and any other options, critiques, suggestions, or comments... Especially: is SHDocVw still the preferred method of creating a "captive but not imbedded" Internet Explorer session?

As I see things, the tricky bit is disposing of the unmanaged InternetExplorerApplication COM object, so I've wrapped it in an IDisposable class called InternetExplorer

My basic approach is:

  1. Application.Run MyApp, which is-a ApplicationContext, and is IQuitable.
    • I think an app is needed to keep the program open whilste we wait for the IE request?
    • I guess maybe a (non-daemon) listener-loop thread might also work?
  2. MyApp's constructor creates a new InternetExporer object passing (IQuitable)this
  3. InternetExporer's constructor starts a new IE session, and navigates it to a URL.
  4. When a certain URL is requested InternetExporer calls-back the "parents" Quit method.

Background:

The real story is: I'm writing a plugin for MapInfo (A GIS Client). The plugin hijacks the "Start Extraction" HTTP request from IE to the server, modifies the URL slightly and sends a HTTPRequest in it's place. We parse the respose XML into MIF files [PDF 196K], which we then import and open in MapInfo. Then we quit the IE session, and close the "plugin" application.

SSCCE

using System;
using System.Windows.Forms;

// IE COM interface
// reference ~ C:\Windows\System32\SHDocVw.dll 
using SHDocVw; 

namespace QuitAppFromCaptiveIE
{
    static class Program {
        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MyApp());
        }
    }

    interface IQuitable {
        void Quit();
    }

    public class MyApp : ApplicationContext, IQuitable {
        private InternetExplorer ie = null; 

        public MyApp() {
            // create a new Internet Explorer COM component - starts IE application.
            this.ie = new InternetExplorer(this);
            this.ie.Open("www.microsoft.com");
        }

        #region IQuitable Members

        public void Quit() {
            if (ie != null) {
                ie.Dispose();
                ie = null;
            }
            Application.Exit();
        }

        #endregion
    }

    class InternetExplorer : IDisposable, IQuitable
    {
        // allows us to end the parent application when IE is closed.
        private IQuitable parent;
        private bool _parentIsQuited = false;
        private bool _ieIsQuited = false;

        private SHDocVw.InternetExplorer ie; // Old (VB4 era) IE COM component

        public InternetExplorer(IQuitable parent) {
            // lock-onto the parent app to quit it when IE is closed.
            this.parent = parent;
            // create a new Internet Explorer COM component - starts IE application.
            this.ie = new SHDocVw.InternetExplorerClass();
            // hook-up our navigate-event interceptor
            ie.BeforeNavigate2 += new DWebBrowserEvents2_BeforeNavigate2EventHandler(ie_BeforeNavigate2);
        }

        public void Open(string url) {
            object o = null;
            // make the captive IE session navigate to the given URL.
            ie.Navigate(url, ref o, ref o, ref o, ref o);
            // now make the ie window visible
            ie.Visible = true;
        }

        // this callback event handler is invoked prior to the captive IE 
        // session navigating (opening) a URL. Navigate-TWO handles both
        // external (normal) and internal (AJAX) requests. 
        // For example: You could create a history-log file of every page
        // visited by each captive session.
        // Being fired BEFORE the actual navigation allows you to hijack
        // (ie intercept) requests to certain URLs; in this case a request
        // to http://support.microsoft.com/ terminates the Browser session
        // and this program!
        void ie_BeforeNavigate2(object pDisp, ref object URL, ref object Flags, ref object TargetFrameName, ref object PostData, ref object Headers, ref bool Cancel) {
            if (URL.Equals("http://support.microsoft.com/")) {
                this.Quit();
            }
        }

        #region IDisposable Members

        public void Dispose() {
            quitIE();
        }

        #endregion

        private void quitIE() {
            // close my unmanaged COM object
            if (ie != null && !_ieIsQuited) {
                _ieIsQuited = true;
                ie.Quit();
                ie = null;
            }
        }

        #region IQuitable Members

        public void Quit() {
            // close my unmanaged COM object
            quitIE();
            // quit the parent app as well.
            if (parent != null && !_parentIsQuited) {
                _parentIsQuited = true;
                parent.Quit();
                parent = null;
            }
        }

        #endregion
    }

}

Solution

  • The long and the short of it appears to be (I'm still NOT an expert, by any stretch of the imagination) that SHDocVw.dll is still the preferred method for launching a "captive" Internet Explorer session (as apposed to embedding a browser in your application).

    The code I posted previously isn't the best solution, IMHO. In the final version:

    • IQuitable is history
    • Both MyApp and InternetExplorer classes implement IDisposable
    • Both Dispose methods just return if *_isDisposed* is true.

    The following coded includes some pseudocode, for brevity:

      private volatile bool _isDisposed = false;
    
      /**
      * _isDisposed stops the two "partners" in the conversation (us and 
      * Internet Explorer) from going into "infinite recursion", by calling 
      * each others Dispose methods within there Dispose methods.
      *
      * NOTE: I **think** that making _isDisposed volatile deals adequately
      * with the inherent race condition, but I'm NOT certain! Comments
      * welcome on this one.
      */
      public void Dispose() {
        if (!_isDisposed) {
          _isDisposed = true;
          try {
            try { release my unmanaged resources } catch { log }
            try {
              IE calls MyApp.Dispose() here // and FALLOUT on failure
              MyApp calls IE.Dispose(); here
            } catch {
              log
            }
          } finally {
            base.Dispose(); // ALLWAYS dispose base, no matter what!
          }
        }
      }
    

    To quit the application from the IE class you just call it's local Dispose method, which calls MyApps Dispose, which calls IE's Dispose again but isDisposed is true so it just returns. Then we call Application.ExitThread() and fall out of MyApp's Dispose... and then we fall out of IE's Dispose method, and the event-system stops; and the application terminates nicely. Finally!

    NOTE ON EDIT: I've just reused this approach with a Robo framework process, which is as a "captive process" of MyApp... the thing it's remote-controling. Convoluted, but it works... So I update my "self answer" with my latest learnings while I was here.