The presence of WindowsFormsHost causes the IsKeyboardFocusWithinChanged to be fired at most twice and not more
Asked Answered
C

1

9

I discover a very strange behavior of WindowsFormsHost in WPF. I find that if a WPF control doesn't have WindowsFormsHost as a child control, then IsKeyboardFocusWithinChanged fires properly-- it is fired whenever the WPF control gains or loses focuses, and the variable IsKeyboardFocusWithin is toggled as expected ( true when the control gains focus, false when loses focus).

But, if I host a WindowsFormHost in WPF, then after a short while, the IsKeyboardFocusWithinChanged event is no longer fired for both the WPF mother control and the WindowsFormHost child control.

I can't find in MSDN documentation or SO why so, any reason?

This is my code:

MainWindow.xaml

<Window x:Class="WpfApplication1.MainWindow"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525" IsKeyboardFocusWithinChanged="Window_IsKeyboardFocusWithinChanged">
    <Grid Name="grid1">
    </Grid>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        var mtbDate = new System.Windows.Forms.MaskedTextBox("00/00/0000");
        host.Child = mtbDate;
        host.IsKeyboardFocusWithinChanged += Host_IsKeyboardFocusWithinChanged;
        grid1.Children.Add(host);
    }

    private void Host_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        Console.WriteLine(host.IsKeyboardFocusWithin.ToString()+" blah");
    }

    private System.Windows.Forms.Integration.WindowsFormsHost host;

    private void Window_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
            Console.WriteLine(IsKeyboardFocusWithin.ToString());
    }
}

When the lines involving WindowsFormHost are commented out, then IsKeyboardFocusWithin is true whenever the control gains focus, and false when the control loses focus.

When the lines involving WindowsFormHost are there, then IsKeyboardFocusWithin is true, until I click on the control, and then host.IsKeyboardFocusWithin becomes false, and IsKeyboardFocusWithin also becomes false, and then, no matter what I do, IsKeyboardFocusWithinChanged event will never be fired again.

Coop answered 15/4, 2017 at 4:44 Comment(2)
Typical airspace issue, hard to see what they could have done to make it more predictable. As far as the OS knows, only a HwndHost ever has the focus in a "pure" app and WPF has to emulate focus. But now you added a control that has its own Hwnd, it gets the focus notifications. And there's nothing else you can click on to move the focus back to a UIElement in this sample app, so that's all she wrote. Why this is a problem is not clear, I suppose you can use the MaskedTextBox' Enter/Leave/IsFocused members to work around it.Proulx
@HansPassant, then the problem becomes how do I make IsKeyboardFocusWithin=true? It's a getter property, no setterCoop
L
4

Updated Answer - 05/11

Optimized previous solution to support multiple WindowsFormsHost elements in a Window. Also, updated style to highlight focused control with green border if IsKeyboardFocusWithin property is true.

MainWindow.xaml

<Window x:Class="WpfApplication1.MainWindow"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="325">
    <Window.Resources>
        <ResourceDictionary>
            <Style TargetType="Border">
                <Setter Property="BorderThickness" Value="2" />
                <Setter Property="BorderBrush" Value="Transparent" />
                <Style.Triggers>
                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Child.IsKeyboardFocusWithin}" Value="True">
                        <Setter Property="BorderBrush" Value="Green" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ResourceDictionary>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Button x:Name="hiddenBtn" Height="1" Width="1" />
        <StackPanel Grid.Column="0" x:Name="leftPanel" Margin="5">
            <Label HorizontalContentAlignment="Right">Start Date</Label>
            <Label HorizontalContentAlignment="Right">End Date</Label>
            <Label HorizontalContentAlignment="Right">Phone Number</Label>
            <Label HorizontalContentAlignment="Right">Zip Code</Label>
        </StackPanel>
        <StackPanel Grid.Column="1" x:Name="rightPanel" Margin="5">

        </StackPanel>
    </Grid>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        GenerateControls();
    }

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);

        HwndSource source = PresentationSource.FromVisual(this) as HwndSource;
        source.AddHook(WndProc);
    }

    private const int WM_KILLFOCUS = 0x0008;
    private const int WM_ACTIVATEAPP = 0x001c;
    private const int WM_PARAM_FALSE = 0x00000000;

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        // Handle messages...
        if (msg == WM_KILLFOCUS)
        {
            Console.WriteLine(wParam + " " + lParam);
            //suppress kill focus message if host has keyboardfocus, else don't
            var hosts = FindVisualChildren<WindowsFormsHost>(this);
            var focusedControlHwnd = wParam.ToInt32();
            if(focusedControlHwnd != 0)
            {
                handled = hosts.Any(x => x.Child.Handle.ToInt32() == focusedControlHwnd);
            }
        }
        else if (msg == WM_ACTIVATEAPP && wParam.ToInt32() == WM_PARAM_FALSE)
        {
            //now the kill focus could be suppressed event during window switch, which we want to avoid
            //so we make sure that the host control property is updated 
            var hosts = FindVisualChildren<WindowsFormsHost>(this);
            if (hosts.Any(x => x.IsKeyboardFocusWithin))
                hiddenBtn.Focus();
        }

        return IntPtr.Zero;
    }

    private void GenerateControls()
    {
        System.Windows.Forms.MaskedTextBox maskedTextBox;
        System.Windows.Forms.Integration.WindowsFormsHost host;

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        maskedTextBox = new System.Windows.Forms.MaskedTextBox("00/00/0000");
        host.Child = maskedTextBox;
        rightPanel.Children.Add(new Border() { Child = host });

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        maskedTextBox = new System.Windows.Forms.MaskedTextBox("00/00/0000");
        host.Child = maskedTextBox;
        rightPanel.Children.Add(new Border() { Child = host });

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        maskedTextBox = new System.Windows.Forms.MaskedTextBox("(000)-000-0000");
        host.Child = maskedTextBox;
        rightPanel.Children.Add(new Border() { Child = host });

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        maskedTextBox = new System.Windows.Forms.MaskedTextBox("00000");
        host.Child = maskedTextBox;
        rightPanel.Children.Add(new Border() { Child = host });
    }

    public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
    {
        if (depObj != null)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
                if (child != null && child is T)
                {
                    yield return (T)child;
                }

                foreach (T childOfChild in FindVisualChildren<T>(child))
                {
                    yield return childOfChild;
                }
            }
        }
    }
}

