Sometimes you won’t know in advance how many instances of a given control might be required on a form. Let’s say you’re designing a form for displaying the names of all tables in a database. It’s practically impossible to design a form that will accommodate every database users might throw at your application. Another typical example is a form for entering family related data, which includes the number of children in the family and their ages. As soon as the user enters (or changes) the number of children, you should display as many TextBox controls as there are children to collect their ages.
For these situations, it is possible to design dynamic forms, which are populated at runtime. The simplest approach is to create more controls rather than you’ll ever need and set their Visible properties to False at design time. At runtime, you can display the controls by switching their Visible properties to True. As you know already, quick-and-dirty methods are not the most efficient ones. You must still rearrange the controls on the form to make it look nice at all times. The proper method to create dynamic forms at runtime is to add controls to and remove them from your form as needed, using the techniques discussed in this section.
Just as you can create new instances of forms, you can also create new instances of any control and place them on a form. The Form object exposes the Controls collection, which contains all the controls on the form. This collection is created automatically as you place controls on the form at design time, and you can access the members of this collection from within your code. It is also possible to add new members to the collection, or remove existing members, with the Add and Remove statements accordingly.
The Form’s Controls Collection
All the controls on a form are stored in the Controls collection, which is a property of the Form object. The Controls collection exposes members for accessing and manipulating the controls at runtime, and they’re the usual members of a collection:
Add method
The Add method adds a new element to the Controls collection. In effect, it adds a new control on the current form. The Add method accepts a reference to a control as an argument and adds it to the collection. Its syntax is the following, where controlObj is an instance of a control:
Controls.Add(controlObj)
Code language: VB.NET (vbnet)
To place a new Button control on the form, declare a variable of the Button type, set its properties, and then add it to the Controls collection:
Dim bttn As New System.WinForms.Button
bttn.Text = "New Button"
bttn.Left = 100
bttn.Top = 60
bttn.Width = 80
Me.Controls.Add(bttn)
Code language: VB.NET (vbnet)
Remove method
The Remove method removes an element from the Controls collection. It accepts as an argument either the index of the control to be removed or a reference to the control to be removed (a variable of the Control type that represents one of the controls on the form). The syntax of these two forms of the Remove method is the following:
Me.Controls.Remove(index)
Me.Controls.Remove(controlObj)
Code language: VB.NET (vbnet)
Count property
This property returns the number of elements in the Controls collection. Notice that if there are container controls, the controls in the containers are not included in the count. For example, if your form contains a Panel control, the controls on the panel won’t be included in the value returned by the Count property. The Panel control, however, has its own Controls collection.
All method
This method returns all the controls on a form (or on a container control) as an array of the System.WinForms.Control type. You can iterate through the elements of this array with the usual methods exposed by the Array class.
Clear method
The Clear method removes all the elements of the Controls array and effectively clears the form.
The Controls collection is also a property of any control that can host other controls. Many of the controls that come with VB 2008, such as the Panel control, can host other controls. As you recall from our discussion of the Anchor and Dock properties, it’s customary to place controls on a panel and handle them collectively as a section of the form. They are moved along with the panel at design time, and they’re rearranged as a group at runtime. The panel belongs to the form’s Controls collection, and it provides its own Controls collection, which lets you access the controls on the panel.
The ShowControls Example Project
The ShowControls Example project (Figure 5.13) demonstrates the basic methods of the Controls array. Open the project and add any number of controls on its main form. You can place a panel to act as a container for other controls as well. Just don’t remove the button at the top of the form (the Scan Controls On This Form button), which contains the code to list all the controls.
Figure 5.13 – Accessing the controls on a form at runtime
The code behind the Scan Controls On This Form button enumerates the elements of the form’s Controls collection. The code doesn’t take into consideration containers within containers. This would require a recursive routine, which would scan for controls at any depth. The code that iterates through the form’s Controls collection and prints the names of the controls in the Output window is shown in Listing 5.7.
Listing 5.7: Iterating the Controls Collection
Private Sub Button1_Click(...) Handles Button1.Click
Dim Control As Windows.Forms.Control
For Each Control In Me.Controls
Debug.WriteLine(Control.ToString)
If Control.GetType Is GetType(System.Windows.Forms.Panel) Then
Dim nestedControl As Windows.Forms.Control
For Each nestedControl In Control.Controls
Debug.WriteLine(" " & nestedControl.ToString)
Next
End If
Next
End Sub
Code language: VB.NET (vbnet)
The form shown in Figure 5.13 produced the following output (the controls on the Panel are indented to stand out in the listing):
Panel1: System.Windows.Forms.Panel,
BorderStyle: System.Windows.Forms.BorderStyle.FixedSingle
CheckBox4: System.Windows.Forms.CheckBox, CheckState: 0
CheckBox3: System.Windows.Forms.CheckBox, CheckState: 0
HScrollBar1: System.Windows.Forms.HScrollBar,
Minimum: 0, Maximum: 100, Value: 0
CheckedListBox1: System.Windows.Forms.CheckedListBox,
Items.Count: 3, Items[0]: Item 1
TextBox2: System.Windows.Forms.TextBox,
Text: TextBox2
TextBox1: System.Windows.Forms.TextBox,
Text: TextBox1
Button4: System.Windows.Forms.Button,
Text: Button4
Code language: VB.NET (vbnet)
To find out the type of individual controls, call the GetType method. The following statement examines whether the control in the first element of the Controls collection is a TextBox:
If Me.Controls(0).GetType Is GetType(system.WinForms.TextBox) Then
MsgBox("It's a TextBox control")
End If
Code language: VB.NET (vbnet)
Notice the use of the Is operator in the preceding statement. The equals operator will cause an exception because objects can be compared only with the Is operator. (You’re comparing instances, not values.)
If you know the type’s exact name, you can use a statement like the following:
If Me.Controls(i).GetType.Name = "TextBox" Then ...
Code language: VB.NET (vbnet)
To access other properties of the control represented by an element of the Controls collection, you must first cast it to the appropriate type. If the first control of the collection is a TextBox control, use the CType() function to cast it to a TextBox variable and then request its Text property:
If Me.Controls(0).GetType Is GetType(system.WinForms.TextBox) Then
Debug.WriteLine(CType(Me.Controls(0), TextBox).Text)
End If
Code language: VB.NET (vbnet)
The If statement is necessary, unless you can be sure that the first control is a TextBox control. If you omit the If statement and attempt to convert the control to a TextBox, a runtime exception will be thrown if the object Me.Controls(0) isn’t a TextBox control.
The DynamicForm Example Project
The DynamicForm Example Project demonstrates how to handle controls at runtime from within your code (Figure 5.14), which is a simple data-entry window for a small number of data points. The user can specify at runtime the number of data points she wants to enter, and the number of TextBoxes on the form is adjusted automatically.
Figure 5.14 – The DynamicForm Example Project
The control you see at the top of the form is the NumericUpDown control. All you really need to know about this control is that it displays an integer in the range specified by its Minimum and Maximum properties and allows users to select a value. It also fires the ValueChanged event every time the user clicks one of the two arrows or types another value in its edit area. This event handler’s code adds or removes controls on the form, so that the number of text boxes (as well as the number of corresponding labels) matches the value on the control. Listing 5.8 shows the handler for the ValueChanged event of the NumericUpDown1 control. The ValueChanged event is fired when the user clicks one of the two arrows on the control or types a new value in the control’s edit area.
Listing 5.8: Adding and Removing Controls at Runtime
Private Sub NumericUpDown1_ValueChanged(...) _
Handles NumericUpDown1.ValueChanged
Dim TB As New TextBox()
Dim LBL As New Label()
Dim i, TBoxes As Integer
' Count all TextBox controls on the Form
For i = 0 To Me.Controls.Count - 1
If Me.Controls(i).GetType Is GetType(System.Windows.Forms.TextBox) Then
TBoxes = TBoxes + 1
End If
Next
' Add new controls if number of controls on the Form is less
' than the number specified with the NumericUpDown control
If TBoxes < NumericUpDown1.Value Then
TB.Left = 100
TB.Width = 120
TB.Text = ""
For i = TBoxes To CInt(NumericUpDown1.Value) - 1
TB = New TextBox()
LBL = New Label()
If NumericUpDown1.Value = 1 Then
' the first TextBox control is added to the
' Controls collection when the form is loaded
TB.Top = 20
Else
' If this is the first TextBox control added to the
' Controls collection by increasing the value of the
' NumericUpDown control, set its Top coordinate explicitly
If NumericUpDown1.Value = 2 Then
TB.Top = 20 + TB.Height + 5
Else
' all other TextBox controls on the form are placed
' 5 pixels below the previous TextBox control
TB.Top = Me.Controls(Me.Controls.Count - 2).Top + 25
End If
End If
' Set the trivial properties of the new controls
LBL.Left = 20
LBL.Width = 80
LBL.Text = "Data Point " & i
LBL.Top = TB.Top + 3
TB.Left = 100
TB.Width = 120
TB.Text = ""
' add controls to the form
Me.Controls.Add(TB)
Me.Controls.Add(LBL)
' and finally connect their GotFocus/LostFocus
' events to the appropriate handler
AddHandler TB.Enter, New System.EventHandler(AddressOf TBox_Enter)
AddHandler TB.Leave, New System.EventHandler(AddressOf TBox_Leave)
Next
Else
For i = Me.Controls.Count - 1 To Me.Controls.Count - _
2 * (TBoxes - CInt(NumericUpDown1.Value)) Step -2
Me.Controls.Remove(Controls(i))
Me.Controls.Remove(Controls(i - 1))
Next
End If
End Sub
Code language: VB.NET (vbnet)
Ignore the AddHandler statements for now; they’re discussed in the following section. First, the code counts the number of TextBoxes on the form; then it figures out whether it should add or remove elements from the Controls collection. To remove controls, the code iterates through the last n controls on the form and removes them. The number of controls to be removed is the following, where TBoxes is the total number of controls on the form minus the value specified in the NumericUpDown control:
2 * (TBoxes - NumericUpDown1.Value)
Code language: VB.NET (vbnet)
If the value entered in the NumericUpDown control is less than the number of TextBox controls on the form, the code removes the excess controls from within a loop. At each step, it removes two controls, one of them a TextBox and the other a Label control with the matching caption. (That’s why the loop variable is decreased by two.) The code also assumes that the first two controls on the form are the Button and the NumericUpDown controls. If the value entered by the user exceeds the number of TextBox controls on the form, the code adds the necessary pairs of TextBox and Label controls to the form.
To add controls, the code initializes a TextBox (TB) and a Label (LBL) variable. Then, it sets their locations and the label’s caption. The left coordinate of all labels is 20, their width is 80, and their Text property (the label’s caption) is the order of the data item. The vertical coordinate is 20 pixels for the first control, and all other controls are 3 pixels below the control on the previous row. After a new control is set up, it’s added to the Controls collection with one of the following statements:
Me.Controls.Add(TB) ' adds a TextBox control
Me.Controls.Add(LBL) ' adds a Label control
Code language: VB.NET (vbnet)
The code contains a few long lines, but it isn’t really complicated. It’s based on the assumption that except for the first few controls on the form, all others are pairs of Label and TextBox controls used for data entry. You can simplify the code a little by placing the Label and Text-Box controls on a Panel and manipulate the Panel’s Controls collection. This collection contains only the data-entry controls, and the form may contain any number of additional controls.
To use the values entered by the user on the dynamic form, we must iterate the Controls collection, extract the values in the TextBox controls, and use them. Listing 5.9 shows how the top Process Values button scans the TextBox controls on the form and performs some basic calculations with them (counting the number of data points and summing their values).
Listing 5.9: Reading the Controls on the Form
Private Sub Button1 Click(...) Handles Button1.Click
Dim TBox As TextBox
Dim Sum As Double = 0, points As Integer = 0
Dim iCtrl As Integer
For iCtrl = 0 To Me.Controls.Count - 1
If Me.Controls(iCtrl).GetType Is _
GetType(System.Windows.Forms.TextBox) Then
TBox = CType(Me.Controls(iCtrl), TextBox)
If IsNumeric(TBox.Text) Then
Sum = Sum + Val(TBox.Text)
points = points + 1
End If
End If
Next
MsgBox("The sum of the " & points.ToString & _
" data points is " & Sum.ToString)
End Sub
Code language: VB.NET (vbnet)
You can add more statements to calculate the mean and other vital statistics, or you can process the values in any other way. You can even dump all the values into an array and then use the array notation to manipulate them.
The project’s form has its AutoScroll property set to True, so that users can scroll it up and down if they specify a number of data points that exceeds the vertical dimension of the form. The two controls on the top-right side of the form, however, must remain at their location at all times.
I placed them on a Panel control and added some code to the form’s Scroll event handler, so that every time the user scrolls the form, the Panel control maintains its distance from the top and right edges of the form (otherwise, the two controls would scroll out of view). A single statement is all it takes to keep the Panel control in view at all times:
Private Sub Form1 Scroll(ByVal sender As Object, _
ByVal e As System.Windows.Forms.ScrollEventArgs) _
Handles Me.Scroll
Panel1.Top = Panel1.Top + (e.NewValue - e.OldValue)
End Sub
Code language: VB.NET (vbnet)
You should try to redesign this application and place the data-entry controls on a Panel with its AutoSize and AutoScroll properties set to True.
The second button on the form does the exact same thing as the top one, only this one uses a For Each … Next loop structure to iterate through the form’s controls.