Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Using the new WebView2 AllowHostInputProcessing Keyboard Mapping Feature


:P
On this page:

Menu Accelerator Web View Banner

The WebView2 control is a complex beast and when you're building hybrid Desktop/Web applications that do serious interaction between a WebView2 control and a Desktop interface, you're going to find that there are a lot of areas where the keyboard and focus behavior is not as natural as you want it to be. Probably the most glaring example of this for me in Markdown Monster has been key forwarding and specifically in handling the main window menu accelerators.

Markdown Monster is a Markdown editor that uses a JavaScript based editor control, running in a WebView. The editor control interacts in a million ways with the WPF desktop interface, primarily from .NET and WPF into the JavaScript editor, but also with some operations in the JS code triggering the WPF UI with content updates from the editor. There are tons of UI operations like popping up context menus, hot keys that trigger operations in the WPF app and so on.

For the most part this all works fine with most editor operations handled internally in the editor without ever forwarding keys into the host WPF application. So navigation keys and plain text input is all managed in the JS code.

The WebView natively also supports some forwarding of keyboard operations from the WebView into the host if the WebView doesn't handle the keys. This may or may not be the behavior that you want, but even then it's tricky: some key events don't forward to WPF because the browser overrides them or in some cases forwarded keys are not triggered quite in the same way as 'native' keys trigger in WPF controls.

What this means is that there's a lot of custom key handling to override default key behavior and some trial and error to figure whether keys need to be handled in JavaScript or in WPF. 😏

Problem: Main Window Menu Activation (Alt Key Triggering)

For me, one big issue has been correctly handling the main window menu accelerator keys in the WPF Window host. Accelerators are those 'highlighted' keys on buttons or the main application menu, which based on Windows conventions should activate when you press the Alt key. In any 'normal' Windows application pressing Alt anywhere, forces focus into the main menu bar, which in turn shows these accelerators like this:

Pressing Alt Activates Menu Mnemonics
Figure 1 - Accelerator keys show as underlines in menus and buttons and are typically triggered by pressing alt in the related context. Here alt-t brings up the Tools menu. Note the underlines in the main menu and submenu.

The old Hack

Prior to the recently introduced AllowInputToHostMapping feature in the WebView, automatic alt key forwarding did not work. Instead, I had to resort to some ugly workarounds that:

  • intercept alt-key presses
  • use specific timing in code to determine whether it's a key combo or treated like an accelerator key
  • manipulate focus explicitly and forward keys into WPF

It worked, but very badly. Alt-key combos often failed to activate the menu, or the menu would activate but required another hit of alt key to actually activate properly.

It's some nasty code:

Old Alt Key Hack

... and even with all of that it doesn't even work very well.

WebView Focus Context is Internal

The reason for all of this WebView misbehavior is that by default the WebView control handles key events internally and maintains focus internally. Focus in the WebView control stays inside of the control unless you explicitly click outside of it into another control.

For a lot of scenarios that is actually the behavior you want - you don't want all events or even keystrokes bleed outside of the WebView. Focusing out adds overhead and can easily cause weird side effects where common browser operations trigger both in the browser and then also in the host.

Instead the WebView lets you handle events in JavaScript code, the WebView Shell (ie. things like ctrl-f Find, ctrl-r Refresh etc.) and then the .NET Wrapper exposes some of the events into the WPF host via the control wrapper. This works reasonably well for most things although it does mean if you need specific key sequences and shortcuts to work, you're likely going to have to build handlers both in JavaScript and in WPF.

For example, in Markdown Monster I have a ton of keyboard shortcuts that are user customizable, and those are configured through a KeyBindingManager which is configurable and has options to define where a particular key is handled:

  • In JavaScript Code via Command Name
  • In .NET Via an Command object
  • In some rare cases both

If this sounds extreme - it is due to the nature of this application which is highly customizable and because it's an editor it explicitly deals with many custom key combinations. Figuring out what to handle where is a bit of trial and error.

Generally speaking if you control the Html page, handling events in JavaScript is preferable as that's the fastest way to handle events. Calling into .NET is not exactly slow, but it does have more overhead than just running native JavaScript code and in my case calling editor APIs directly, rather than round tripping into .NET to something similar. So raw text operations are handled in the JS code typically while complex or UI heavy or input output related operations typically run in .NET handlers through the WPF interface.

For most hybrid applications this isn't a huge issue because you likely just have a few keyboard operations - if any - that need to be explicitly handled.

But one thing that almost every hybrid app has to deal with is the Main Menu handling, which is as described above is definitely not well behaved from within a WebView.

Enter CoreWebView2ControllerOptions.AllowHostInputProcessing