Screenshot

enter image description here

Previous Answer - 05/05

As Hans Passant mentioned in the comment, this behavior is caused due to the fact the WindowsFormsHost and the MaskedTextBox have different Hwnd(s).

The first time you click on host-control, the child control will get focus, and the IsKeyboardFocusedWithin is set properly. But as soon as the child control gets the focus, the OS notices the difference in Hwnd and sends the kill-focus message to WPF window - which in turn sets the IsKeyboardFocusedWithin as false.

What you can do is add a WndProc hook to your WPF main window, and suppress the kill-focus message - only when the host-control's IsKeyboardFocusedWithin value is true.

However, there is a side effect - when you do switch away from WPF window, the host-control's IsKeyboardFocusedWithin value might stay true. In order to resolve this, you can use a simple traversal trick to shift the focus when window-diactivated message is sent and hence have the property IsKeyboardFocusedWithin updated according to current state.

Source code sample: I used a StackPanel instead of a Grid, in order to display the TextBox(s)

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        host = new System.Windows.Forms.Integration.WindowsFormsHost();
        var mtbDate = new System.Windows.Forms.MaskedTextBox("00/00/0000");
        host.Child = mtbDate;
        host.IsKeyboardFocusWithinChanged += Host_IsKeyboardFocusWithinChanged;
        stackPanel1.Children.Add(host);

        textBox1 = new TextBox();
        stackPanel1.Children.Add(textBox1);

        textBox2 = new TextBox();
        stackPanel1.Children.Add(textBox2);
    }

    private void Host_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        Console.WriteLine(host.IsKeyboardFocusWithin.ToString() + " blah");
        textBox1.Text = $"Host.IsKeyboardFocusedWithin = {host.IsKeyboardFocusWithin}";
    }

    private System.Windows.Forms.Integration.WindowsFormsHost host;
    private TextBox textBox1;
    private TextBox textBox2;

    private void Window_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        Console.WriteLine(IsKeyboardFocusWithin.ToString());
        textBox2.Text = $"Window.IsKeyboardFocusedWithin = {IsKeyboardFocusWithin}";
    }

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);

        HwndSource source = PresentationSource.FromVisual(this) as HwndSource;
        source.AddHook(WndProc);
    }

    private const int WM_KILLFOCUS = 0x0008;
    private const int WM_ACTIVATEAPP = 0x001c;
    private const int WM_PARAM_FALSE = 0x00000000;

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        // Handle messages...
        if (msg == WM_KILLFOCUS) 
        {
            //suppress kill focus message if host has keyboardfocus, else don't
            handled = host.IsKeyboardFocusWithin;
        }
        else if (msg == WM_ACTIVATEAPP && wParam.ToInt32() == WM_PARAM_FALSE)
        {
            //now the kill focus could be suppressed event during window switch, which we want to avoid
            //so we make sure that the host control property is updated by traversal (or any other method)
            if (host.IsKeyboardFocusWithin)
            {
                host.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
            }
        }

        return IntPtr.Zero;
    }
}

And, the result will look like this:

With Focus

enter image description here

Without Focus

enter image description here

Lecia answered 5/5, 2017 at 15:59 Comment(1)
@Graviton: Have optimized the solution to be more elegant in handling multiple WindowsFormsHost elements for focus issue . Hopefully, this will help resolve the problem. Thanks.Lecia

© 2022 - 2024 — McMap. All rights reserved.