WPF TabControl focus behavior with invisible tabs (MVVM)

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:

/// <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):

/// <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:

/// <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:

/// <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.

Advertisements

How to instantiate derived LINQ to SQL entities

A lot has been written about how to set up (table per class) inheritance with LINQ to SQL. I use it a lot and I always use an enum as inheritance discriminator.
But now I need to instantiate new instances of derived classes in code. The user is provided a ComboBox with the available enum values and an ‘Add’ button. When the ‘Add’ button is pressed, I need an instance of the type that ‘belongs’ to the selected enum.

The obvious (and fastest) approach would be: the switch statement.

	public enum MyEnum
	{
		Rectangle = 1,
		Triangle = 2,
	}

	public MyShape CreateInstance(MyEnum requiredShape)
	{
		switch (requiredShape)
		{
			case MyEnum.Rectangle:
				return new MyRectangle();

			case MyEnum.Triangle:
				return new MyTriangle();

			default:
				throw new System.NotSupportedException(requiredShape.ToString());
		}
	}

The drawbacks of this approach are:

  • For each set of classes with subclasses you will need to write a switch statement.
  • Maintainability; when the inheritance specifications change, you also need to update this code.

It would be easier (less code, better maintainable) to have a method that will figure out the class to instantiate from the inheritance mappings of the LINQ entity. Using reflection on the InheritanceMappingAttributes is how to determine the type that belongs to a certain enum value. Indeed, this will be slower, but I will take that for granted.
Some LINQ entities might have been set up like this:

	[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.MyShape")]
	[global::System.Data.Linq.Mapping.InheritanceMappingAttribute(Code="Rectangle", Type = typeof(MyRectangle), IsDefault = true)]
	[global::System.Data.Linq.Mapping.InheritanceMappingAttribute(Code="Triangle", Type = typeof(MyTriangle))]
	public abstract partial class MyShape : INotifyPropertyChanging, INotifyPropertyChanged
	{
		[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Type", DbType="Int NOT NULL", CanBeNull=false, IsDiscriminator=true)]
		public MyEnum Type
		{
			get { ... }
			set { ... }
		}
		...
	}

	public partial class MyRectangle : MyShape
	{
		...
	}

	public partial class MyTriangle : MyShape
	{
		...
	}

Let’s create a factory(ish) class (it’s not a fully equipped object factory…) that does the reflection work and the instantiation of the appropriate class. It can be a simple static class with a static method that will do the job:

	public static class LinqEntityFactory
	{
		public static TBaseClass CreateEntity(Enum inheritanceModifier)
			where TBaseClass : class
		{
		...
		}
	}

Time to the implement the CreateEntity method. Always start with checking the input values:

			// Check the input.
			if (inheritanceModifier == null)
			{
				throw new System.ArgumentNullException("inheritanceModifier");
			}

The type of the class to instantiate is specified in one of the attributes. If the attribute is not found, fall back to the attribute that is marked as default.

			Type enumType = inheritanceModifier.GetType();
			// Get the inheritance attributes.
			MemberInfo info = typeof(TBaseClass);
			var attribs = info.GetCustomAttributes(typeof(InheritanceMappingAttribute), false).Cast();

			// Find the inheritance attribute that specifies the requested object type.
			// Instead of a string comparison, try to parse the Code value. The Code
			// of the attribute is always provided as string, representing either
			// the name of the Enum value or the (int) value of it.
			var attr = attribs.Where(a =>
				inheritanceModifier.Equals(Enum.Parse(enumType, a.Code.ToString())))
				.SingleOrDefault();
			// If the attribute was not found, find the inheritance default.
			if (attr == null)
			{
				attr = attribs.Where(a => a.IsDefault).SingleOrDefault();
			}

If the provided class is really marked with InheritanceModifierAttributes, then we now have the attribute. What is left is to create the instance.
But beware, the type may represent an abstract class. Instantiating abstract classes at runtime cause errors which can be hard to debug. Therefore, throw an exception in this case providing the value of the enum.
Also an additional check is added for the case the method is called on an object that has no InheritanceModifierAttributes at all.

			// The attribute contains the type of the object to instantiate.
			// Verify whether the required type is not abstract. Abstract types
			// can occur when the default inheritance attribute has been
			// specified with an abstract class. In that case, raise an
			// error because it can be hard to debug a runtime abstract class
			// instantiation exception.
			// By the way, there is always a default inheritance attribute,
			// however since LINQ to SQL entities inherited from object (by default)
			// this method can also be called on objects without any inheritance
			// specifier. So always check for null.
			if (attr == null || attr.Type.IsAbstract)
			{
				throw new System.NotSupportedException(inheritanceModifier.ToString());
			}
			else
			{
				// Create and return the required instance.
				return (TBaseClass)Activator.CreateInstance(attr.Type);
			}

And this is how to use the factory:

MyEnum requiredShape = MyEnum.Triangle;
MyShape shape = LinqEntityFactory.CreateEntity<MyShape>(requiredShape);
// shape is now an instance of MyTriangle.

That’s it. Thanks for reading an I hope you will find this post useful.