Automatic Runtime Tab Order Management for Windows Forms

 

 

This article presents the TabOrderManager, which is a class that automatically adjusts the tab order on a Windows form based on different high-level schemes.

 

Automatic Runtime Tab Order Management for Windows Forms

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 GroupBoxes across-first, but tab through set of enclosed TextBoxes down-first.

Here is an example invocation of the TabOrderManager to set an across-first tabbing strategy in C#:

Collapse | Copy Code
// In constructor after InitializeComponent (or whatever other // code might set controls' TabIndex properties). (new TabOrderManager(this)).SetTabOrder(TabOrderManager.TabScheme.AcrossFirst);

And in Visual Basic.NET:

Collapse | Copy Code
'// In constructor after InitializeComponent (or whatever '// other code might set controls' TabIndex properties). 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 TabOrderManagers 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, GroupBoxes, Panels, or UserControls, 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 Controls, 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:

Collapse | Copy Code
// // DemoForm // 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:

  1. 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.
  2. 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

你可能感兴趣的:(windows)