3

I have two controls a ToolBar and a TextBox. The ToolBar has a Button which opens a Popup with another Button in it.

Current behavior: if i click inside the TextBox and it becomes focused and then click the button from ToolBar which opens a Popup the TextBox is still focused and receives all Keyboard input.

Now i know this is the default behavior for items inside a FocusScope which the ToolBar is, but i don't need this behavior when a popup is open. How can i avoid it ?

Here is the example:

<Window x:Class="WpfApplication67.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
</Window.Resources>
<StackPanel HorizontalAlignment="Left" Width="400">
    <ToolBar>
        <ToggleButton Name="openButton">Open Popup</ToggleButton>
        <Popup Focusable="True"
            Placement="Right"        
            StaysOpen="False" 
            IsOpen="{Binding IsChecked, ElementName=openButton, Mode=TwoWay}"
            PlacementTarget="{Binding ElementName=openButton}">
            <StackPanel 
                Width="100" 
                Height="100" 
                Background="DarkGray" 
                Focusable="True">
                    <Button>More</Button>
                </StackPanel>
        </Popup>
    </ToolBar>

    <TextBox Text="Set focus on this..." />

</StackPanel>

EDIT: I'm striving to find an explanation about Who moves the focus on button click inside a nested FocusScope and How i can stop some Buttons (like the one inside a Popup) from doing it.

Novitchi S
  • 3,711
  • 1
  • 31
  • 44
  • 1
    Set focus on stackpanel manually on `Opened` event of Popup like this - `stackPanel.Focus()`. – Rohit Vats Mar 10 '14 at 17:36
  • I was considering this as @Anatoliy Nikolaev suggested to move the focus and it works fine, but if i click on the "More" Button the focus will be restored to the FocusedElement of the main FocusScope (the Window) so the TextBox will become focused again. – Novitchi S Mar 10 '14 at 17:41
  • And i cannot set `Focusable="False"` on buttons within the popup since the popups can have lots of complex functionality. – Novitchi S Mar 10 '14 at 17:47
  • I have posted an answer. See if that helps you out. – Rohit Vats Mar 15 '14 at 20:12

2 Answers2

7

You mainly have three requirements (correct me if am wrong):

  1. If pop up is opened, focus should be inside popUp i.e. on StackPanel.
  2. On close of popUp, focus should retained back to textBox.
  3. When button inside popUp is clicked, focus should not leave the popUp.

Let's pick these requirements one by one.

If pop up is opened, focus should be inside popUp i.e. on StackPanel.

Like I mentioned in the comments, put focus on stackPanel on Opened event of PopUp:

private void Popup_Opened(object sender, EventArgs e)
{
   stackPanel.Focus();
}

On close of popUp, focus should retained back to textBox.

Hook Closed event and put focus back on TextBox:

private void Popup_Closed(object sender, EventArgs e)
{
   textBox.Focus();
}

When button inside popUp is clicked, focus should not leave the popUp.

Now, comes the tricky part. As mentioned in the comments as soon as you click on button inside popUp, focus is moved outside of PopUp.

What you can do prevent is to attach a handler to PreviewLostKeyboardFocus event of stackPanel. In handler check for condition if keyBoard focus is within popUp, set e.Handled = true so that event gets handled here only and no bubble event is raised which will force the keyboard focus outside of stackPanel.

That being said in case you have another TextBox inside stackPanel besies button, handling event won't allow you to move focus within popUp as well. To avoid such situations you can check if new focused element doesn't belong within stackPanel then only handle the event.

Here's the code to achieve that (add handler on PreviewLostKeyboardFocus event of StackPanel):

private void stackPanel_PreviewLostKeyboardFocus(object sender, 
                                             KeyboardFocusChangedEventArgs e)
{
   var newFocusedChild = e.NewFocus as FrameworkElement;
   bool isMovingWithinPanel = newFocusedChild != null
                              && newFocusedChild.Parent == stackPanel;

   // If keyboard focus is within stackPanel and new focused element is
   // outside of stackPanel boundaries then only handle the event.
   if (stackPanel.IsKeyboardFocusWithin && !isMovingWithinPanel)
      e.Handled = true;
}
Rohit Vats
  • 79,502
  • 12
  • 161
  • 185
  • What i was doing before this, was removing all parent focus scopes when a popup is opened, but this solution is better even though it's strange that there is not a better way to stop the issue number 3. Your solution was changing the `KeyboardFocus` to `null`, what i need is to have the focus back to the `Button`, i added `this.Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() => (e.OldFocus as FrameworkElement).Focus()));` to `PreviewLostKeyboardFocus` handler after `e.Handled` is set to true and it finally works. – Novitchi S Mar 17 '14 at 10:04
  • Glad it worked for you..!! Yeah focus is one thing which is real complex in WPF and have been a pain area :( – Rohit Vats Mar 17 '14 at 11:57
3

In this situation, there are two ways, both options have been added in the handler of Click event for openButton.

First

The easiest option, it is clear focus for your keyboard, like this:

private void openButton_Click(object sender, RoutedEventArgs e)
{
    Keyboard.ClearFocus();
}

Second

A more universal method it is move Focus to the parent:

private void openButton_Click(object sender, RoutedEventArgs e)
{
    FrameworkElement parent = (FrameworkElement)MyTextBox.Parent;

    while (parent != null && parent is IInputElement && !((IInputElement)parent).Focusable)
    {
        parent = (FrameworkElement)parent.Parent;
    }

    DependencyObject scope = FocusManager.GetFocusScope(MyTextBox);
    FocusManager.SetFocusedElement(scope, parent as IInputElement);                 
}

For the latter case, I created attached behavior to make it more convenient to use, which can be founded here:

Set focus back to its parent?

Edit

If you want to be when you close Popup focus back to the TextBox, then add handlers of events Opened and Closed for Popup like this:

private void MyPopup_Opened(object sender, EventArgs e)
{
    Keyboard.ClearFocus();
    StackPanelInPopup.Focus();
}

private void MyPopup_Closed(object sender, EventArgs e)
{
    MyTextBox.Focus();
}
Community
  • 1
  • 1
Anatoliy Nikolaev
  • 22,370
  • 15
  • 69
  • 68
  • If the focus is moved from TextBox to its parent, i will lose the ability to restore the focus automatically when the popup is closed. I tried to move it inside the Popup but there is still an issue with this described in comment to @Rohit Vats from above. – Novitchi S Mar 10 '14 at 17:43
  • @NovitchiS: How about `Keyboard.ClearFocus();`? I tested at both ways and they worked perfectly. – Anatoliy Nikolaev Mar 10 '14 at 17:50
  • The `Keyboard.ClearFocus();` has the same issue as moving focus to parent, the `TextBox` will lose its focus but when the popup is closed the focus will not be restored to the `TextBox` automatically. I am very happy with the focus being always inside the popup while it is open. – Novitchi S Mar 10 '14 at 17:57
  • I am still investigating it, and my biggest problem is still present - when i click the button inside the popup the keyboard focus is moved away from the popup, so if i have a logic inside the popup to close on Esc key it will never enter the event-handler. – Novitchi S Mar 11 '14 at 09:55