Free!  FoxPro to WPF screen converter
   Almost free!  One day of FREE VFP to WPF evaluation of your FoxPro app ONSITE
   Free!  Read the foreword to Les' new book (available here)...
Skip Navigation Links

WinForms User Controls and SubClassed Controls

Why you might not need what you thought you needed


Les Pinter

Login or register to download source code

Introduction

News flash: The controls that come with the WinForms design surface are never supposed to be used on a form.

Here's why: The first thing your client will say when she sees your 120 forms is "can you make them pretty?", If you used the controls that appear in the Windows Forms Designer toolbox, you're going to be up late.

What you're supposed to do is design your own controls, so that a single change in each of your control classes will affect all of the labels, textboxes, radio buttons on all of your forms. In this article, we'll see how that's done in Windows Forms applications. In part II, we'll do the same thing with Windows Presentation Foundation (WPF) controls.

(Note: The source code download contains both VB and C# versions of the code shown in this article. Just register to download the code.)

My first attempt

Since I came from a world (the F-world) where all of this is done visually, my first instinct was to create a User Control. I created a WinForms project, then right-clicked on the Project and selected UserControl, as seen in Fig. 1, giving it the name UCTextBox:

Adding a User Control to the project
Fig. 1 - Adding a user control

This created a UserControl Container, into which I dropped a TextBox (Fig. 2):

Adding a TextBox to my User Control
Fig. 2 - Adding a TextBox to my User Control

I anchored the TextBox on all four corners and shrank the container to slightly larger than the textbox; it doesn't really matter, since the container doesn't show on a form at run time; but I'm a tidy freak. I then verified that AutoToolboxPopulate was set to true in the Tools, Options, Windows Forms Designer dialog, picked my new control from the top of the Toolbox (Fig. 3), dropped it on the form, and went to databind its Text Property to my BindingSource. What I saw is what you see in Fig. 4, below: No Text property.

My UserControl in the toolbox
Fig. 3 - My UserControl in the Toolbox


This is my UserControl on my form
Fig. 4 - My UserControl properties don't include Text

The reason is that usercontrols might contain several WinForms controls that expose a Text property, like a label and a textbox; so in order to say which one, you have to add your own Text property and associate it with the Text property of the correct control - even if there's only one. So I added a Property procedure to my usercontrol class, as shown below. I show it as a graphic, because the little green squiggly underneath the word Text is the problem.


Property Procedure Text

Fig 5 - Property Procedure Text

The yellow-highlighted simulated ToolTip says it all. It has to be overriden. So add the word Override:

Public Class UCTextBox

  Public Overrides Property Text() As String
    Get
      Return TextBox1.Text
    End Get
    Set(ByVal value As String)
      TextBox1.Text = value
    End Set
  End Property

End Class

However, that didn't solve my problem. I needed to add a few attributes. A little googling told me that the attributes that I needed were Bindable, Browsable and DesignerSerializationVisibility(Visible). But, as you can see in Fig. 6 below, they weren't selectable when I typed a left-carat:


The attributes I needed didn't appear!
Fig. 6 - No Bindable attribute!

After some more googling, I found out that I need an Imports statement to add System.ComponentModel. I would have eventually guessed that - in about a year! No matter; I added it, and IntelliSense came to the rescue. Here's what I ended up with:

Imports System.ComponentModel

Public Class UCTextBox

  <Bindable(True), _
   Browsable(True), _
   Category("_Pinter"), _
   DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)> _
   Public Overrides Property Text() As String
    Get
      Return TextBox1.Text
    End Get
    Set(ByVal value As String)
      TextBox1.Text = value
    End Set
  End Property

End Class

The Category tag is there to force my Text property to bubble up to the top of the properties sheet, as seen in Fig. 7:


The properties sheet with my Text property visible
Fig. 7 - The properties sheet with my Text property visible

Draining the swamp

