Search code examples
c#.netwpfcomwebbrowser-control

WebBrowser control's shortcut keys are not working


I have a WPF project that loads a window that contains a WebBrowser control. The class that opens the window is made available for COM interoperability.

When running the project as a windows application the window is opened and the WebBrowser control works fine, but when compiled as a class library and COM is used from an external application to open the window, none of the WebBrowser's shortcut keys work. (e.g. CTRL+A, DELETE, CTRL+X, TAB etc.)

This SO Question seems to explain the cause of the issue, but the suggestions there don't work for me as PreProcessMessage or ProcessCmdKey never get called. (unless run as a windows application)

I also read through the links here and here that discuss calling the TranslateAccelerator method. but i am unable to attempt this as none of the KeyDown events I subscribe to are being fired. I've tried WebBrowser.KeyDown, WebBrowser.PreviewKeyDown, and various onkeydown events associated with WebBrowser.Document and WebBrowser.Document.Body. none of these were triggered for me. (unless run as a windows application)

COM visible class

[ProgId("My.Project")]
[ComVisible(true)]
public class MyComVisibleClass : IMyComVisibleInterface
{
    private BrowserWindow myWpfWindow;
    public void OpenWpfWindow()
    {
        ...
        myWpfWindow = new myWpfWindow();
        ...
        myWpfWindow.Show();
    }
}

XAML

<WebBrowser x:Name="EmbeddedBrowser" Focusable="True"    />
<!--I tried using forms host too-->
<!--<WindowsFormsHost Name="wfHost" Focusable="True" >
    <common:WebBrowser x:Name="EmbeddedBrowser" WebBrowserShortcutsEnabled="True"     ObjectForScripting="True"  />
</WindowsFormsHost>-->

WPF browser window

public partial class BrowserWindow : Window
    {
        public BrowserWindow(Uri uri)
        {
            InitializeComponent();
            ...
            EmbeddedBrowser.Focus();
            EmbeddedBrowser.Navigate(uri); 
            ...  
        }
    }
}

What can I do to enable the shortcut keys when opened through COM interop?


