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 is 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:
public class TabControlBehavior
{
#region Property FocusFirstVisibleTab
public static readonly DependencyProperty FocusFirstVisibleTabProperty =
DependencyProperty.RegisterAttached("FocusFirstVisibleTab", typeof(bool),
typeof(TabControlBehavior),
new FrameworkPropertyMetadata(OnFocusFirstVisibleTabPropertyChanged));
public static bool GetFocusFirstVisibleTab(TabControl element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (bool)element.GetValue(FocusFirstVisibleTabProperty);
}
public static void SetFocusFirstVisibleTab(TabControl element, bool value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(FocusFirstVisibleTabProperty, value);
}
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):
private static void OnFocusFirstVisibleTabPropertyChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var tabControl = d as TabControl;
if (tabControl != null)
{
if ((bool)e.NewValue)
{
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
{
tabControl.Items.CurrentChanged -= new EventHandler(TabControl_Items_CurrentChanged);
var collection = tabControl.Items as INotifyCollectionChanged;
if (collection != null)
{
collection.CollectionChanged -=
new NotifyCollectionChangedEventHandler(TabControl_Items_CollectionChanged);
}
foreach (var item in tabControl.Items)
{
TabItem tab = item as TabItem;
if (tab != null)
{
tab.IsVisibleChanged -=
new DependencyPropertyChangedEventHandler(TabItem_IsVisibleChanged);
}
}
}
}
}
static void TabControl_Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
throw new System.NotImplementedException("TabControlBehavior");
}
static void TabControl_Items_CurrentChanged(object sender, EventArgs e)
{
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:
static void TabControl_Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var collection = sender as ItemCollection;
if (collection != null)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace:
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
TabItem tab = item as TabItem;
if (tab != null)
{
tab.IsVisibleChanged +=
new DependencyPropertyChangedEventHandler(TabItem_IsVisibleChanged);
}
}
}
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:
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;
}
if (collection.Count > 0 && collection.CurrentItem == null)
{
collection.MoveCurrentToFirst();
}
}
}
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
:
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 && element.Visibility != Visibility.Visible)
{
element.Dispatcher.BeginInvoke(new Action(() => collection.MoveCurrentToNext()),
System.Windows.Threading.DispatcherPriority.Input);
}
}
}
static void TabItem_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
TabItem tab = sender as TabItem;
if (tab != null && tab.IsSelected && tab.Visibility != Visibility.Visible)
{
TabControl tabControl = tab.Parent as TabControl;
if (tabControl != null)
{
if (!tabControl.Items.MoveCurrentToNext())
{
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.