Home > OS >  Return keyboard focus
Return keyboard focus

Time:12-19

I have an application where the main window contains a user control, and inside that user control are items stored in an ItemsControl. Each item can be removed by clicking an 'x' button.

The problem I am facing is that although the Keyboard focus is initially set to the user control, when you remove an item, focus is then transferred to the main window, instead of back to the user control?

Is there a way I can fix this without having to add code behind to manually store/retrieve/set focus after the click?

I have lots of these buttons within my application and I'm trying to avoid having to add code all over the place to manage returning the Focus.

I have created a very simple example to show the issue :

    <UserControl x:Class="WpfApp28.MyControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid Width="300">
        <StackPanel>
            <ItemsControl ItemsSource="{Binding}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <TextBlock Text="{Binding}" />
                            <Button Content="x"
                                    Width="20"
                                    HorizontalAlignment="Right"
                                    VerticalAlignment="Center"
                                    Click="Button_Click" />
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </StackPanel>
    </Grid>
</UserControl>



public partial class MyControl : UserControl
{
    public MyControl()
    {
        InitializeComponent();
        Focusable = true;
        Loaded  = MyControl_Loaded;
    }

    private void MyControl_Loaded(object sender, RoutedEventArgs e)
    {
        Keyboard.Focus(this);
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        if (sender is FrameworkElement fe && fe.DataContext is string item)
        {
            (DataContext as ObservableCollection<string>).Remove(item);
        }
    }
}

<Window x:Class="WpfApp28.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApp28"
    Title="MainWindow" Height="450" Width="800">
<local:MyControl DataContext="{Binding Items}" />
 public partial class MainWindow : Window
{
    public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();

    public MainWindow()
    {
        Items.Add("hello");
        Items.Add("there");
        Items.Add("world");

        DataContext = this;
        InitializeComponent();

        DispatcherTimer t = new DispatcherTimer();
        t.Interval = TimeSpan.FromMilliseconds(250);
        t.Tick  = T_Tick;
        t.Start();
    }

    private void T_Tick(object? sender, EventArgs e)
    {
        Title = Keyboard.FocusedElement?.GetType().ToString() ?? "NULL";
    }
}

CodePudding user response:

The reason that the keyboard focus moves to the hosting Window is obvious once you understand how WPF handles focus. It's important to know that WPF uses scopes in which the focus traverses the elements.
There can be multiple focus scopes allowing multiple elements to remain focused simultaneously.
By default, the hosting Window defines a focus scope. Since it is the only focus scope, it is global (the scope of the complete visual tree).

What happens in your code in short:

  1. The Button receives the focus via mouse click
  2. The click handler removes the clicked item and therefore the clicked Button from the visual tree
  3. WPF moves focus back to the focus scope root, which is the MainWindow in your case

You have multiple options to prevent the focus from being moved back to the focus root. Some involve code-behind.

The following examples show how to move the focus back to the parent UserControl. But it could be any element as well:

  1. You can configure the Button (the element that "steals" the current focus) to be not focusable. This only works if the UserControl is already focused:
<DataTemplate>
  <Button Content="x"
          Focusable="False" />
</DataTemplate>
  1. You can introduce a new focus scope. Since you want the UserControl itself to be focused, you must choose the root element of the UserControl. You can achieve this by using the FocusManager helper class:
<UserControl>
  <Grid x:Name="RootPanel" 
        FocusManager.IsFocusScope="True"
        Width="300">
  </Grid>
</UserControl>
  1. You can of course register a Button.Click handler or preferably a routed command to move the focus back to the UserControl explicitly. A routed command can be more convenient in most cases. It allows to send a command parameter that makes the code-behind simpler.
    Note, since Button.Click is a routed event, you can simply register a Button.Click event handler on the UserControl. This example uses the existing click handler that is used to remove the item from the ItemsControl:

UserControl.xaml

<DataTemplate>
  <Button Content="x"
          Click="OnButtonClick" />
</DataTemplate>

UserControl.xaml.cs

private void OnButtonClick(object sender, RoutedEventArgs)
{
  /* Delete the item */

  Keyboard.Focus(this);
}

Final suggested solution

To improve your code and handling of the UserControl you must definitely implement an ItemsSource dependency property and use a routed command to delete the items.

The following example uses the predefined ApplicationCommands.Delete routed command. You will notice how simple the code has become:

MyControl.xaml.cs

public partial class MyControl : UserControl
{
  public IList ItemsSource
  {
    get => (IList)GetValue(ItemsSourceProperty);
    set => SetValue(ItemsSourceProperty, value);
  }

  public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
    "ItemsSource",
    typeof(IList),
    typeof(UserControl4), new PropertyMetadata(default));

  public MyControl()
  {
    InitializeComponent();
    this.Focusable = true;
  }

  public override void OnApplyTemplate() 
    => Keyboard.Focus(this);


  private void DeleteItemCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    => this.ItemsSource.Remove(e.Parameter);

  private void DeleteItemCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    => e.CanExecute = this.ItemsSource.Contains(e.Parameter);
}

MyControl.xaml

<UserControl>
  <UserControl.CommandBindings>
    <CommandBinding Command="{x:Static ApplicationCommands.Delete}"
                    Executed="DeleteItemCommand_Executed"
                    CanExecute="DeleteItemCommand_CanExecute" />
  </UserControl.CommandBindings>

  <Grid x:Name="RootPanel" 
        FocusManager.IsFocusScope="True">
    <StackPanel>
      <ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:UserControl4}, Path=ItemsSource}"
               >
        <ItemsControl.ItemTemplate>
          <DataTemplate>
            <Grid>
              <TextBlock Text="{Binding}" />
              <Button Content="x"
                      Command="{x:Static ApplicationCommands.Delete}"
                      CommandParameter="{Binding}"
                      Width="20"
                      HorizontalAlignment="Right"
                      VerticalAlignment="Center" />
            </Grid>
          </DataTemplate>
        </ItemsControl.ItemTemplate>
      </ItemsControl>
    </StackPanel>
  </Grid>
</UserControl>

MainWindow.xaml

<Window>
  <MyControl ItemsSource="{Binding Items}" />
</Window>

Remarks You should consider to use a ListBox instead of the pure ItemsControl.
ListBox is an extended ItemsControl. It will significantly improve performance and provides a ScrollViewer by default.

  • Related