Solution

  • One real bug in your solution is here:

    hHook = SetWindowsHookEx(WH_GETMESSAGE, new HookHandlerDelegate(HookCallBack), (IntPtr)0, GetCurrentThreadId());
    

    The newly allocated delegate new HookHandlerDelegate(HookCallBack) gets garbage-collected at some point, which later leads AccessViolationException. You should keep a strong reference to this delegate until you have called UnhookWindowsHookEx:

    this._hookCallBack = new HookHandlerDelegate(HookCallBack);
    this.hHook = SetWindowsHookEx(WH_GETMESSAGE, _hookCallBack, (IntPtr)0, GetCurrentThreadId());
    

    That said, I still don't think this is the right approach to tackle the problem. From the comments to question:

    So, does myWpfWindow behaves like a modeless, independent top-level window in that legacy app? Or does it correlate somehow with the rest of the legacy app's GUI?


    independent top level window.

    WPF and Win32 Interoperation (particulary, Sharing Message Loops Between Win32 and WPF) assumes you have control over the Win32 legacy app's code.

    Apparently, this is not the case here, therefore I suggest you open this WPF window on a separate UI thread with WPF dispatcher (and its own message loop). This would solve the WebBrowser shortcut issues and, potentially, some other issues as well.

    You can use AttachThreadInput to attach the user input queue of the original STA thread (where your COM object lives) to the that of the new WPF thread. There are other aspects, like marshaling COM events and methods calls to the correct thread. The below code illustrates this concept. It's a complete WinForms test app which uses a COM object which, in turn, creates a WPF window with WebBrowser on a dedicated thread.

    using System;
    using System.Runtime.InteropServices;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    using System.Windows.Threading;
    
    namespace LegacyWinApp
    {
        // by noseratio - https://stackoverflow.com/a/28573841/1768303
    
        /// <summary>
        /// Form1 - testing MyComVisibleClass from a client app
        /// </summary>
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
                this.Load += Form1_Load;
            }
    
            private void Form1_Load(object sender, EventArgs e)
            {
                var comObject = new MyComVisibleClass();
    
                var status = new Label { Left = 10, Top = 10, Width = 50, Height = 25, BorderStyle = BorderStyle.Fixed3D };
                this.Controls.Add(status);
    
                comObject.Loaded += () =>
                    status.Text = "Loaded!";
    
                comObject.Closed += () =>
                    status.Text = "Closed!";
    
                var buttonOpen = new Button { Left = 10, Top = 60, Width = 50, Height = 50, Text = "Open" };
                this.Controls.Add(buttonOpen);
                buttonOpen.Click += (_, __) =>
                {
                    comObject.Open();
                    status.Text = "Opened!";
                    comObject.Load("http://example.com");
                };
    
                var buttonClose = new Button { Left = 10, Top = 110, Width = 50, Height = 50, Text = "Close" };
                this.Controls.Add(buttonClose);
                buttonClose.Click += (_, __) =>
                    comObject.Close();
            }
        }
    
        /// <summary>
        /// MyComVisibleClass
        /// </summary>
    
        [ComVisible(true), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
        public interface IComObject
        {
            void Open();
            void Load(string url);
            void Close();
        }
    
        [ComVisible(true), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
        public interface IComObjectEvents
        {
            void Loaded();
            void Closed();
        }
    
        /// <summary>
        /// MyComVisibleClass
        /// </summary>
        [ComVisible(true)]
        [ClassInterface(ClassInterfaceType.None)]
        [ComDefaultInterface(typeof(IComObject))]
        [ComSourceInterfaces(typeof(IComObjectEvents))]
        public class MyComVisibleClass : IComObject
        {
            internal class EventHelper
            {
                MyComVisibleClass _parent;
                System.Windows.Threading.Dispatcher _clientThreadDispatcher;
    
                internal EventHelper(MyComVisibleClass parent)
                {
                    _parent = parent;
                    _clientThreadDispatcher = System.Windows.Threading.Dispatcher.CurrentDispatcher;
                }
    
                public void FireLoaded()
                {
                    _clientThreadDispatcher.InvokeAsync(() =>
                        _parent.FireLoaded());
                }
    
                public void FireClosed()
                {
                    _clientThreadDispatcher.InvokeAsync(() =>
                        _parent.FireClosed());
                }
            }
    
            WpfApartment _wpfApartment;
            BrowserWindow _browserWindow;
            readonly EventHelper _eventHelper;
    
            public MyComVisibleClass()
            {
                _eventHelper = new EventHelper(this);
            }
    
            // IComObject methods
    
            public void Open()
            {
                if (_wpfApartment != null)
                    throw new InvalidOperationException();
    
                // start a new thread with WPF Dispatcher
                _wpfApartment = new WpfApartment();
    
                // attach the input queue of the current thread to that of c
                var thisThreadId = NativeMethods.GetCurrentThreadId();
                _wpfApartment.Invoke(() =>
                    NativeMethods.AttachThreadInput(thisThreadId, NativeMethods.GetCurrentThreadId(), true));
    
                // create an instance of BrowserWindow on the WpfApartment's thread
                _browserWindow = _wpfApartment.Invoke(() => new BrowserWindow(_eventHelper) { 
                    Left = 200, Top = 200, Width = 640, Height = 480 });
                _wpfApartment.Invoke(() => _browserWindow.Initialize());
            }
    
            public void Load(string url)
            {
                if (_wpfApartment == null)
                    throw new InvalidOperationException();
    
                _wpfApartment.Run(async () =>
                {
                    try
                    {
                        await _browserWindow.LoadAsync(url);
                        _eventHelper.FireLoaded();
                    }
                    catch (Exception ex)
                    {
                        System.Windows.MessageBox.Show(ex.Message);
                        throw;
                    }
                });
            }
    
            public void Close()
            {
                if (_wpfApartment == null)
                    return;
    
                if (_browserWindow != null)
                    _wpfApartment.Invoke(() => 
                        _browserWindow.Close());
    
                CloseWpfApartment();
            }
    
            void CloseWpfApartment()
            {
                if (_wpfApartment != null)
                {
                    _wpfApartment.Dispose();
                    _wpfApartment = null;
                }
            }
    
            // IComObjectEvents events
    
            public event Action Loaded = EmptyEventHandler;
    
            public event Action Closed = EmptyEventHandler;
    
            // fire events, to be called by EventHelper
    
            static void EmptyEventHandler() { }
    
            internal void FireLoaded()
            {
                this.Loaded();
            }
    
            internal void FireClosed()
            {
                _browserWindow = null;
                CloseWpfApartment();            
                this.Closed();
            }
        }
    
        /// <summary>
        /// BrowserWindow
        /// </summary>
        class BrowserWindow: System.Windows.Window
        {
            System.Windows.Controls.WebBrowser _browser;
            MyComVisibleClass.EventHelper _events;
    
            public BrowserWindow(MyComVisibleClass.EventHelper events)
            {
                _events = events;
                this.Visibility = System.Windows.Visibility.Hidden;
                this.ShowActivated = true;
                this.ShowInTaskbar = false;
            }
    
            bool IsReady()
            {
                return (this.Visibility != System.Windows.Visibility.Hidden && _browser != null);
            }
    
            public void Initialize()
            {
                if (IsReady())
                    throw new InvalidOperationException();
    
                this.Show();
                _browser = new System.Windows.Controls.WebBrowser();
                this.Content = _browser;
            }
    
            public async Task LoadAsync(string url)
            {
                if (!IsReady())
                    throw new InvalidOperationException();
    
                // navigate and handle LoadCompleted
                var navigationTcs = new TaskCompletionSource<bool>();
    
                System.Windows.Navigation.LoadCompletedEventHandler handler = (s, e) =>
                    navigationTcs.TrySetResult(true);
    
                _browser.LoadCompleted += handler;
                try
                {
                    _browser.Navigate(url);
                    await navigationTcs.Task;
                }
                finally
                {
                    _browser.LoadCompleted -= handler;
                }
    
                // make the content editable to check if WebBrowser shortcuts work well
                dynamic doc = _browser.Document;
                doc.body.firstChild.contentEditable = true;
                _events.FireLoaded();
            }
    
            protected override void OnClosed(EventArgs e)
            {
                base.OnClosed(e);
                _browser.Dispose();
                _browser = null;
                _events.FireClosed();
            }
        }
    
        /// <summary>
        /// WpfApartment
        /// </summary>
        internal class WpfApartment : IDisposable
        {
            Thread _thread; // the STA thread
    
            TaskScheduler _taskScheduler; // the STA thread's task scheduler
    
            public TaskScheduler TaskScheduler { get { return _taskScheduler; } }
    
            // start the STA thread with WPF Dispatcher
            public WpfApartment()
            {
                var tcs = new TaskCompletionSource<TaskScheduler>();
    
                // start an STA thread and gets a task scheduler
                _thread = new Thread(_ =>
                {
                    // post the startup callback,
                    // it will be invoked when the message loop stars pumping
                    Dispatcher.CurrentDispatcher.InvokeAsync(
                        () => tcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext()), 
                        DispatcherPriority.ApplicationIdle);
    
                    // run the WPF Dispatcher message loop
                    Dispatcher.Run();
                });
    
                _thread.SetApartmentState(ApartmentState.STA);
                _thread.IsBackground = true;
                _thread.Start();
                _taskScheduler = tcs.Task.Result;
            }
    
            // shutdown the STA thread
            public void Dispose()
            {
                if (_taskScheduler != null)
                {
                    var taskScheduler = _taskScheduler;
                    _taskScheduler = null;
    
                    if (_thread != null && _thread.IsAlive)
                    {
                        // execute Dispatcher.ExitAllFrames() on the STA thread
                        Task.Factory.StartNew(
                            () => Dispatcher.ExitAllFrames(),
                            CancellationToken.None,
                            TaskCreationOptions.None,
                            taskScheduler).Wait();
    
                        _thread.Join();
                    }
                    _thread = null;
                }
            }
    
            // Task.Factory.StartNew wrappers
            public void Invoke(Action action)
            {
                Task.Factory.StartNew(action,
                    CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Wait();
            }
    
            public TResult Invoke<TResult>(Func<TResult> func)
            {
                return Task.Factory.StartNew(func,
                    CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Result;
            }
    
            public Task Run(Action action, CancellationToken token = default(CancellationToken))
            {
                return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
            }
    
            public Task<TResult> Run<TResult>(Func<TResult> func, CancellationToken token = default(CancellationToken))
            {
                return Task.Factory.StartNew(func, token, TaskCreationOptions.None, _taskScheduler);
            }
    
            public Task Run(Func<Task> func, CancellationToken token = default(CancellationToken))
            {
                return Task.Factory.StartNew(func, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
            }
    
            public Task<TResult> Run<TResult>(Func<Task<TResult>> func, CancellationToken token = default(CancellationToken))
            {
                return Task.Factory.StartNew(func, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
            }
        }
    
        /// <summary>
        /// NativeMethods
        /// </summary>
        internal class NativeMethods
        {
            [DllImport("kernel32.dll", PreserveSig = true)]
            public static extern uint GetCurrentThreadId();
    
            [DllImport("user32.dll", PreserveSig = true)]
            public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
        }
    }