9

Here is the challenge. I'm deriving from the Framework's WebBrowserSite class. An instance of my derived class, ImprovedWebBrowserSite, is returned via WebBrowser.CreateWebBrowserSiteBase, which I override in my derived version of the WebBrowser class - specifically to provide a custom site object. The Framework's WebBrowser implementation further passes it to down to the underlying unmanaged WebBrowser ActiveX control.

So far, I've managed to override IDocHostUIHandler in my ImprovedWebBrowserSite implementation (like this). I'm now looking for more core COM interfaces, like IOleClientSite, which I want to pass-through to WebBrowserSite. All of them are exposed to COM with ComImport, but declared as private or internal by the Framework's implementation of WebBrowserSite/UnsafeNativeMethods. Thus, I cannot explicitly re-implement them in the derived class. I have to define my own versions, like I did with IDocHostUIHandler.

So, the question is, how do I call a method of a private or internal COM interface defined in WebBrowserSite, from my derived class? For example, I want to call IOleClientSite.GetContainer. I can use reflection (like this), but that would be the last resort, second to re-implementing WebBrowser from scratch.

My thinking is, because the Framework's private UnsafeNativeMethods.IOleClientSite and my own ImprovedWebBrowserSite.IOleClientSite are both COM interfaces, declared with the ComImport attribute, the same GUID and identical method signatures. There's COM Type Equivalence in .NET 4.0+, so there has to be a way to do it without reflection.

[UPDATE] Now that I've got a solution, I believe it opens some new and interesting possibilities in customizing the WinForms version of WebBrowser control.

This version of the question was created after my initial attempt to formulate the problem in a more abstract form was dubbed misleading by a commentator. The comment has been removed later, but I decided to keep both versions.

Why did I not want to use reflection to solve this problem? For a few reasons:

  • Dependency on the actual symbolic names of the internal or private methods, as given by the implementers of WebBrowserSite, unlike with a COM interface, which is about the binary v-table contract.

  • Bulky reflection code. E.g., consider calling the base's private TranslateAccelerator via Type.InvokeMember, and I have ~20 methods like that to call.

  • Although less important, efficiency: a late-bound call via reflection is always less efficient than a direct call to a COM interface method via v-table.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • why do you want to override them and not simply copy-paste their declarations? – VladL Oct 31 '13 at 22:42
  • @VladL, have a look at [.NET 4.x sources](http://referencesource.microsoft.com/netframework.aspx). `WebBrowserSite` does a lot of stuff as to ActiveX hosting, I cannot just bypass calling its implementation of COM interfaces. I certainly can copy-paste the interface declarations, but I have to call the original implementation too. – noseratio Oct 31 '13 at 22:47
  • 1
    I don't think you can have type equivalence between non public types. If you try to add a TypeIdentifier attribute to a non public type, you will get this type of error: `"Could not load type 'IOleClientSite' from assembly 'blabla...'. The type is marked as eligible for type equivalence, but it is not marked as public."` – Simon Mourier Nov 01 '13 at 08:56
  • @SimonMourier, it appears to be possible via an aggregated proxy object, created with `Marshal.CreateAggregatedObject`. – noseratio Nov 02 '13 at 05:37
  • What's wrong with reflection? Create a delegate to the private method and the calls are as fast as any delegate calls. – Ark-kun Nov 10 '13 at 00:20
  • @Ark-kun, I've updated the question with an explanation on why I didn't want to use reflection. – noseratio Nov 10 '13 at 02:12
  • Are you sure about the 3)? Function pointer is a function pointer. Why would calling it be less efficient than calling an interface method which is also called indirectly? In my tests, calling delegate with parameters has the same performance as calling interface method with parameters. – Ark-kun Nov 10 '13 at 02:15
  • @Ark-kun, I'm 99.99% sure. If you have spare time to challenge this, feel free to take the working model from [here](http://stackoverflow.com/a/19737565/1768303) and do some timing on a tight loop, reflection vs direct COM calls. I'd be interested to know the actual results, if you could share your test case on pastebin.com. Anyhow, as I said, #3 is the least important consideration on my list. – noseratio Nov 10 '13 at 02:26
  • @Noseratio Sadly, I don't really know (right now) how to create COM objects and call their methods with or without reflection. My logic was that for the low-overhead calls (via interface), the reflected delegates (`Delegate.CreateDelegate(...)`) have nearly the same performance as the virtual interface calls. So, I thought, that for heavier-overhead COM calls, the difference would be even more negligeable. I'll write back if I ever work with COM and test this. – Ark-kun Nov 11 '13 at 19:31

