How to focus on a newly added TabItem of a WPF TabControl?
Asked Answered
W

1

2

I'm facing an issue where I need to be able to actually focus a TabItem tab in a TabControl like I would press the TAB key. I know I could use SendKeys.SendWait("{TAB}"); in order to achieve that but I would prefer a more robust solution.

The tab items are bound to an ItemSource of an ObservableCollection. That collection contains a list of view models later bound to the tab items title, icon, content and more. The collection is filled and emptied at runtime, I don't have a static collection.

I've made a sample project to illustrate the issue (see code below).

Current behavior after I added a tab item to the collection looks like this: New tab isn't focused

After pressing the "Add Tab" button on the left a new tab is added to the ObservableCollection which is then reflected in a new tab item in the tab control.

I try to focus that new tab with:

  • Set the SelectedItem of the TabControl after a new item is added (see code below)
  • Keyboard.Focus((UIElement)sender) in the tab item loaded event (see code below)
  • by setting FocusManager.FocusedElement="{Binding ElementName=tabHeader}" in XAML (see code below)

None of those attempts seems to work.

Desired behavior would be that the new added tab item actually get focused (like I would press the TAB key) which would look like this:

New tab is focused

To create this image I pressed TAB after I added a new tab item.

I couldn't find any working solution here or here and also Microsoft's documentation regarding Focus didn't shed enough light.

Any hint is appreciated.




To reproduce the screenshots above here is my sample project:

MainWindow.xaml

<Window x:Class="WpfAppTabControl.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:WpfAppTabControl"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:MainViewViewModel}"
        Title="MainWindow" Height="450" Width="800">
    <Grid>

    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="350"/>
      <ColumnDefinition Width="12"/>
      <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <Button Grid.Column="0" Grid.Row="2" Margin="50,50,50,50" Command="{Binding AddTabCommand}">Add Tab</Button>

    <GridSplitter Grid.Column="1" Grid.Row="2" ResizeDirection="Columns" ResizeBehavior="PreviousAndNext" HorizontalAlignment="Stretch"/>

    <TabControl Grid.Column="2"
                Grid.Row="2"
                ItemsSource="{Binding ItemsToDisplay}"
                SelectedItem="{Binding ActiveTabItem}"
                SelectionChanged="OnSelectionChanged"
                HorizontalContentAlignment="Stretch"
                VerticalContentAlignment="Stretch"
                FocusManager.FocusedElement="{Binding ElementName=tabHeader}"
                FocusManager.IsFocusScope="True">
      <TabControl.ItemContainerStyle>
        <Style TargetType="TabItem">
          <EventSetter Event="Loaded" Handler="OnTabItemLoaded"/>
          <Setter Property="IsSelected" Value="True"/>
          <Setter Property="Focusable" Value="True"/>
          <Setter Property="HeaderTemplate">
            <Setter.Value>
              <DataTemplate>
                <DockPanel x:Name="tabHeader" Margin="-5,-1,-5,0" Focusable="True">
                  <Button x:Name="closeButton"
                          Width="16"
                          Height="16"
                          Margin="20, 10, 10, 10"
                          Command="{Binding CloseTabCommand}"
                          CommandParameter="{Binding Title}"
                          BorderBrush="Transparent"
                          DockPanel.Dock="Right"
                          BorderThickness="0">
                    <Image x:Name="tabIcon" Source="/close.png" Height="16" Margin="0,0,0,0" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                  </Button>
                  <TextBlock Margin="5, 10, 10, 10" VerticalAlignment="Center" Text="{Binding Title}"/>
                </DockPanel>
              </DataTemplate>
            </Setter.Value>
          </Setter>
          <Setter Property="Content" Value="{Binding Content}" />
        </Style>
      </TabControl.ItemContainerStyle>
    </TabControl>

  </Grid>
</Window>

MainWindow.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfAppTabControl
{
   public partial class MainWindow : Window
   {
      public MainWindow()
      {
         InitializeComponent();
         DataContext = new MainViewViewModel();
      }

      private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
      {
         var viewModel = (MainViewViewModel)DataContext;
         viewModel.NotifySelectedItemChanged(e.AddedItems);
      }

      private void OnTabItemLoaded(object sender, RoutedEventArgs e)
      {
         Keyboard.Focus((UIElement)sender);
      }
   }
}

ItemViewModel.cs

using Prism.Mvvm;
using System;
using System.Windows.Input;

namespace WpfAppTabControl
{
   public class ItemViewModel : BindableBase, IDisposable
   {
      private string title;
      private string content;

