Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Getting the Current TabItem when the Tab is not selected in WPF


:P
On this page:

Banner

This is a quick reminder to self on how to retrieve a TabItem from a WPF TabControl when the tab is not currently selected because I run into this quite frequently and the flail around trying to remember how to get non-selected items on any list control including tab controls.

Hovered Tab vs Selected Tab

The issue is that WPF doesn't have a direct mechanism to retrieve a TabItem that the cursor is over if it's not actually selected. Hovering is not selecting and so SelectedItem and the 'Hovered Item' are two very different thing and WPF is adverse to the latter 😄 WPF only lets you retrieve an item directly via the various Selected... properties like SelectedItem, SelectedValue, SelectedIndex etc. all of which only work off the item that is selected... which may be different from the item that the mouse is currently hovering over.

The most common scenarios for retrieving the hovered item are:

  • Popping up a context menu
  • Showing a Tooltip

Both of these are context sensitive operations and that's exactly why they need more than what the Selected... properties can provide.

Hovered selections are not that common, but when you need it you need it!

An Example of Hovered Tabs

Here's an example that demonstrates this exact setup in Markdown Monster for manipulating Sidebar panels on the (icon only) tab control on the left:

Markdown Monster Sidebar Tab Context Menu
Figure 1 - A Sidebar of tabs, that allows selection of tabs that are not selected to show an appropriate context menu

The icon tabs on the left can be accessed with the context menu even when the tab is not active. You can see that the right tab is selected as the context menu shows the tab name and icon along with the context options. I've run into similar scenarios with context menus, and tooltips that need to pop up over non active icons - the idea of being able to select the tab

ContextMenu Opening Event Handling

Tab Controls are a pain in the ass in WPF because they have a very peculiar way of what is considered a tab. For some operations only the header is considered as a tab, but for others the entire tab including it's body/content size is considered.

So in the above scenario I need to capture the currently hovered tab. I started with just using the selected item:

private void LeftSidebar_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
    var src = sender as TabControl
    var tab = src?.SelectedItem;
    
    if (tab == null) return;
}    

For many situations this is probably all that you need. If you are working only off selected tabs then this code works totally fine.

In the case of my Sidebar example though the above would cause a problem: If I hover over the favorite tab when the Folder Browser is actually active, the context menu that pops up shows the Folder Browser as the context item - Not what I want.

Fix it: OringalSource and Parent Hierarchy

So, to find a non-active tab item is a bit less obvious. You can start by looking at the original source element that the context menu event is initiated from using e.OriginalSource. You then walk up the parent hierarchy to check if you have a TabItem (a MetroTabItem in my case) in the list of parents.

Here's what this looks like:

private async void LeftSidebar_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
    var src = e.OriginalSource as FrameworkElement;
    if (src == null) return;

    var tab = WindowUtilities.FindAncestor<MetroTabItem>(src);
    if (tab == null) return;

    // now pop up the menu with the TabItem as context
    var context = new LeftSidebarContextMenu(tab);
    await context.OpenContextMenu();
    e.Handled = true;
}

The key here is a small utility function WindowUtilities.FindAncestor<T>() which is used to walk up the control hierarchy to see if there's a matching control of the provided generic type - in this case a MetroTabItem (a TabItem subclass from MahApps.Metro).

Here's what that method looks like:

/// <summary>
/// Finds a type of element in the parent chain of an element
/// </summary>
/// <typeparam name="T">Type of Element to find</typeparam>
/// <param name="currentControl">control to start from</param>
/// <returns></returns>
public static T FindAncestor<T>(DependencyObject currentControl)
    where T : DependencyObject
{
    do
    {
        if (currentControl is T) return (T) currentControl;

        currentControl = VisualTreeHelper.GetParent(currentControl);
    } while (currentControl != null);

    return null;
}

FindAncestor<T> is quite useful for many similar operations - you can use a similar approach to find non-selected TreeView or ListView items for example.

Summary

It's a simple and pretty old trick, but it's one of those things that I at least always forget and curse when I need to get access to the actual item, and not the selected item. I'm always naively thinking there should be a native way on control to get this, but... no!

The trick of looking at the original source control and then walking up the tree, looking for specific types of controls is useful here for find the TabItem, but it's also useful for most other Items Controls. For me at least, this is not the thing that I reach for and so this post serves as a reminder and placeholder so I can quickly jog my memory in the future. If you found your way here via search, it probably saved you a few cycles as well...

this post created and published with the Markdown Monster Editor
Posted in WPF  

The Voices of Reason


 

Stephen Cleary
November 09, 2024

# re: Getting the Current TabItem when the Tab is not selected in WPF

Ah, the old "navigate the WPF tree" technique. One thing I've found useful here is to define enumerable primitives for different "axes" (visual/logical tree, ancestors/descendants, whatever) and then you can query the WPF tree using LINQ:

public static IEnumerable<DependencyObject> SelfAndAncestors(this DependencyObject currentControl)
{
    while (currentControl != null)
    {
        yield return currentControl;
        currentControl = VisualTreeHelper.GetParent(currentControl);
    }
}

Once you have simple helper methods like the above, then you can query it in any way you want:

var tab = src.SelfAndAncestors().OfType<MetroTabItem>().FirstOrDefault();

That way you have an "ancestor axis" that you can traverse, and the consuming code is more explicit - it's clear that it's looking for the first MetroTabItem and returns null if none is found (without having to check the XML documentation for FindAncestor).


Rick Strahl
November 09, 2024

# re: Getting the Current TabItem when the Tab is not selected in WPF

@Steven - oh I like that! Adding to my helper library! Thanks!


Felix Strauss
November 11, 2024

# re: Getting the Current TabItem when the Tab is not selected in WPF

Interesting topic! I am experimenting with this too. I have two WPF TabControls next to each other (like a split view). My goal is to know which tab is currently active. SelectedItem does not change if the user switches to the tab in the other control. A tab is considered 'active' if a child control (eg. TextBox) has user focus.

Maybe you have an idea how I could solve this problem?


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024