Graviton Graviton - 1 year ago 49
C# Question

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

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.

Answer Source

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

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download