The purpose of this exercise was to be able to open the Data Sources window and assign my new control as the default control to use for columns of type CHAR when creating a Detail screen from a SQL Table using the Data Form Assistant. The documentation says you do this by opening the Data Sources window, creating a connection to your database, selecting a table to use for the typed Dataset, then using Customize to make your control the Default control for CHAR fields. (Fig. 8). So I tried. Unfortunately, my UCTextBox didn't appear in the list of controls (Fig. 9):


Select Customize to make your usercontrol the default
Fig. 8 - Select Customize to make your usercontrol the default


No UCTextbox!
Fig. 9 - My UCTextbox isn't there!

Finally I figured out that I had to add an attribute to the UCTextBox class itself to tell the DataForm Wizard that it could be bound to a BindingSource: That required the following attribute at the top of the class declaration:

Imports System.ComponentModel

<DefaultBindingProperty("Text")> _
Public Class UCTextBox

Now I was able to see my usercontrol and to set it as the default control for databinding (Fig. 10).

UCTextbox now appears
Fig. 10 - UCTextbox now appears

Assigning UCTextboxes as the bindable control for each column
Fig. 11 - Assigning UCTextboxes as the bindable control for each column

And, after assigning my control to each column in the Customers table, I dragged the Customers table name from the Data Sources window to a form, and voilĂ , my form looked like I expected (Fig. 12)

My form using UCTextboxes

Fig. 12 - Assigning UCTextboxes as the bindable control for each column

And when I ran it, the data appeared (Fig. 13):


Form showing data from the Customers table
Fig. 13 - My form showing data from the Customers table

Hello, Houston...

But when I changed something and clicked on the little diskette to save it, it didn't save the changes! So back to Google. And sure enough, there was a problem...

The problem, it seems, is that you just can't use Text as your bindable property. You have to use something else - typically Value. And you have to tell the Data Form Wizard to bind to Value instead of Text. Here's how it ended up:

Imports System.ComponentModel

<DefaultBindingProperty("Value")> _
Public Class UCTextBox

  <Bindable(True), _
   Browsable(True), _
   Category("_Pinter"), _
   DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)> _
   Public Property Value() As String
    Get
      Return TextBox1.Text
    End Get
    Set(ByVal value As String)
      TextBox1.Text = value
    End Set
  End Property

End Class

And now for something completely different...

By now, I was pretty disappointed with User Controls. This seemed like an awful lot of work just to be able to change the color of all of my labels. So I looked around the Internet, and found out that I wasn't supposed to be using UserControls to subclass forms controls. Just subclass them! Here's how:

Right-click on your project name and add a class called SubClassedControls:


Add a class
Fig. 14 - Add a class

Change the name of the class to SCTextBox and add Inherits TextBox at the end of the Class declaration. Add another class called SCLabel with : Inherits Label at the end. And while you're at it, add Imports System.ComponentModel at the top of the file:

Imports System.ComponentModel

Public Class SCTextBox : Inherits TextBox

End Class

Public Class SCLabel : Inherits Label

End Class

Compile the project, add a new form called Form2, and make it the Startup Form in the project's Properties sheet. Now open the Toolbox with Ctrl+Alt+X. There are your SCLabel and SCTextBox classes. Drag one of each onto the form and check them out in the Properties Sheet: The Text property is there, and you didn't have to do anything. So far, so good.

Now, open the Data Sources window, click on the down-arrow beside the Customers table name, select Customize. Select the CHAR datatype and uncheck UCTextBox, then check SCTextBox and set it as the default. Change the control to use for each of the columns in Customers to SCTextBox. Finally, in the Data Sources window, make sure Details is selected from the dropdown arrow beside the Customers table name, and drag it onto the form. Press F5 to find out that this does everything that our UCTextBox did, with just a few seconds' effort. Doh.

But the original objective was to be able to change all labels and textboxes in a single place. Can we do it? Open SubClassedControls.vb and change it to this:

Imports System.ComponentModel

Public Class SCTextBox : Inherits TextBox

End Class

