Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

WPF TabControl Focus Behavior with Invisible Tabs (MVVM)

5.00/5 (2 votes)
27 Dec 2014CPOL2 min read 13.4K  
WPF TabControl Focus Behavior with Invisible Tabs (MVVM)

Introduction

Sometimes, I can get really excited about WPF. For example, last week I implemented a MultiDataTrigger to show an appropriate image resource for a certain ViewModel, based on several different values of the object. That worked great.

Other times, I get less excited about WPF, for example when using a TabControl where tabs are hidden or shown, depending on certain properties of the ViewModel.

The problem sometimes when a tab item’s Visibility is set to Collapsed. I would expect that the next visible tab gains the focus, but this does not happen!

To solve this problem, I decided to create an attached behavior, that will shift the current tab to the next (or previous if no next tab is available) as soon as the current tab’s Visibility property changes to Hidden or Collapsed.

To get this done, we’ll create an attached behavior for the TabControl with the FocusFirstVisibleTab dependency property , which you can use in XAML.
The behavior you will get if you set TabControl.FocusFirstVisibleTab=”True” is two things:

  • When the current tab of the tab control is not visible, move to the next tab item.
  • When the currently visible tab’s Visibility property changes to a non-visible state, move to the next tab item.

Start by creating the static class that holds the dependency property and methods to handle the change of the FocusFirstVisibleTab property:

C#
/// <summary>Behaviors for TabControl.
/// </summary>
public class TabControlBehavior
{
    #region Property FocusFirstVisibleTab
 
    /// <summary>Whether to focus the first visible tab.
    /// </summary>
    /// <remarks>Setting the FocusFirstVisibleTab 
    /// attached property to true will focus the next visible tab when
    /// the current selected tab's Visibility property is set to Collapsed or Hidden.</remarks>
    public static readonly DependencyProperty FocusFirstVisibleTabProperty =
        DependencyProperty.RegisterAttached("FocusFirstVisibleTab", typeof(bool),
            typeof(TabControlBehavior), 
            new FrameworkPropertyMetadata(OnFocusFirstVisibleTabPropertyChanged));
 
    /// <summary>Gets the focus first visible tab value of the given element.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <returns></returns>
    public static bool GetFocusFirstVisibleTab(TabControl element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }
        return (bool)element.GetValue(FocusFirstVisibleTabProperty);
    }
 
    /// <summary>Sets the focus first visible tab value of the given element.
    /// </summary>
    /// <param name="element">The element.</param>
    /// <param name="value">if set to <c>true</c> [value].</param>
    public static void SetFocusFirstVisibleTab(TabControl element, bool value)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }
        element.SetValue(FocusFirstVisibleTabProperty, value);
    }
 
    /// <summary>Determines whether the value of the 
    /// dependency property <c>IsFocused</c> has change.
    /// </summary>
    /// <param name="d">The dependency object.</param>
    /// <param name="e">The <see 
    /// cref="System.Windows.DependencyPropertyChangedEventArgs"/> 
    /// instance containing the event data.</param>
    private static void OnFocusFirstVisibleTabPropertyChanged
        (DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        throw new System.NotImplementedException("TabControlBehavior");
    }
 
    #endregion Property FocusFirstVisibleTab
}

What we have to do now is to attach event handlers to the tab item collection (in case tabs are added or removed) and to each individual tab (in case the current tab’s Visibility property has changed).

This is accomplished in the implementation of the OnFocusFirstVisibleTabPropertyChanged method (which gets fired when the attached behavior’s property is changed):

C#
/// <summary>Determines whether the value of the dependency property <c>IsFocused</c> has change.
/// </summary>
/// <param name="d">The dependency object.</param>
/// <param name="e">The <see 
/// cref="System.Windows.DependencyPropertyChangedEventArgs"/> 
/// instance containing the event data.</param>
private static void OnFocusFirstVisibleTabPropertyChanged
    (DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var tabControl = d as TabControl;
    if (tabControl != null)
    {
        // Attach or detach the event handlers.
        if ((bool)e.NewValue)
        {
            // Enable the attached behavior.
            tabControl.Items.CurrentChanged += new EventHandler(TabControl_Items_CurrentChanged);
            var collection = tabControl.Items as INotifyCollectionChanged;
            if (collection != null)
            {
                collection.CollectionChanged += 
                    new NotifyCollectionChangedEventHandler(TabControl_Items_CollectionChanged);
            }
        }
        else
        {
            // Disable the attached behavior.
            tabControl.Items.CurrentChanged -= new EventHandler(TabControl_Items_CurrentChanged);
            var collection = tabControl.Items as INotifyCollectionChanged;
            if (collection != null)
            {
                collection.CollectionChanged -= 
                    new NotifyCollectionChangedEventHandler(TabControl_Items_CollectionChanged);
            }
            // Detach handlers from the tab items.
            foreach (var item in tabControl.Items)
            {
                TabItem tab = item as TabItem;
                if (tab != null)
                {
                    tab.IsVisibleChanged -= 
                        new DependencyPropertyChangedEventHandler(TabItem_IsVisibleChanged);
                }
            }
        }
    }
}
 