2 Answers2

9

Finally, I believe I've solved the problem using Marshal.CreateAggregatedObject, with some help from @EricBrown.

Here's the code that makes possible customizing WebBrowserSite OLE interfaces, using IOleClientSite as an example, calling the private COM-visible implementation of WebBrowserSite. It can be extended to other interfaces, e.g. IDocHostUIHandler.

using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace CustomWebBrowser
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        private void MainForm_Load(object sender, EventArgs e)
        {
            var wb = new ImprovedWebBrowser();
            wb.Dock = DockStyle.Fill;
            this.Controls.Add(wb);
            wb.Visible = true;
            wb.DocumentText = "<b>Hello from ImprovedWebBrowser!</b>";
        }
    }

    // ImprovedWebBrowser with custom pass-through IOleClientSite 
    public class ImprovedWebBrowser: WebBrowser
    {
        // provide custom WebBrowserSite,
        // where we override IOleClientSite and call the base implementation
        protected override WebBrowserSiteBase CreateWebBrowserSiteBase()
        {
            return new ImprovedWebBrowserSite(this);
        }

        // IOleClientSite
        [ComImport(), Guid("00000118-0000-0000-C000-000000000046")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IOleClientSite
        {
            void SaveObject();

            [return: MarshalAs(UnmanagedType.Interface)]
            object GetMoniker(
                [In, MarshalAs(UnmanagedType.U4)] int dwAssign,
                [In, MarshalAs(UnmanagedType.U4)] int dwWhichMoniker);

            [PreserveSig]
            int GetContainer([Out] out IntPtr ppContainer);

            void ShowObject();

            void OnShowWindow([In, MarshalAs(UnmanagedType.I4)] int fShow);

            void RequestNewObjectLayout();
        }

        // ImprovedWebBrowserSite
        protected class ImprovedWebBrowserSite :
            WebBrowserSite,
            IOleClientSite,
            ICustomQueryInterface,
            IDisposable
        {
            IOleClientSite _baseIOleClientSite;
            IntPtr _unkOuter;
            IntPtr _unkInnerAggregated;
            Inner _inner;

            #region Inner
            // Inner as aggregated object
            class Inner :
                ICustomQueryInterface,
                IDisposable
            {
                object _outer;
                Type[] _interfaces;

                public Inner(object outer)
                {
                    _outer = outer;
                    // the base's private COM interfaces are here
                    _interfaces = _outer.GetType().BaseType.GetInterfaces(); 
                }

                public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv)
                {
                    if (_outer != null)
                    {
                        var ifaceGuid = iid;
                        var iface = _interfaces.FirstOrDefault((t) => t.GUID == ifaceGuid);
                        if (iface != null)
                        {
                            var unk = Marshal.GetComInterfaceForObject(_outer, iface, CustomQueryInterfaceMode.Ignore);
                            if (unk != IntPtr.Zero)
                            {
                                ppv = unk;
                                return CustomQueryInterfaceResult.Handled;
                            }
                        }
                    }
                    ppv = IntPtr.Zero;
                    return CustomQueryInterfaceResult.Failed;
                }

                ~Inner()
                {
                    // need to work out the reference counting for GC to work correctly
                    Debug.Print("Inner object finalized.");
                }

                public void Dispose()
                {
                    _outer = null;
                    _interfaces = null;
                }
            }
            #endregion

            // constructor
            public ImprovedWebBrowserSite(WebBrowser host):
                base(host)
            {
                // get the CCW object for this
                _unkOuter = Marshal.GetIUnknownForObject(this);
                Marshal.AddRef(_unkOuter);
                try
                {
                    // aggregate the CCW object with the helper Inner object
                    _inner = new Inner(this);
                    _unkInnerAggregated = Marshal.CreateAggregatedObject(_unkOuter, _inner);

                    // turn private WebBrowserSiteBase.IOleClientSite into our own IOleClientSite
                    _baseIOleClientSite = (IOleClientSite)Marshal.GetTypedObjectForIUnknown(_unkInnerAggregated, typeof(IOleClientSite));
                }
                finally
                {
                    Marshal.Release(_unkOuter);
                }
            }

            ~ImprovedWebBrowserSite()
            {
                // need to work out the reference counting for GC to work correctly
                Debug.Print("ImprovedClass finalized.");
            }

            public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv)
            {
                if (iid == typeof(IOleClientSite).GUID)
                {
                    // CustomQueryInterfaceMode.Ignore is to avoid infinite loop during QI.
                    ppv = Marshal.GetComInterfaceForObject(this, typeof(IOleClientSite), CustomQueryInterfaceMode.Ignore);
                    return CustomQueryInterfaceResult.Handled;
                }
                ppv = IntPtr.Zero;
                return CustomQueryInterfaceResult.NotHandled;
            }

            void IDisposable.Dispose()
            {
                base.Dispose();

                // we may have recicular references to itself
                _baseIOleClientSite = null;

                if (_inner != null)
                {
                    _inner.Dispose();
                    _inner = null;
                }

                if (_unkInnerAggregated != IntPtr.Zero)
                {
                    Marshal.Release(_unkInnerAggregated);
                    _unkInnerAggregated = IntPtr.Zero;
                }

                if (_unkOuter != IntPtr.Zero)
                {
                    Marshal.Release(_unkOuter);
                    _unkOuter = IntPtr.Zero;
                }
            }

            #region IOleClientSite
            // IOleClientSite
            public void SaveObject()
            {
                Debug.Print("IOleClientSite.SaveObject");
                _baseIOleClientSite.SaveObject();
            }

            public object GetMoniker(int dwAssign, int dwWhichMoniker)
            {
                Debug.Print("IOleClientSite.GetMoniker");
                return _baseIOleClientSite.GetMoniker(dwAssign, dwWhichMoniker);
            }

            public int GetContainer(out IntPtr ppContainer)
            {
                Debug.Print("IOleClientSite.GetContainer");
                return _baseIOleClientSite.GetContainer(out ppContainer);
            }

            public void ShowObject()
            {
                Debug.Print("IOleClientSite.ShowObject");
                _baseIOleClientSite.ShowObject();
            }

            public void OnShowWindow(int fShow)
            {
                Debug.Print("IOleClientSite.OnShowWindow");
                _baseIOleClientSite.OnShowWindow(fShow);
            }

            public void RequestNewObjectLayout()
            {
                Debug.Print("IOleClientSite.RequestNewObjectLayout");
                _baseIOleClientSite.RequestNewObjectLayout();
            }
            #endregion
        }
    }
}
Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • It works well in .Net 4.5 but a FatalExecutionEngineError in .Net4.0 It says " The runtime has encountered a fatal error. The address of the error was at 0x6ef47786, on thread 0x2e18. The error code is 0xc0000005. This error may be a bug in the CLR or in the unsafe or non-verifiable portions of user code." This error occurs at the following line: _baseIDocHostUiHandler = (NativeMethods.IDocHostUIHandler)Marshal.GetTypedObjectForIUnknown(_unkInnerAggregated, typeof(NativeMethods.IDocHostUIHandler)); Any ideas? – Vallabha Vamaravelli Mar 12 '15 at 07:55
  • @VallabhaV, no high hopes for .NET 4.0, I'm short on spare time for this, sorry. – noseratio Mar 12 '15 at 12:00
  • Implementing IDisposable does not magically make consumers use it. _unkOuter and _unkInnerAggregated are only used in the constructor so they shouldn't be kept alive for the whole lifetime of the instance. Instead they should both be released in the constructor after use, the CreateAggregatedObject will already have AddRef'ed them - and the AddRef call should be removed from the constructor. And then get rid of the IDisposable implementation, it's not going to be used anyways. – poizan42 Oct 27 '19 at 14:49
  • @poizan42, good to know this piece of code got your interested, and thanks for your ideas on how to improve it. – noseratio Oct 28 '19 at 05:58
4

Just a thought but maybe you can use some of the source code from here. You would be reimplementing but it might give you what you want.

Danexxtone
  • 773
  • 4
  • 11
  • Thanks, I've actually studied the Framework sources already - very helpful in understanding how `WebBrowser` works. Although the best I could do with it in this case is to borrow the definitions of `UnsafeNativeMethods`. – noseratio Nov 01 '13 at 12:55