Public Class SCLabel : Inherits Label

  Sub New()
    ForeColor = Color.Purple
    Font = New System.Drawing.Font( _
      "Courier New", _
      10.0!, _
      System.Drawing.FontStyle.Regular, _
      System.Drawing.GraphicsUnit.Point, _
      CType(0, Byte))
  End Sub

End Class

The only problem, of course, is that the Data Form Wizard generated instances of our subclassed CSTextBox for the data bound controls; it just used orginary labels for the labels. How can we change all of the instances of System.Windows.Forms.Label to New _406___WinForms_User_Controls.SCLabel? How about Global Search and Replace in the code editor? Try it: The result (slightly remodeled) appears in Fig. 15:

Using our subclassed labels
Fig. 15 - Using our subclassed labels

Let's add a little pizazz to the textboxes. Copy the constructor New from the SCLabel class to the SCTextBox class. I changed the font size to 9 points, and removed the color Purple. I then selected SCTExtBox Events from the top left object selector dropdown, and Enter from the top right event selector. The SCTextBox_Enter prototype was added.

Customizing the textbox
Fig. 16 - Customizing the textbox

The user can now more easily see where the cursor is, since the control that currently has the focus is white on blue.

The form using subclassed controls
Fig. 17 - The form using subclassed controls

Variations on a theme

Of course, these classes can themselves be subclassed. After I found out how to set all of my labels' color in a single place, the next thing I wanted to do was to replace the WinForms masked edit control, which truly sucks, with one the worked more intuitively. I also wanted it to change colors when getting and losing focus. So can I subclass SCTextbox? You betcha! Add the following at the end of SubClassedControls.vb:

Public Class SCNumeric : Inherits SCTextBox

  Private _Decimals As Integer = 0

  Public Property Decimals() As Integer

    Get
      Return _Decimals
    End Get
    Set(ByVal value As Integer)
      If value < 0 Or value > 5 Then
        MessageBox.Show("Range is 0 to 5")
      Else
        _Decimals = value
      End If
    End Set

  End Property

  Private Sub SCNumeric_KeyDown( _
    ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) _
    Handles Me.KeyDown

    Select Case e.KeyValue
      Case Strings.Asc(".")
      Case Strings.Asc("0") To Strings.Asc("9")
      Case Keys.Back
      Case Else
        e.SuppressKeyPress = True
    End Select

  End Sub

  Private Sub SCNumeric_LostFocus( _
    ByVal sender As Object, ByVal e As System.EventArgs) _
    Handles Me.LostFocus

    Text = Strings.FormatNumber(CInt(Text), Decimals)

  End Sub

End Class

Drag an instance of this control onto your form, set its Decimals property to, say, 2, and try it. That's what I was after to begin with! Hard to believe it took this entire journey to get there. But it was worth it.

Conclusion

So what are User Controls used for? The purpose of User Controls is to gather several controls and properties together and present them as a single component. As such, they permit developers to consolidate what they've learned about a feature and share it with others. You can use GDI methods to draw objects that look exactly the way you want them to. You can combine existing controls to create new functionality. If it's a one-off feature, you might not need a user control; but if you work with other developers, consider organizing your best ideas as user controls.

A quick jog around Google finds hundreds of user controls - for example, the Microsoft Virtual Earth Control on CodePlex, a User Control that hosts a web browser and exposes a number of useful settings. There are hundreds of others. If you spend a little time looking around the Internet, you'll come to appreciate what can be done with User controls. And, you might find one that does just what you need, so that you don't have to write it yourself<g>.

In the next article, we'll go through pretty much the same exercise regarding Windows Presentation Foundation User Controls and Custom Controls. Although there will be some analogies, it's more different than similar. But it's also worth the trip. I hope you've gotten as much out of reading this article as I got out of writing it.

qqq


Copyright(C) Pinter Consulting, 2012Tel: +1 (650) 464-6924
Automated conversion between C# and VB by Visible C#/Visible VB from Tangible Software Solutions