Introduction
Good tab orders on your forms are important. They allow experienced users to more rapidly interact with your application, and they may even be necessary to enable your application for users that cannot manipulate the mouse. You may sometimes find it desirable to set the tab order at runtime. For example, you may allow your users to customize the visibility or position of form controls, and you'd like a professional tab order even when you don't know exactly how the final form will look. Or you may not want to have to worry about maintaining the tab order for complex forms at design-time as the design changes. The TabOrderManager
class will help you easily automate the process of applying a nice tab order at runtime. And the TabSchemeProvider
component makes it simple to set up your tabbing strategies in the Windows Forms Designer.
Background
This article assumes a basic familiarity with Windows Forms programming in the .NET Framework using C# or Visual Basic .NET.
Using the code
The TabOrderManager
class is currently available in both C# and Visual Basic .NET. It's quite simple: Given a container control (probably a Form
, but possibly a GroupBox
, TabControl
, Panel
, etc.), calling the SetTabOrder
method will automatically adjust the TabIndex
properties on child controls to implement a tab order that is either primarily "across the container, then down" or "down the container, then across". By default, this strategy is inherited by child container controls. However, using the SetSchemeForControl
method, you can override the strategy for specific containers. One situation where this is useful is when you would like users to tab through a series of GroupBox
es across-first, but tab through set of enclosed TextBox
es down-first.
Here is an example invocation of the TabOrderManager
to set an across-first tabbing strategy in C#:
(new TabOrderManager(this)).SetTabOrder(TabOrderManager.TabScheme.AcrossFirst);
And in Visual Basic.NET:
Dim tom As TabOrderManager = New TabOrderManager(Me)
tom.SetTabOrder(TabOrderManager.TabScheme.AcrossFirst)
Implementation
To implement a given tab scheme strategy, we need to sort the controls and then set their tab order. The key is how the controls are sorted. If the primary scheme is "across the container, then down", our primary sorting priority is by the controls' Top
property values. If and only if the Top
properties are the same, then we fall back to sorting on the Left
property values. The converse is true for the "down the container, then across" tabbing scheme. We use the .NET Framework's built-in ability to sort collections based on a custom IComparer
implementation. Our IComparer
is called TabSchemeComparer
, and its Compare
method implements the sorting strategy described above.
If a given control is itself a container (that is, it has child controls), we need to recurse. If the scheme override functionality is being used, we need to check whether or not we need to change the tab scheme at each level of the recursion. Tab scheme overrides for individual containers are tracked using a HashTable
that lives in the TabOrderManager
that the user creates. When we recurse, we create a new TabOrderManager
and pass the overrides down. The process of creating auxiliary TabOrderManager
s is invisible to the client code. To see a tab scheme override in action, you may choose to add a down-first override to the group box inside the tab control of the demo application.
The TabSchemeProvider Component
You may also want to configure your tab schemes in the Windows Forms designer without having to write any code. The TabSchemeProvider
component is a thin wrapper around the TabOrderManager
class to allow you to do just that. The TabSchemeProvider
implements the IExtenderProvider
interface to dynamically add a TabScheme
property to your container controls. A complete discussion of IExtenderProvider
is beyond the scope of this article. Here, I will simply explain how you can take advantage of the TabSchemeProvider
functionality and discuss the interesting implementation details.
TabSchemeProvider Usage
In the included solution, the TabOrder
class library project contains both the TabOrderManager
class and the TabSchemeProvider
component. After you build this project, you can then add the TabSchemeProvider
component to your Windows Forms toolbox by right-clicking, choosing Add/Remove Items, and browsing to the TabOrder.dll assembly that you compiled. Once it's on the toolbox, you may drag and drop it onto a Windows Form. There it sits in the component tray. But if you now examine the Properties window for your Form
, GroupBox
es, Panel
s, or UserControl
s, you will see a new TabScheme
property which you can set to None
(the default), AcrossFirst
, or DownFirst
. At runtime, during the Form Load
event, the selected tab scheme for each container will propagate down through its child controls as though you had TabOrderManager.SetTabOrder
on the containers in question.
TabSchemeProvider Implementation
There are some implementation details for the TabSchemeProvider
control that are worth noting. As I stated above, the TabSchemeProvider
gets its interesting functionality from the TabOrderManager
class. Thus, getting it to work is just a matter of creating a TabOrderManager
instance for the top-level form, adding overrides for all other container controls with the TabScheme
property set, and calling TabOrderManager.SetTabOrder
in the Form Load
event (which occurs after the form-designer-generated code has positioned all of the controls in the Form
's control hierarchy).
The surprisingly difficult part is getting a reference to the top-level Form
whose Load
event we need. Components like the TabSchemeProvider
are not sited on the Form
at runtime, and therefore do not automatically have access to the control hierarchy. This is in contrast to Control
s, which have properties like Control.Parent
or Control.TopLevelControl
. For the purposes of the TabSchemeProvider
component, when the Form
itself is one of the controls whose tab order needs to be set, this is not a problem. In that case, the designer generates a line of code like the following:
this.tabSchemeProvider1.SetTabScheme(this,
SMcMaster.TabOrderManager.TabScheme.AcrossFirst);
The TabSchemeProvider
can detect that the TabScheme
of the Form
is being set and use the given Form
instance to wire up a Load
event handler. Case closed. But what happens when the Form
has the default tab scheme of TabOrderManager.TabScheme.None
? In that case, no such line of code is generated by the designer, and it is no longer clear from where the TabSchemeProvider
can obtain its Form
instance. One might think that when SetTabScheme
is called for non-Form
Controls, you could just grab the Form
reference out of the Control.TopLevelControl
property. Unfortunately, this does not work, because in general, SetTabScheme
may get called before the Control
and/or its parents have been added to the Form
, and Control.TopLevelControl
is null
in that situation.
One solution to this problem would have been to remove the DefaultValue
attribute from the extender's TabScheme
property, effectively forcing the Windows Forms Designer to generate a SetTabScheme
call for each and every supported container control including the top-level form. This isn't very satisfying. So, I worked until I came up with an alternative.
The final implementation recognizes that there are two possibilities when the TabSchemeProvider
component is notified via a call to SetTabScheme
that a container wants its tab order managed:
- The control is already a part of the
Form
's control hierarchy (which includes the case where the control is the form). Then we can directly and immediately obtain a reference to the Form
and hook its Load
event.
- The control is not already a part of the
Form
's control hierarchy. Here, we hook the ParentChanged
event for the control and for all of its ancestors. Eventually, the control or one of its ancestors will be added to the Form
, and we can hook the Form
's Load
event in the resulting ParentChanged
handler.
When we finally do discover the form reference via the handling of either of the two possibilities, we have the information we need and can short-circuit further processing of ParentChanged
events.
Finally, if you use more than one TabSchemeProvider
on a given form, the resulting tab order will depend on the order in which they process the Form Load
event. In other words, the behavior is undefined, so you should avoid this scenario.
History
- Initial release: 09/28/2004
- Added
TabSchemeProvider
component: 10/28/2004