/// <summary>Handles the CollectionChanged event of the TabControl.Items collection.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see 
/// cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> 
/// instance containing the event data.</param>
static void TabControl_Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    // TODO: Attach or detach an event handler to the IsVisibleChanged event of each tab item.
    throw new System.NotImplementedException("TabControlBehavior");
}
 
/// <summary>Handles the CurrentChanged event of the TabControl.Items collection.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see 
/// cref="System.EventArgs"/> instance containing the event data.</param>
static void TabControl_Items_CurrentChanged(object sender, EventArgs e)
{
    // TODO: Test whether the new current item is visible. If not, move on to the next.
    throw new System.NotImplementedException("TabControlBehavior");
}

Now we get to the point where to attach event handlers to the IsVisibleChanged event of each tab. Keep in mind to detach the event handler from old items:

C#
/// <summary>Handles the CollectionChanged event of the TabControl.Items collection.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see 
/// cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> 
/// instance containing the event data.</param>
static void TabControl_Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    // Attach event handlers to each tab so that when the Visibility property changes of the selected tab,
    // the focus can be shifted to the next (or previous, if not next tab available) tab.
    var collection = sender as ItemCollection;
    if (collection != null)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
            case NotifyCollectionChangedAction.Remove:
            case NotifyCollectionChangedAction.Replace:
                // Attach event handlers to the Visibility and IsEnabled properties.
                if (e.NewItems != null)
                {
                    foreach (var item in e.NewItems)
                    {
                        TabItem tab = item as TabItem;
                        if (tab != null)
                        {
                            tab.IsVisibleChanged += 
                            new DependencyPropertyChangedEventHandler(TabItem_IsVisibleChanged);
                        }
                    }
                }
                // Detach event handlers from old items.
                if (e.OldItems != null)
                {
                    foreach (var item in e.OldItems)
                    {
                        TabItem tab = item as TabItem;
                        if (tab != null)
                        {
                            tab.IsVisibleChanged -= 
                            new DependencyPropertyChangedEventHandler(TabItem_IsVisibleChanged);
                        }
                    }
                }
                break;
            case NotifyCollectionChangedAction.Reset:
                // Attach event handlers to the Visibility and IsEnabled properties.
                foreach (var item in collection)
                {
                    TabItem tab = item as TabItem;
                    if (tab != null)
                    {
                        tab.IsVisibleChanged += 
                        new DependencyPropertyChangedEventHandler(TabItem_IsVisibleChanged);
                    }
                }
                break;
            case NotifyCollectionChangedAction.Move:
            default:
                break;
        }
 
        // Select the first element if necessary.
        if (collection.Count > 0 &amp;&amp; collection.CurrentItem == null)
        {
            collection.MoveCurrentToFirst();
        }
    }
}
 
/// <summary>Handles the IsVisibleChanged event of the tab item.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see 
/// cref="System.Windows.DependencyPropertyChangedEventArgs"/> 
/// instance containing the event data.</param>
static void TabItem_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    throw new System.NotImplementedException("TabControlBehavior");
}

That leaves us with only two unimplemented methods; TabItem_IsVisibleChanged and TabControl_Items_CurrentChanged:

C#
/// <summary>Handles the CurrentChanged event of the TabControl.Items collection.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see 
/// cref="System.EventArgs"/> instance containing the event data.</param>
static void TabControl_Items_CurrentChanged(object sender, EventArgs e)
{
    var collection = sender as ItemCollection;
    if (collection != null)
    {
        UIElement element = collection.CurrentItem as UIElement;
        if (element != null &amp;&amp; element.Visibility != Visibility.Visible)
        {
            // Change to a new item async, 
            // because during the execution of this event handler, it is not possible
            // to change to a new item.
            element.Dispatcher.BeginInvoke(new Action(() => collection.MoveCurrentToNext()), 
                System.Windows.Threading.DispatcherPriority.Input);
        }
    }
}
 
/// <summary>Handles the IsVisibleChanged event of the tab item.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see 
/// cref="System.Windows.DependencyPropertyChangedEventArgs"/> 
/// instance containing the event data.</param>
static void TabItem_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    TabItem tab = sender as TabItem;
    if (tab != null &amp;&amp; tab.IsSelected &amp;&amp; tab.Visibility != Visibility.Visible)
    {
        // Move to the next tab item.
        TabControl tabControl = tab.Parent as TabControl;
        if (tabControl != null)
        {
            if (!tabControl.Items.MoveCurrentToNext())
            {
                // Could not move to next, try previous.
                tabControl.Items.MoveCurrentToPrevious();
            }
        }
    }
}

That’s it.

Usage in WPF (do not forget to specify the xmln namespace that holds the behavior):

One last note: Since the behavior uses the TabControl.Items property, you must specify the IsSynchronizedWithCurrentItem property.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)