As of WebView .NET SDK 1.0.3351 and WebView runtime 1.0.1901.177 you can now have more of keyboard and mouse events bleed into WPF. This specifically fixes the default behavior of the Main Menu accelerator keys in Windows. When this option is enabled more keyboard events using 'special' keys bubble into the host interface where they previously did not.

But it doesn't come without caveats, especially if you've been handling keys the way that it has been working previously. Specifically AllowHostInputProcessing intercepts keys before they hit the WebView Control, which means that some Windows/WPF default behaviors now end up overriding keys in the WebView.

I'll have more on that a little bit later. Suffice it to say there are 'issues' you'll likely have to deal with, especially if you've handled keys based on the old behavior with WebView first key processing as was necessary before.

To enable the new AllowHostInputProcessing behavior, requires that you configure the CoreWebView2ControllerOptions.AllowHostInputProcessing which needs to be set up during the WebView environment setup. It's not very obvious as it's part of the WebView Environment setup which is optional - most applications probably don't do this setup by default - so if you want to use this feature a couple of extra steps are required.

I'm going to demonstrate this as part of a helper function (part of the Westwind.WebView library) as that shows the entire process and is probably a good way in general to instantiate an interactive .NET/JS based WebView control. This helper uses a shared WebView environment, so that multiple controls in the same application all share the same WebView environment without having to explicit configure each one.

First here's the relevant bit of code that's needed to AllowHostInputProcessing:

if (allowHostInputProcessing)
{
    var opts = Environment.CreateCoreWebView2ControllerOptions();
    opts.AllowHostInputProcessing = true;                    
    await webBrowser.EnsureCoreWebView2Async(environment, opts);
}

Essentially you need to create an instance of the Controller options and then assign it to an environment that is assigned when the Web view is initially created.

What this means is that you can't just simply instantiate a WebView and have this work - you need a bit of machinery to create an environment explicitly, which BTW is a good idea and highly recommended anyway.

I use a helper method for this with every WebView I use which does the following:

  • Sets up a new WebView Environment in a specified (shared) path
    (important because deploy folders often don't allow writing which breaks initialization)
  • Reuses a previously created environment
  • If allowHostInputProcessing is passed, sets it on the environment
  • Calls and awaits the WebView Initialization (ensure that CoreWebView2 is available)

Here's this that wraps all this busy work into an easy to use function (part of Westwind.Webview source on GitHub)

public async Task InitializeWebViewEnvironment(WebView2 webBrowser,
            CoreWebView2Environment environment = null, 
            string webViewEnvironmentPath = null, 
            bool allowHostInputProcessing = false)
{
    try
    {
        if (environment == null)
            environment = Environment;

        if (environment == null)
        {
            // lock
            await _EnvironmentLoadLock.WaitAsync();

            if (environment == null)
            {
                var envPath = webViewEnvironmentPath ?? Current.EnvironmentFolderName;
                if (string.IsNullOrEmpty(envPath))
                    Current.EnvironmentFolderName = Path.Combine(Path.GetTempPath(),
                        Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly().Location) + "_WebView");

                // must create a data folder if running out of a secured folder that can't write like Program Files
                environment = await CoreWebView2Environment.CreateAsync(userDataFolder:  EnvironmentFolderName,
                    options: EnvironmentOptions);

                Environment = environment;
            }

            _EnvironmentLoadLock.Release();
        }

        // *** THIS HERE ***
        if (allowHostInputProcessing)
        {
            var opts = Environment.CreateCoreWebView2ControllerOptions();
            opts.AllowHostInputProcessing = true;                    
            await webBrowser.EnsureCoreWebView2Async(environment, opts);
        }
        else
            await webBrowser.EnsureCoreWebView2Async(environment);
    }
    catch (Exception ex)
    {
        throw new WebViewInitializationException($"WebView EnsureCoreWebView2AsyncCall failed.\nFolder: {EnvironmentFolderName}", ex);
    }
}

That's a bit of code, but it ensures that you safely create a WebView instance with an attached envirnment and wait for it to initialize.

Important:
await webBrowser.EnsureCoreWebView2Async(environment, opts) waits until the WebView has loaded. This method also will not complete if the WebView is not visible. By extension the helper method which calls this method also does not complete until those criteria are met.

Using this code to initialize the environment, menu accelerators now work as expected. Pressing alt activates the main menu even from within the WebView control!

Yay!

Caveats: Watch Tab and Potentially other Navigation Keys

Once you set AllowHostInputProcessing=true keyboard behavior now passes through many things that you might not expect. One of the things that is really noticeable is the Tab and Shift-Tab key which now moves focus to the next control. Again, this may be desirable in some scenarios, but in my editor scenario it most certainly is not as Tab is definitely a character that needs to apply to my text editing!

