In C# WinUI, a crash about "System. ExecutionInexception" is caused for unknown reasons

72 Views Asked by At

I try to hook the keyborad. Display a window when the number lock key and the caps lock key are pressed, and display the status of the current lock key in the window. When I try to click the number lock key or caps lock key quickly, it will crash in App.g.i.cs.Visual Studio display An unhandled exception of type "system. executionengenexception" occurred in Microsoft.WinUI.dll.

This is code when crash in App.g.i.cs:

static void Main(string[] args)
{
    XamlCheckProcessRequirements();
    
    global::WinRT.ComWrappersSupport.InitializeComWrappers();
    global::Microsoft.UI.Xaml.Application.Start((p) => {
        var context = new global::Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext(global::Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
        global::System.Threading.SynchronizationContext.SetSynchronizationContext(context);
        new App();
    });
}

I think it seems that this crash happened because of the rapid change of TextBlock's Text.

This is FlyoutPage.xaml:

<Page
    x:Class="MoreFlyout.Views.FlyoutPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="using:MoreFlyout.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Page.Resources>
        <AcrylicBrush x:Key="CustomAcrylicBrush" AlwaysUseFallback="False" />
    </Page.Resources>

    <Page.ContextFlyout>
        <Flyout x:Name="FlyoutWindowContextFlyout" ShouldConstrainToRootBounds="False">
            <Flyout.SystemBackdrop>
                <local:AcrylicSystemBackdrop />
            </Flyout.SystemBackdrop>
            <Flyout.FlyoutPresenterStyle>
                <Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
                    <Setter Property="Background" Value="Transparent" />
                </Style>
            </Flyout.FlyoutPresenterStyle>

            <Grid
                x:Name="ContentArea"
                MinWidth="164"
                Padding="-8">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="48" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>

                <HyperlinkButton
                    x:Name="IconButton"
                    Grid.Column="0"
                    Width="32"
                    Height="32"
                    IsEnabled="False">
                    <FontIcon
                        x:Name="StatusFontIcon"
                        Margin="-4"
                        FontSize="12"
                        Foreground="White"
                        Glyph="&#xE72E;" />
                </HyperlinkButton>

                <TextBlock
                    x:Name="StatusTextBlock"
                    Grid.Column="1"
                    VerticalAlignment="Center"
                    TextAlignment="Center" />
            </Grid>
        </Flyout>
    </Page.ContextFlyout>
</Page>

This is FlyoutPage.xaml.cs:

using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using MoreFlyout.Helpers;
using MoreFlyout.ViewModels;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;

namespace MoreFlyout.Views;

public sealed partial class FlyoutPage : Page
{
    private readonly Microsoft.UI.Dispatching.DispatcherQueue dispatcherQueue;
    private static System.Timers.Timer? aTimer;
    private UnhookWindowsHookExSafeHandle HookID;

    private const int WM_KEYDOWN = 0x0100;
    private const int VK_NUMLOCK = 0x90;
    private const int VK_CAPSLOCK = 0x14;

    private static bool numKeyState = false;
    private static bool capsKeyState = false;

    public FlyoutViewModel ViewModel
    {
        get;
    }

    public FlyoutPage()
    {
        ViewModel = App.GetService<FlyoutViewModel>();
        InitializeComponent();

        dispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();

        numKeyState = (PInvoke.GetKeyState(VK_NUMLOCK) & 1) == 1;
        capsKeyState = (PInvoke.GetKeyState(VK_CAPSLOCK) & 1) == 1;

        HookID = PInvoke.SetWindowsHookEx(WINDOWS_HOOK_ID.WH_KEYBOARD_LL, HookCallback, null, 0);

        App.FlyoutWindow.Closed += FlyoutWindowClosed;
    }

    private void FlyoutWindowClosed(object sender, WindowEventArgs args) => HookID.Close();

    private LRESULT HookCallback(int nCode, WPARAM wParam, LPARAM lParam)
    {
        if (nCode >= 0 && wParam == WM_KEYDOWN)
        {
            var vkCode = Marshal.ReadInt32(lParam);
            if (vkCode == VK_NUMLOCK)
            {
                StatusTextBlock.Text = numKeyState ? "StatusWords_NumUnlock".GetLocalized() : "StatusWords_NumLock".GetLocalized();
                StatusFontIcon.Glyph = numKeyState ? "\uE785" : "\uE72E";
                numKeyState = !numKeyState;
            }
            else if (vkCode == VK_CAPSLOCK)
            {
                StatusTextBlock.Text = capsKeyState ? "StatusWords_CapsUnlock".GetLocalized() : "StatusWords_CapsLock".GetLocalized();
                StatusFontIcon.Glyph = capsKeyState ? "\uE785" : "\uE72E";
                capsKeyState = !capsKeyState;
            }

            if(FlyoutWindowContextFlyout.IsOpen == false) FlyoutWindowContextFlyout.ShowAt(this);
        }
        return PInvoke.CallNextHookEx(null, nCode, wParam, lParam);
    }

I try to include StatusTextBlock.Text = numKeyState ? "StatusWords_NumUnlock".GetLocalized() : "StatusWords_NumLock".GetLocalized(); in try catch, but nothing changed, still display a crash about "System. ExecutionInexception".

I use these NuGet for core code:

  • Microsoft.Windows.CsWin32 version: 0.3.49-beta
  • Microsoft.WindowsAppSDK version: 1.5.240311000
1

There are 1 best solutions below

3
Simon Mourier On BEST ANSWER

This happens because you're running on .NET wich as a garbage collector (GC) which can move things around without telling you. Hey, that's called "managed" for a reason :-)

But when you hook the keyboard using a native API, you give Windows a raw pointer that's always supposed to point to your method code. If GC changes what the pointer points to, all sort of things can happen at callback time, such as Execution Engine Exceptions, or crashes, etc. This is what you see.

To fix this, you must ask GC to not move your method code. You can use a GCHandle for that or simply create a reference to a static method.

That's what demonstrated here, I keep FlyoutPage's HookCallback instance method almost as is (result is not needed anymore) but give Windows a real static hook method that never moves during runtime:

public sealed partial class FlyoutPage : Page
{
    // this static assignement will ensure GC doesn't move the procedure around
    private static readonly HOOKPROC _hook = GlobalHookCallback;
    private static readonly UnhookWindowsHookExSafeHandle _hookId;

    static FlyoutPage()
    {
        // hook here (expected to be done in UI thread in our case, it facilitates everything)
        _hookId = PInvoke.SetWindowsHookEx(WINDOWS_HOOK_ID.WH_KEYBOARD_LL, _hook, null, 0);
        
        // unhook on exit (more or less useless)
        AppDomain.CurrentDomain.ProcessExit += (s, e) => { _hookId.Close(); };
    }

    // the hook function must be static
    private static LRESULT GlobalHookCallback(int nCode, WPARAM wParam, LPARAM lParam)
    {
        // lucky us WH_KEYBOARD_LL calls back on initial hooking thread, ie: the UI thread
        // so no need for Dispatcher mumbo jumbo
        
        // get your navigation service and defer to the instance method if found
        var navigation = App.GetService<INavigationService>();
        if (navigation?.Frame != null)
        {
            if (navigation.Frame.Content is not FlyoutPage)
            {
                navigation.NavigateTo(typeof(FlyoutViewModel).FullName!);
            }

            if (navigation.Frame.Content is FlyoutPage page)
            {
                page.HookCallback(nCode, wParam, lParam);
            }
        }
        return PInvoke.CallNextHookEx(null, nCode, wParam, lParam);
    }

    public FlyoutPage()
    {
        ... remove hook from here
    }

    private void HookCallback(int nCode, WPARAM wParam, LPARAM lParam)
    {
        ... just defined as void 
    }
 }