VB.NET: What Happened to Control Arrays

How to Handle Collections of Controls in VB.NET

The omission of control arrays from VB.NET is a challenge for those teaching about arrays.

  • No longer is it possible to simply copy a control, such as a textbox, and then paste it (once or several times) to create a control array.
  • The VB.NET code for creating a structure similar to a control array has been, in all the books on VB.NET that I have bought and online, much longer and much more complex. It lacks the simplicity of coding a control array that is found in VB6.

    If you reference the VB6 compatibility library, there are objects in there that act pretty much like control arrays. To see what I mean, simply use the VB.NET upgrade wizard with a program that contains a control array. The code is ugly again, but it works. The bad news is that Microsoft will not guarantee that the compatibility components will continue to be supported, and you're not supposed to use them.

    The VB.NET code to create and use "control arrays" is much longer and much more complex.

    According to Microsoft, to do something even close to what you can do in VB 6 requires the creation a "simple component that duplicates control array functionality."

    You need both a new class and a hosting form to illustrate this. The class actually creates and destroys new labels. The complete class code is as follows:

    Public Class LabelArray
        Inherits System.Collections.CollectionBase
        Private ReadOnly HostForm As _
        System.Windows.Forms.Form
        Public Function AddNewLabel() _
        As System.Windows.Forms.Label
            ' Create a new instance of the Label class.
            Dim aLabel As New System.Windows.Forms.Label
            ' Add the Label to the collection's
        ' internal list.
            Me.List.Add(aLabel)
            ' Add the Label to the Controls collection   
            ' of the Form referenced by the HostForm field.
            HostForm.Controls.Add(aLabel)
            ' Set intial properties for the Label object.
            aLabel.Top = Count * 25
            aLabel.Width = 50
            aLabel.Left = 140
            aLabel.Tag = Me.Count
            aLabel.Text = "Label " & Me.Count.ToString
            Return aLabel
        End Function
        Public Sub New( _
        ByVal host As System.Windows.Forms.Form)
            HostForm = host
            Me.AddNewLabel()
        End Sub
        Default Public ReadOnly Property _
            Item(ByVal Index As Integer) As _
            System.Windows.Forms.Label
            Get
                Return CType(Me.List.Item(Index), _
            System.Windows.Forms.Label)
            End Get
        End Property
        Public Sub Remove()
            ' Check to be sure there is a Label to remove.
            If Me.Count > 0 Then
                ' Remove the last Label added to the array 
                ' from the host form controls collection. 
            ' Note the use of the default property in 
                ' accessing the array.
                HostForm.Controls.Remove(Me(Me.Count - 1))
                Me.List.RemoveAt(Me.Count - 1)
            End If
        End Sub
    End Class

     

    To illustrate how this class code would be used, you could create a Form that calls it. You would have to use the code shown below in the form:

    Public Class Form1
        Inherits System.Windows.Forms.Form
    
    #Region " Windows Form Designer generated code "
    
    ' Also you must add the statement:
    '   MyControlArray = New LabelArray(Me)
    ' after the InitializeComponent() call in the 
    ' hidden Region code.
    
        ' Declare a new ButtonArray object.
        Dim MyControlArray As LabelArray
    
        Private Sub btnLabelAdd_Click( _
    	ByVal sender As System.Object, _
    	ByVal e As System.EventArgs) _
    	Handles btnLabelAdd.Click
            ' Call the AddNewLabel method 
            ' of MyControlArray.
            MyControlArray.AddNewLabel()
            ' Change the BackColor property 
            ' of the Button 0. 
            MyControlArray(0).BackColor = _
    		System.Drawing.Color.Red
        End Sub
    
        Private Sub btnLabelRemove_Click( _
    	ByVal sender As System.Object, _
    	ByVal e As System.EventArgs) _
    	Handles btnLabelRemove.Click
            ' Call the Remove method of MyControlArray.
            MyControlArray.Remove()
        End Sub
    End Class

    First, this doesn't even do the job at Design Time like we used to do it in VB 6! And second, they aren't in an array, they are in a VB.NET Collection - a much different thing than an array.

    The reason VB.NET doesn't support the VB 6 "control array" is that there is no such thing as a "control" "array" (note the change of quotation marks). VB 6 creates a collection behind-the-scenes and makes it appear as an array to the developer. But it's not an array and you have little control over it beyond the functions provided through the IDE.

    VB.NET, on the other hand, calls it what it is: a collection of objects. And they hand the keys to the kingdom to the developer by creating the whole thing right out in the open.

    As an example of the kind of advantages this gives the developer, in VB 6 the controls had to be of the same type, and they had to have the same name. Since these are just objects in VB.NET, you can make them different types and give them different names and still manage them in the same collection of objects.

    In this example, the same Click event handles two buttons and a checkbox and displays which one was clicked. Do that in one line of code with VB 6!

    Private Sub MixedControls_Click( _
        ByVal sender As System.Object, _
        ByVal e As System.EventArgs) _
        Handles Button1.Click, _
                Button2.Click, _
                CheckBox1.Click
        ' The statement below has to be one long statement!


        ' It's on four lines here to keep it narrow
        ' enough to fit on a web page
        Label2.Text = 
        Microsoft.VisualBasic.Right(sender.GetType.ToString, 
        Len(sender.GetType.ToString) - 
        (InStr(sender.GetType.ToString, "Forms") + 5))
    End Sub

    The substring calculation is kind of complex, but it isn't really what we're talking about here. You could do anything in the Click event. You could, for example, use the Type of the control in an If statement to do different things for different controls.

    Frank's Computing Studies Group Feedback on Arrays

    Frank's Study Group provided an example with a form that has 4 labels and 2 buttons. Button 1 clears the labels and Button 2 fills them. It's a good idea to read Frank's original question again and notice that the example he used was a loop that is used to clear the Caption property of an array of Label components.

    Here's the VB.NET equivalent of that VB 6 code. This code does what Frank originally asked for!

    Public Class Form1
          Inherits System.Windows.Forms.Form
      
      #Region " Windows Form Designer generated code "
      
              Dim LabelArray(4) As Label
              'declare an array of labels
          Private Sub Form1_Load( _
              ByVal sender As System.Object, _
              ByVal e As System.EventArgs) _
              Handles MyBase.Load
              SetControlArray()
          End Sub
      
          Sub SetControlArray()
              LabelArray(1) = Label1
              LabelArray(2) = Label2
              LabelArray(3) = Label3
              LabelArray(4) = Label4
          End Sub
      
          Private Sub Button1_Click( _
              ByVal sender As System.Object, _
              ByVal e As System.EventArgs) _
              Handles Button1.Click
              'Button 1 Clear Array
              Dim a As Integer
              For a = 1 To 4
                  LabelArray(a).Text = ""
              Next
          End Sub
      
          Private Sub Button2_Click( _
              ByVal sender As System.Object, _
              ByVal e As System.EventArgs) _
              Handles Button2.Click
              'Button 2 Fill  Array
              Dim a As Integer
              For a = 1 To 4
                  LabelArray(a).Text = _
                  "Control Array " & CStr(a)
              Next
          End Sub
      End Class

    If you experiment with this code, you will discover that in addition to setting properties of the Labels, you can also call methods. So why did I (and Microsoft) go to all the trouble to build the "Ugly" code in Part I of the article?

    I have to disagree that it's really a "Control Array" in the classic VB sense. The VB 6 Control Array is a supported part of the VB 6 syntax, not just a technique. In fact, maybe the way to describe this example is that it is an array of controls, not a Control Array.

    In Part I, I complained that the Microsoft example ONLY worked at run time and not design time. You can add and delete controls from a form dynamically, but the whole thing has to be implemented in code.

    You can't drag and drop controls to create them like you can in VB 6. This example works mainly at design time and not at run time. You can't add and delete controls dynamically at run time. In a way, it's the complete opposite of the Part I example.

    The classic VB 6 control array example is the same one that is implemented in the VB .NET code. Here in VB 6 code (this is taken from Mezick & Hillier, Visual Basic 6 Certification Exam Guide, p 206 - slightly modified, since the example in the book results in controls that can't be seen):

    Dim MyTextBox as VB.TextBox
    Static intNumber as Integer
    intNumber = intNumber + 1
    
    Set MyTextBox = _
    	Me.Controls.Add("VB.TextBox", _
    	"Text" & intNumber) 
    MyTextBox.Text = MyTextBox.Name 
    MyTextBox.Visible = True 
    MyTextBox.Left = _
    (intNumber - 1) * 1200

    But as Microsoft (and I) agree, VB 6 control arrays aren't possible in VB.NET. So the best you can do is duplicate the functionality. My article duplicated the functionality found in the Mezick & Hillier example. The Study Group code duplicates the functionality of being able to set properties and call methods.

    So the bottom line is that it really depends on what you want to do. VB.NET doesn't have the whole thing wrapped up as part of the language -- Yet -- but ultimately it's far more flexible.

    John Fannon's Take on Control Arrays

    John wrote:​ I needed control arrays because I wanted to put a simple table of numbers on a form at run time. I didn't want the nausea of placing them all individually and I wanted to use VB.NET. Microsoft offers a very detailed solution to a simple problem, but it's a very large sledgehammer to crack a very small nut. After some experimentation, I eventually hit upon a solution. Here's how I did it.

    The About Visual Basic example above shows how you can create a TextBox on a Form by creating an instance of the object, setting properties, and adding it to the Controls collection that is part of the Form object.

    Dim txtDataShow As New TextBox
    txtDataShow.Height = 19
    txtDataShow.Width = 80
    txtDataShow.Location = New Point(X, Y)
    Me.Controls.Add(txtDataShow)
    Although the Microsoft solution creates a Class, I reasoned that it would be possible to wrap all this in a subroutine instead. Every time you call this subroutine you create a new instance of the textbox on the form. Here's the complete code:

    Public Class Form1
        Inherits System.Windows.Forms.Form

    #Region " Windows Form Designer generated code "

        Private Sub BtnStart_Click( _
            ByVal sender As System.Object, _
            ByVal e As System.EventArgs) _
            Handles btnStart.Click

            Dim I As Integer
            Dim sData As String
            For I = 1 To 5
                sData = CStr(I)
                Call AddDataShow(sData, I)
            Next
        End Sub
        Sub AddDataShow( _
            ByVal sText As String, _
            ByVal I As Integer)

            Dim txtDataShow As New TextBox
            Dim UserLft, UserTop As Integer
            Dim X, Y As Integer
            UserLft = 20
            UserTop = 20
            txtDataShow.Height = 19
            txtDataShow.Width = 25
            txtDataShow.TextAlign = _
                HorizontalAlignment.Center
            txtDataShow.BorderStyle = _
                BorderStyle.FixedSingle
            txtDataShow.Text = sText
            X = UserLft
            Y = UserTop + (I - 1) * txtDataShow.Height
            txtDataShow.Location = New Point(X, Y)
            Me.Controls.Add(txtDataShow)
        End Sub
    End Class
    Very good point, John. This is certainly a lot more simple than the Microsoft code ... so I wonder why they insisted on doing it that way?

    To begin our investigation, let's try changing one of the property assignments in the code. Let's change

    txtDataShow.Height = 19
    to

    txtDataShow.Height = 100
    just to make sure that there is a noticeable difference.

    When we run the code again, we get ... Whaaaat??? ... the same thing. No change at all. In fact, you can display the value with a statement like MsgBox (txtDataShow.Height) and you still get 20 as the value of the property no matter what you assign to it. Why does that happen?

    The answer is that we're not deriving our own Class to create the objects, we're just adding things to another Class so we have to follow the rules of the other class. And those rules state that you can't change the Height property. (Wellllll ... you can. If you change the Multiline property to True, then you can change the Height.)

    Why VB.NET goes ahead and executes the code without even a whimper that there might be something wrong when, in fact, it totally disregards your statement is a whole 'nother gripe. I might suggest at least a warning in the compile, however. (Hint! Hint! Hint! Is Microsoft listening?)

    The example from Part I inherits from another Class, and this makes the properties available to the code in the inheriting Class. Changing the Height property to 100 in this example gives us the expected results. (Again ... one disclaimer: When a new instance of a large Label component is created, it covers up the old one. To actually see the new Label components, you have to add the method call aLabel.BringToFront().)

    This simple example shows that, although we CAN simply add objects to another Class (and sometimes this is the right thing to do), programming control over the objects requires that we derive them in a Class and the most organized way (dare I say, "the .NET way" ??) is to create properties and methods in the new derived Class to change things. John remained unconvinced at first. He said that his new approach suits his purpose even though there are limitations from not being "COO" (Correctly Object Oriented). More recently, however, John wrote,

    " ... after writing a set of 5 textboxes at runtime, I wanted to update the data in a subsequent part of the program - but nothing changed - the original data was still there.

    I found that I could get round the problem by writing code to take off the old boxes and putting them back again with new data. A better way to do it would be to use Me.Refresh. But this problem has drawn my attention for the need to supply a method to subtract the textboxes as well as add them."

    John's code used a global variable to keep track of how many controls had been added to the form so a method ...

    Private Sub Form1_Load( _
       ByVal sender As System.Object, _
       ByVal e As System.EventArgs) _
       Handles MyBase.Load
       CntlCnt0 = Me.Controls.Count
    End Sub

    Then the "last" control could be removed ...

    N = Me.Controls.Count - 1
    Me.Controls.RemoveAt(N)
    John noted that, "maybe this is a bit clumsy."

    It's the way Microsoft keeps track of objects in COM AND in their "ugly" example code above.

    I've now returned to the problem of dynamically creating controls on a form at run time and I have been looking again at the 'What Happened to Control Arrays' articles.

    I have created the classes and can now place the controls onto the form in the way I want them to be.

    John demonstrated how to control the placement of controls in a group box using the new classes he has started using. Maybe Microsoft had it right in their "ugly" solution after all!