      public ItemViewModel(ICommand closeTabCommand)
      {
         CloseTabCommand = closeTabCommand ?? throw new ArgumentNullException(nameof(closeTabCommand));
      }

      public string Title
      {
         get => title;

         set
         {
            if (title != value)
            {
               title = value;
               RaisePropertyChanged(title);
            }
         }
      }
      public string Content
      {
         get => content;

         set
         {
            if (content != value)
            {
               content = value;
               RaisePropertyChanged(content);
            }
         }
      }

      public ICommand CloseTabCommand
      {
         get;
         private set;
      }

      public void Dispose()
      {
         // disposing stuff
      }
   }
}

MainViewViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;

namespace WpfAppTabControl
{
   public class MainViewViewModel : BindableBase
   {
      private readonly ObservableCollection<ItemViewModel> itemsToDisplay;
      private ItemViewModel activeTabItem;

      public MainViewViewModel()
      {
         itemsToDisplay = new ObservableCollection<ItemViewModel>();
         AddTabCommand = new DelegateCommand(AddTab);
      }

      public IEnumerable<ItemViewModel> ItemsToDisplay => itemsToDisplay;

      public ICommand AddTabCommand
      {
         get;
         private set;
      }

      public ItemViewModel ActiveTabItem
      {
         get => activeTabItem;
         set
         {
            if (activeTabItem != value)
            {
               activeTabItem = value;
               RaisePropertyChanged();
            }
         }
      }

      private void AddTab()
      {
         var tabCountString = (itemsToDisplay.Count + 1).ToString();
         var itemViewModel1 = new ItemViewModel(new DelegateCommand<string>(title => { CloseTab(title); }))
         {
            Title = "NewTab" + tabCountString,
            Content = "Content of " + "NewTab" + tabCountString,
         };

         itemsToDisplay.Add(itemViewModel1);
         ActiveTabItem = itemViewModel1;
      }

      private void CloseTab(string title)
      {
         if (string.IsNullOrEmpty(title))
         {
            return;
         }

         var tabToClose = itemsToDisplay.FirstOrDefault(viewModel => viewModel.Title == title);
         if (tabToClose != null)
         {
            itemsToDisplay.Remove(tabToClose);
         }
      }

      public void NotifySelectedItemChanged(IList addedItems)
      {
         if (addedItems.Count > 0)
         {
            // stuff to do
         }
      }
   }
}
Waxplant answered 10/11, 2022 at 8:14 Comment(6)
Setting focus on a control and making its focus visual (dotted lines) visible will not necessarily happen at the same time. Which do you want?Ernaldus
If possible both in this order.Waxplant
According to this comment https://mcmap.net/q/671528/-focus-visual-not-showing-when-navigating-focus-programically, focus visual will be shown only if the last input was from the keyboard.Ernaldus
There seems to be some sort of workaround here (social.msdn.microsoft.com/Forums/en-US/…) to make this visual focus to show up. I couldn't make it work yet. Anyway in this case I would want setting the focus on that newly added tab item. I'm still not sure if what I did actually sets the focus.Waxplant
The focus visual is meant to indicate the current navigated element. Because when navigating by keyboard you have no cue to show the navigation order (tab index). When navigating by mouse, the user always knows the navigated target because he explicitly clicked on it. Forcing the focus visual to always show will break the Windows UX (aside from looking ugly).Dictaphone
When selecting a TabItem e.g., via TabControl.SelectedItem, the selected tab is visually highlighted which makes the focus visual redundant. It's more useful if the user wants to keyboard select e.g. a button (as button doesn't have such highlight behavior).Dictaphone
C
0

Had the same issue. Add the new tabitem to tabcontrol: example TabItem newtabitem = new TabItem then use newtabitem.Focus(). This moves the focus to the added tab.

Consequent answered 4/12, 2022 at 8:15 Comment(5)
I've a collection where I add entries which automatically result in a new tab. From where do I get the TabItem newtabitem from?Waxplant
newtabitem is a placeholder for the local variable for the new tabitem. It is used to access objects of the tabitem such as header Style Name etc. My use is from a button click which displays a WPF page as the content of the tab. which I then set the focus.Consequent
Well, as I described in my question I do have a collection of view models which when added to a collection automatically creates a tab item from it. I'm wondering if you found a way to access this tab item?Waxplant
In your AddTab() method try itemViewModel1.Focus()Consequent
I've already checked that. There is no Focus() method. The view model I use doesn't extend any TabItem class or so (public class ItemViewModel : BindableBase, IDisposable). See my code above. This is the problem, I don't have "direct" access to the generated tab item.Waxplant

© 2022 - 2024 — McMap. All rights reserved.