All posts tagged Tabbing

I think the title says it pretty well: we want the DatePicker control to not be stupid and allow tabbing to the calendar button so we can meet mouse-less compliance standards.

Q: Why didn’t Microsoft do this to begin with?
A: *shrugs* But, we’re going to fix it.

The code:


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

/// <summary>
/// Extended DatePicker.
/// </summary>
public class ExtendedDatePicker : DatePicker
{
    #region Members

    private bool _tabInvoked;

    #endregion

    #region Constructors

    /// <summary>
    /// Initializes a new instance of the <see cref="ExtendedDatePicker"/> class.
    /// </summary>
    public ExtendedDatePicker()
    {
        _tabInvoked = false;
        this.PreviewKeyDown += DatePickerPreviewKeyDown;
        this.CalendarClosed += DatePickerCalendarClosed;

        // Allow to be a tab target
        IsTabStop = true;
    }

    #endregion

    #region Events

    /// <summary>
    /// Dates the picker preview key down.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="KeyEventArgs"/> instance containing the event data.
    /// </param>
    private void DatePickerPreviewKeyDown(object sender, KeyEventArgs e)
    {
        var datePicker = sender as DatePicker;
        if (datePicker == null || e.Key != Key.Tab) return;

        // Mark that "tab" was pressed
        _tabInvoked = true;

        // Reverse drop down opened state
        datePicker.IsDropDownOpen = !datePicker.IsDropDownOpen;
    }

    /// <summary>
    /// Dates the picker calendar closed.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.
    /// </param>
    private void DatePickerCalendarClosed(object sender, RoutedEventArgs e)
    {
        if (!_tabInvoked) return;

        // Reset marker
        _tabInvoked = false;

        // Go to next control in sequence
        var element = Keyboard.FocusedElement as UIElement;
        if (element == null) return;
        element.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
    }

    #endregion
}

Here is what it does in a nutshell:

  • Makes this control a tab stop (it’s not by default)
  • Binds to the PreviewKeyDown event, which catches when Tab is pressed. It then sets a marker, telling the control that the calendar was opened via keyboard and sets IsDropDownOpen to true.
  • Binds to the CalendarClosed event, which (if Tab was invoked) resets the marker and sets the focus to the next control in the view.

It’s not overly complicated, but it solves an annoyance.

Until next time…

One of the issues I have seen around the web is in regard to TabControls and missing content. The “missing” content in this case is the content of tab items within a TabControl that are not in focus. By design, a TabControl will only load the content of a TabItem once it has been brought into focus. This is not bad design if you think about it for a moment.

Q: Shouldn’t we just load into memory what we need at the time we need it to minimize our footprint?
A: Yes, definitely. This is good application design.

But, what about the case where I need information from other tabs that have not yet been brought into focus? For example, what if I have a GridView in another tab and I want to show the row count right when the control is loaded? Well, the answer to that is pre-loading your tab items. And since the TabControl does not have this behavior built-in by default, we will have to do it ourselves.

Here is one way I saw on the web that I wanted to bring to your attention:


for (var i = 0; i < myTabControl.Items.Count; i++)
{
	myTabControl.SelectedIndex = i;
	myTabControl.UpdateLayout();
}

Well, this looks pretty good and it has very little code. This should work, right? No, not in all cases. The reason is that this will cycle through the tabs as fast as the UpdateLayout() call completes on each tab. Because this implementation will not necessarily wait until each tab is done loading, we cannot guarantee that the content of each tab has finished loading into the Visual Tree. What we need is a solution that waits until each tab item has completely loaded its content before we move to the next tab.

So, let’s state our list of objectives:

  • Objective 1: Each tab item should wait to complete loading before the next tab item is selected.
  • Objective 2: We should hide the Tab Control from view until it is done pre-loading.
  • Objective 3: We should return to the first tab in the sequence at completion.

The approach:

In order to effectively meet the first objective, we should chain the loading of each tab through recursion.

So, here are our 2 methods:


/// <summary>
/// Preloads tab items of a tab control in sequence.
/// </summary>
/// <param name="tabControl">The tab control.</param>
public static void PreloadTabs(TabControl tabControl)
{
    // Evaluate
    if (tabControl.Items != null)
    {
        // The first tab is already loaded
        // so, we will start from the second tab.
        if (tabControl.Items.Count > 1)
        {
            // Hide tabs
            tabControl.Opacity = 0.0;

            // Last action
            Action onComplete = () =>
            {
                // Set index to first tab
                tabControl.SelectedIndex = 0;

                // Show tabs
                tabControl.Opacity = 1.0;
            };

            // Second tab
            var firstTab = (tabControl.Items[1] as TabItem);
            if (firstTab != null)
            {
                PreloadTab(tabControl, firstTab, onComplete);
            }
        }
    }
}

/// <summary>
/// Preloads an individual tab item.
/// </summary>
/// <param name="tabControl">The tab control.</param>
/// <param name="tabItem">The tab item.</param>
/// <param name="onComplete">The onComplete action.</param>
private static void PreloadTab(TabControl tabControl, TabItem tabItem, Action onComplete = null)
{
    // On update complete
    tabItem.Loaded += delegate
    {
    	// Update if not the last tab
    	if (tabItem != tabControl.Items.Last())
    	{
    	    // Get next tab
            var nextIndex = tabControl.Items.IndexOf(tabItem) + 1;
            var nextTabItem = tabControl.Items[nextIndex] as TabItem;

            // Preload
            if (nextTabItem != null)
            {
                PreloadTab(tabControl, nextTabItem, onComplete);
            }
        }

        else
        {
            if (onComplete != null)
            {
                onComplete();
            }
        }
    };

    // Set current tab context
    tabControl.SelectedItem = tabItem;
}

So what’s going on here?

  1. PreloadTabs acts as our public method where we pass the TabControl.
  2. The opacity of the TabControl is set to 0 (Objective 2). We do not change the visibility in this case because the UI will not properly update if Visibility is Collapsed.
  3. PreloadTabs will make sure it has tab items and then call our private method PreloadTab for the second tab in the collection (since the first tab would already be loaded by the instantiation of the TabControl).
  4. PreloadTab evaluates the second tab item in the collection by setting the TabControl.SelectedItem property to the current tab.
  5. Once the tab item has completed loading, it will be evaluated. (Objective 1)
  6. If it is not the last tab item in the collection, the next tab item is retrieved and passed to the PreloadTab method.
  7. Note: This behavior will repeat until the last item in the tab item collection has been reached.
  8. Once the last item has been reached, the onComplete action is called.
  9. When the onComplete action is called, we set the SelectedIndex of the TabControl back to 0 (Objective 2), and then restore the opacity of the TabControl back to 1 (Objective 3).

And just like that we have a tab pre-loader for our TabControl.

That’s all for now. Happy coding.