Code Focused
Resize Textboxes Easily
Use .NET's inheritance or extensibility to improve VB's textbox layout.
- By Bill McCarthy
- 06/01/2008
TECHNOLOGY TOOLBOX: VB.NET
An interesting question crossed my desk today: "How do I make my textboxes the same height as my comboboxes?" In Windows.Forms, the default height of a textbox is 20 pixels, while the default height of a combobox is 21. This can give your forms a jagged appearance if the controls are close to each other. The problem is compounded for a single-line textbox: Adjusting the Height property seems to have no effect either at design time or at runtime.
The interesting aspect of this question is that answering it highlights many .NET features that are easy to take for granted. The first step in resolving this question is to identify the problem. In this case, the problem is that changing the Height property seems to have no effect. Prior to .NET, you had to resort to filing a bug report and waiting for a response, or you had to try various Windows API hacks to see what was going on. In .NET, you can look at the actual code. I still rely on Reflector a lot, but in Visual Studio 2008 you can also download and step through Microsoft's Windows.Forms code directly, as I mentioned in a recent What's Hot column (What's Hot, "Get Source for the Framework," April 2008).
If you use Reflector to look at the code, you need to look in the TextBox class, as well as its TextBoxBase and Control base classes. You find the Height property defined in the Control class. Looking inside the property Set reveals a call to SetBounds, which in turn calls SetBoundsCore. This is where you need to have your wits about you and not just follow the code by clicking on methods in Reflector; it's easy at this stage to end up looking at the wrong method. Although you might have traversed from the TextBox class to code in the Control class, Reflector doesn't track that, so it doesn't know to go to an Overrides method in the inheritance hierarchy. The SetBoundsCore method you want to look at is the one in TextBoxBase, not the Overridable one in the Control class. If you use Visual Studio's source code symbols instead of Reflector, you can step into the code at runtime, and you'll always be in the correct method in the inheritance chain.
You can find one of the keys to the solution inside the SetBoundsCore method:
if (textBoxFlags[autoSize] && !textBoxFlags[multiline])
height = PreferredHeight;
The code checks to see whether the autoSize flag is set when a textbox is not a multiline textbox. If the autoSize flag is set, then the PreferredHeight method returns the calculated height based on the current font. Thus, the solution is to turn AutoSize off; then you can adjust the height manually. This solution sounds so simple you're probably wondering why you need to look through the code to find it. The problem is that AutoSize doesn't appear for a textbox in the Properties window, nor does IntelliSense list it for a textbox in the code editor. To understand what's going on, you need to understand the inheritance rules.
TextBox derives from TextBoxBase, which derives from Control. Control has a Public AutoSize property. The rules of inheritance dictate that a TextBox control must have a Public AutoSize property from the Control class in its inheritance chain, and it does. Code that you type into the editor compiles successfully, even though AutoSize isn't listed:
myTextBox.AutoSize = False
The reason the AutoSize property doesn't show is because the Windows.Forms team decided to hide it by Overriding the property and applying the Browsable( False ) and EditorBrowsable( EditorBrowsableÂState.Never ) attributes to it. You can't change the rules of inheritance, but you can make it appear as if members are hidden. This technique is useful when you decide the base class exposes a member that might cause some confusion. Should you ever need to deprecate a member, these two attributes in conjunction with the Obsolete attribute allow you to hide the member, yet let it be called with a warning.
You might wonder why someone in the Windows.Forms team decided to hide AutoSize. Again, this is where the source code is far more useful than Reflector because the source code includes comments. A note on the AutoSize property says:
/// Note: this works differently than other Controls' Auto-
Size, so we're hiding it to avoid confusion.
And there you have it: The source code explains not only the cause of the behavior you see, but the reason for it. All that's left is to find a solution that enables you to achieve the desired design-time experience of being able to change the height. Obviously, if the Windows.Forms team used inheritance to hide the AutoSize property, you can use inheritance to remove that hiding, unless they marked the property as NotOverridable. The good news is the Windows.Forms team didn't seal the class or mark AutoSize as NonOveridable, so you can quickly and easily create your own TextBox class that derives from TextBox:
Imports System.ComponentModel
Public Class SizeableTextBox
Inherits TextBox
<Browsable(True)> _
<EditorBrowsable
(EditorBrowsableState.Always)> _
<DefaultValue(True)> _
<DesignerSerializationVisibility
(DesignerSerializationVisibility.Visible)> _
Public Overrides Property AutoSize() As Boolean
Get
Return MyBase.AutoSize
End Get
Set(ByVal value As Boolean)
MyBase.AutoSize = value
End Set
End Property
End Class
Inheritance makes it easy to modify an existing control, and obviously you can add other functionality, as well. Using inheritance provides a solution, but it means you have to replace every textbox with your custom TextBox class. Windows.Forms provides an alternative approach -- extender providers -- that allows you to extend existing controls rather than having to replace them.
Extender providers are components that make it appear that another control or component has a given property at design time. A common example of an extender provider is the tooltip component. If you drag a tooltip component onto your form or its component tray, all the controls on the form get a "ToolTip on ToolTip1" property.
To create an extender provider component, start with a new class and inherit System.ComponentModel.Component. The base class on Component provides the necessary interfaces needed to support the design time aspects of dragging a component onto a form. To make your component an extender, you need to Implement IExtenderProvider, which has only one member; the CanExtend method. The CanExtend method should return True for any type of control you want to be able to extend. Typically, you filter for the type of control or component you want to extend:
Public Function CanExtend(ByVal extendee As Object) _
As Boolean Implements IExtenderProvider.CanExtend
If TypeOf extendee Is TextBox Then Return True
End Function
You need to define a matching Get function and Set method for each property you want your extender to provide. For example, if the desired property name was Blink, you would create a GetBlink function and a SetBlink method. The Get function has one parameter, which is the object being extended; this function returns the property value. The Set method has two parameters: The first is the object being extended, and the second is the value for the property. Your extender can provide as many different named properties as you like by repeating the Get and Set pattern with each property name.
The final requirement is that you must add one ProvideProperty attribute to the class for each property your extender provides. This code creates an extender provider that allows AutoSize to be changed at design time on TextBox controls:
Imports System
Imports System.ComponentModel
Imports System.Windows.Forms
<ProvideProperty("AutoSize", GetType(TextBox))> _
Public Class TextBoxAutoSizeEnabler
Inherits System.ComponentModel.Component
Implements IExtenderProvider
Public Sub New(ByVal Container As IContainer)
Container.Add(Me)
End Sub
Public Function CanExtend(ByVal extendee As Object) _
As Boolean Implements IExtenderProvider.CanExtend
If TypeOf extendee Is TextBox Then Return True
End Function
Public Function GetAutoSize(ByVal tbx As TextBox) _
As Boolean
Return tbx.AutoSize
End Function
Public Sub SetAutoSize(ByVal tbx As TextBox, _
ByVal value As Boolean)
tbx.AutoSize = value
End Sub
End Class
To use this extender provider, add the code to your project and build it. Next, drag the TextBoxAutoSizeEnabler component from the toolbox onto the relevant forms. You'll see an "AutoSize on TextBoxAutoSizeEnabler1" property in the properties window for any of the form's textboxes.
Sure, these solutions correct what is arguably a design flaw (the hidden AutoSize property). But it also illustrates how you can use debugging symbols that provide the source code with comments to make locating the origin of the code problem incredibly easy. The comments in the code provide an insight to the reasoning behind design decisions, and .NET's rich inheritance and extensibility models make providing solutions to this problem incredibly fast and easy. Instead of having to second guess or wait for a hotfix or product support knowledge base, .NET lets you locate and fix such issues yourself in minutes, not days.
About the Author
Bill McCarthy is an independent consultant based in Australia and is one of the foremost .NET language experts specializing in Visual Basic. He has been a Microsoft MVP for VB for the last nine years and sat in on internal development reviews with the Visual Basic team for the last five years where he helped to steer the language’s future direction. These days he writes his thoughts about language direction on his blog at http://msmvps.com/bill.