Without any intervention the behavior I now see is that the Tab key does not fire inside of the editor, but instead fires in the control. The behavior is:

  • WPF event fires first
  • If not cancelled it then fires in the WebView/Editor

It turns out that this leaves you with some very BAD choices:

  • Handle the key in WPF and cancel - no good because the key is not forwarded into wpf
  • Handle the key in WPF and don't cancel - no good because Tab fires and focus changes

The solution in the case of my editor is to handle the tab key in WPF and cancel the event, and then explicitly call back into the editor to trigger the Indent/Unindent command behavior. Yeah that's ugly.

Here's what that looks like:

  • Create a custom key Binding for Tab and Shift Tab
  • Create a custom

First add the key bindings (app custom bindings that wrap WPF KeyBindings)

// *** Special Bindings

// Tab Handling for the editor: Bind only the Tab control
// Forced to override Tab Handling here due to
// WebView2 ApplyHostToInputProcessing setting which handles
// Tab before it gets to the JavaScript handler.
KeyBindings.AddRange(

    new AppKeyBinding
    { Id = "EditorCommand_TabKey",
        Key = "Tab",
        Command = model.Commands.KeyBoardOperationCommand,
        CommandParameter = "TabKey",
        HasJavaScriptHandler = false,
        BindingControl = mmApp.Window.TabControl
    },
    new AppKeyBinding
    {
        Id = "EditorCommand_ShiftTabKey",
        Key = "Shift+Tab",
        Command = model.Commands.KeyBoardOperationCommand,
        CommandParameter = "ShiftTabKey",
        HasJavaScriptHandler = false,
        BindingControl = mmApp.Window.TabControl
    }
);

Then handle that with a custom handler:

void Command_KeyBoardOperation()
{
    KeyBoardOperationCommand = new CommandBase(async (parameter, command) =>
    {
        var action = parameter as string;
        if (string.IsNullOrEmpty(action)) return;
       
        if (Model.ActiveEditor?.EditorHandler?.DotnetInterop != null)
            await Model.ActiveEditor.EditorHandler.DotnetInterop.KeyboardCommand(action);
    }, (p, c) => true);
}

// inside of `KeyboardCommand` fire JavaScript handlers that forward to the editor
else if (key == "TabKey")
{
    // override so we don't tab out (AllowHostInput option)
    await JsInterop.Invoke("fireTabKey");  // explicit so Ghost Text works!
}
else if (key == "ShiftTabKey")
{
    // do nothing but keep from tabbing out
    await JsInterop.ExecCommand("outdent");
}

Finally the js code to simulate a key event in ACE editor:

fireTabKey: function (e) {
    const event = new KeyboardEvent("keydown", {
        key: 'Tab',
        keyCode: 9,
        which: 9,
        code: 'Tab',
        bubbles: true,
        cancelable: true,
        shiftKey: false,
        ctrlKey: false,
        altKey: false
    });

    // Dispatch to ACE's hidden textarea
    te.editor.textInput.getElement().dispatchEvent(event);
},

Shift tab simple uses Ace Editor's built-in command via ExecCommand(outdent). The reason tab is handled with the raw event is that there's custom tab key handling in the editor that only gets triggered by the raw event, vs. the editor command which performs the specific indent action.

In simpler scenarios that don't have a high level interface like ACE editor with custom key commands, explicit JavaScript keyboard event injection is the way that you can force keys back into the WebView for handling after WPF has had its shot at it. While it seems like a circle jerk, it's effective and it works.

Phew - a lot of work and as you might guess it took a bit to figure this out.

What about other 'special keys' affected by AllowInputHostProcessing? Well, there might be, but I haven't run into issues that I've noticed (yet?). Markdown Monster exercises keyboard handling pretty heavily and so far I haven't run into any other issues so.

Summary

Keyboard handling for special keys in a WebBrowser control always are and have been a bear to work with. It was a pain in the old IE based WebBrowser control and it's a bear now with the WebView2. Different issues with different behaviors, but similar scenarios. How this is handled is one of those damned if you do, and damned if you don't scenarios, because no matter which route you take there are some things that are not going to work the way you likely want them to work. The alt and tab key behaviors I've described here are perfect examples of that.

But where there's a will there's a way - and there are ways around this. Overall while AllowInputHostProcessing some new problems, I think if you start with that setting on, it provides a better base state for keyboard handling. Other than the tab key I haven't noticed any other abhorrent misbehavior. Hopefully with the information from this post - you can navigate your way more quickly through this than I did. Aloha.

this post created and published with the Markdown Monster Editor
Posted in WPF  WebView  


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2025