Introduction

Every need to get all controls on a form or in a specific container such as a form (only, no children containers), GroupBox, Panel, FlowLayoutPanel etc.? There are several methods to obtain this information, in this article one efficient language extension is used to obtain an Enumerable of all controls or for a specific type of control.

Requires

Framework 4.5 or higher (required for Yield statement used in the extensions)
Microsoft Visual Studio 2015 or higher 

Primitive control iterator

The following method may be considered more than needed for a simple task, the following code can be placed in an event such as a button Click event to iterate all controls on a form. For anything more than simply iterating controls contained on a form the extensions which follow will get the job done no matter the requirements.

Dim ctrl As Control = Me.GetNextControl(Me, True)
Do Until ctrl Is Nothing
    If TypeOf ctrl Is Button AndAlso ctrl.Name <> "cmdClose" Then
        ctrl.Enabled = False
    End If
    ctrl = Me.GetNextControl(ctrl, True)
Loop

Usages for extension methods

Using this core language extension a developer may iterate controls on a form to serialize and deserialize controls to a file, XML, JSON or binary for remembering information stored in these controls or to simply remember one or more properties of a control(s). Note that controls can be synchronized with My.Settings by selecting a control, select (ApplicationSettings) and bind a property to the project settings. See the following Microsoft documentation.

There are/will be times when conventional binding to project settings will not provide the flexibility required for all possible operations such as remembering multiple settings of control or setting up special requirements a developer will have.

Dynamically created controls example

An application requirement is to allow users to create dynamic controls, save and restored.  The following example focuses on one type of control, a button. Call CreateSingleButton will place a single button on a FlowLayoutPanel properly positioned with a Click event.

Imports System.IO
 
Namespace Classes
    Public Class ButtonCreate
        ''' <summary>
        ''' Parent control where button controls will be placed
        ''' </summary>
        Public Property ParentControl() As Control
        Public Property ButtonPreFix() As String = "cmd"
 
        Protected BaseHeight As Integer = 10
        Protected ButtonWidth As Integer = 150
        Private _indexer As Integer = 0
 
        ''' <summary>
        ''' Create a single button with caption and setup an action to open a file
        ''' </summary>
        ''' <param name="pCaption">Text to show</param>
        ''' <param name="pFileName">Existing file to open</param>
        Public Sub CreateSingleButton(pCaption As String, pFileName As String)
 
            _indexer += 1
 
            Dim b = New Button With {
                    .Name = $"{ButtonPreFix}Generated{_indexer}",
                    .Text = pCaption,
                    .Width = ButtonWidth,
                    .Location = New Point(25, BaseHeight),
                    .Parent = ParentControl,
                    .Tag = pFileName
                    }
 
            AddHandler b.Click, Sub(s As Object, e As EventArgs)
                                    Dim buttonName = DirectCast(DirectCast(s, Button).Tag, String)
                                    If File.Exists(buttonName) Then
                                        Process.Start(DirectCast(DirectCast(s, Button).Tag, String))
                                    Else
                                        MessageBox.Show($"{buttonName} not found")
                                    End If
 
                                End Sub
 
            ParentControl.Controls.Add(b)
            BaseHeight += 30
 
        End Sub
    End Class
End Namespace

In a form, the class ButtonCreate is setup as a private form level variable so that controls can be positioned properly using fields in ButonCreate class. A button provides ButtonList language extension to get all buttons on the form which is chained to with another language extension ControlNames to get the button names. What is not shown is using buttons returned to use for storage. JoinedBy is simply an extension to perform a Join using String.Join.

Note several Import statements, the extension methods (and this is true for all that follow) are in a separate class project ready to use in a developer's project by simply including the class library in their project.

Imports System.IO
Imports DescendantsLibrary
Imports ExampleDynamicButtons.Classes
Imports LanguageExtensions
 
Public Class Form1
    Private Creator As ButtonCreate
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Creator = New ButtonCreate() With {.ParentControl = FlowLayoutPanel1}
 
        Dim files = Directory.
                GetFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory), "*.txt")
 
        For Each file As String In files
            Creator.CreateSingleButton(Path.GetFileNameWithoutExtension(file), file)
        Next
    End Sub
 
    Private Sub DescendantsButton_Click(sender As Object, e As EventArgs) _
        Handles DescendantsButton.Click
 
        MessageBox.Show(FlowLayoutPanel1.ButtonList.ControlNames().JoinedBy(Environment.NewLine))
    End Sub
End Class

Get controls on any container

To get controls on any container the following extension methods are provided.



Each of these extension methods is based on the following core extension method. With that for some developers, the extension method below may be enough.

Public Module BaseExtensions
    ''' <summary>
    ''' Provides access to all controls on a form including
    ''' controls within containers e.g. panel and group-box etc.
    ''' </summary>
    ''' <typeparam name="T"></typeparam>
    ''' <param name="control"></param>
    ''' <returns></returns>
    <Runtime.CompilerServices.Extension>
    Public Iterator Function Descendants(Of T As Class)(control As Control) As IEnumerable(Of T)
        For Each child As Control In control.Controls
 
            Dim currentChild = TryCast(child, T)
            If currentChild IsNot Nothing Then
                Yield currentChild
            End If
 
            If child.HasChildren Then
                For Each descendant As T In child.Descendants(Of T)()
                    Yield descendant
                Next
            End If
        Next
 
    End Function
End Module

To get all TextBox controls on a form

TextBoxList()

There is a slight issue to obtain controls on the form canvas with the base extension, it will pick up controls in child containers such as GroupBox, Panels, etc. Rather than have convoluted extension method to filter out children using a where the condition is all that is needed to exclude children controls by a condition that a control is the same parent as the caller (TextBoxList called sole is the same as Me.TextBoxList so Me is the parent). In the following example, this is done along with ordering controls by their name.

Dim orderedByNameOnFormCanvas As String =
        TextBoxList().
        OrderBy(Function(c) c.Name).
        Where(Function(c) c.Parent Is Me).
        ControlNames.
        JoinedBy(Environment.NewLine)

In the screenshot below there are two TextBox controls directly on the form while the other TextBox controls are contained in a GroupBox



To get all TextBox controls in a container such as a GroupBox or Panel.

GroupBox1.TextBoxList()

Sample goes for other controls too, even custom controls that inherit from standard controls while if there are custom controls that don't inher

Adapting to non list items

Although list are the focal point for these extension methods a developer could write an extension method that returns one item. A common requirement is to get a checked item such as a RadioButton or CheckBox from a container. 

In the following code sample a extension method RadioButtonList returns all RadioButton controls on a form (similarly someGroupBox.RadioButonList) and checks to see if a RadioButton is checked. 

RadioButtonList.FirstOrDefault(Function(radioButton) radioButton.Checked )

Included is a wrapper RadioButtonChecked.

Dim result = RadioButtonChecked

Serialize/deserialization

Working off the provided extension methods it is easy to serialize and deserialize controls. In the following example TextBox controls are worked with by writing these controls to a binary file and reading back from the binary file. Since each TextBox is remembered all properties which are serializable are available on restoring these TextBoxes.

Form Closing event remembers the TextBox controls and on form Shown event information is available.

Form code

Imports System.ComponentModel
Imports System.IO
Imports DescendantsLibrary
Imports ExampleListOfTextBox.Classes
 
Public Class Form1
    Private FileName As String =
                Path.Combine(
                    AppDomain.CurrentDomain.BaseDirectory, "controlData.dat")
 
    Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
        Dim ops = New ControlOperations(FileName)
        If ops.FileExists() Then
            If ops.Load() Then
                For Each data As ControlInformation In ops.List
                    Console.WriteLine(data)
                    Dim tb = GroupBox1.Controls.Find(data.Name, False)(0)
                    tb.Text = data.Text
                Next
            Else
                MessageBox.Show($"Failed to load: {ops.LastExceptionMessage}")
            End If
        End If
    End Sub
 
    Private Sub Form1_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing
 
        Dim TextBoxList = GroupBox1.TextBoxList.OrderBy(Function(box) box.TabIndex)
        Dim ops = New ControlOperations(FileName)
 
        ops.List = TextBoxList.
            Select(Function(box) New ControlInformation With
                      {.Name = box.Name, .Text = box.Text}).ToList()
 
        If Not ops.Save() Then
            MessageBox.Show($"Failed to load: {ops.LastExceptionMessage}")
        End If
 
    End Sub
End Class
 
In this case only the following are stored, as indicated prior, select what properties are important in the project worked on.

Namespace Classes
    ''' <summary>
    ''' Represents controls on a form
    ''' </summary>
    <Serializable()>
    Public Class ControlInformation
        Public Property Id() As Integer
        Public Property Name() As String
        Public Property Text As String
 
        Public Overrides Function ToString() As String
            Return $"{Name}, {Text}"
        End Function
    End Class
End Namespace
 
The following class is responsible for interacting with TextBox information.

Imports System.IO
Imports System.Runtime.Serialization.Formatters.Binary
Imports ExampleListOfTextBox.Classes.ExceptionHandling
 
Namespace Classes
    Public Class ControlOperations
        Inherits BaseExceptionProperties
        Public Property List() As List(Of ControlInformation)
        Public Property FileName() As String
        Public Sub New(pFileName As String)
            List = New List(Of ControlInformation)
            FileName = pFileName
        End Sub
        ''' <summary>
        ''' Use this before a load to ensure there is a file to read.
        ''' </summary>
        ''' <returns></returns>
        Public Function FileExists() As Boolean
            Return File.Exists(FileName)
        End Function
        ''' <summary>
        ''' Used to start over again.
        ''' </summary>
        ''' <returns></returns>
        Public Function DeleteFile() As Boolean
            mHasException = False
 
            Try
                File.Delete(FileName)
                Return True
            Catch ex As Exception
                mHasException = True
                mLastException = ex
            End Try
 
            Return IsSuccessful
 
        End Function
        ''' <summary>
        ''' Load data file with known controls
        ''' </summary>
        ''' <returns></returns>
        Public Function Load() As Boolean
 
            mHasException = False
 
            Dim bf As New BinaryFormatter
            Dim item As Object
 
            Dim fs As New FileStream(FileName, FileMode.OpenOrCreate)
 
            Try
                Do
                    item = bf.Deserialize(fs)
 
                    If item.GetType Is GetType(ControlInformation) Then
                        List.Add(CType(item, ControlInformation))
                    End If
 
                Loop While fs.Position < fs.Length - 1
 
            Catch ex As Exception
                mHasException = True
                mLastException = ex
            Finally
                fs.Close()
                If Not (bf Is Nothing) Then
                    bf = Nothing
                End If
            End Try
 
            Return List.Count > 0
 
        End Function
        ''' <summary>
        ''' Save known controls to dat file.
        ''' </summary>
        ''' <returns></returns>
        Public Function Save() As Boolean
 
            mHasException = False
            Dim identifier As Integer = 1
 
            Dim bf As New BinaryFormatter
 
            Try
 
                Using fs As New FileStream(FileName, FileMode.Create)
 
                    For Each Info In List
                        Info.Id = identifier
                        bf.Serialize(fs, Info)
                        identifier += 1
                    Next
 
                    fs.Close()
 
                End Using
            Catch ex As Exception
                mHasException = True
                mLastException = ex
            Finally
                If Not (bf Is Nothing) Then
                    bf = Nothing
                End If
            End Try
 
            Return IsSuccessful
 
        End Function
        ''' <summary>
        ''' Remove ControlInformation by primary key
        ''' </summary>
        ''' <param name="pControlInformation"></param>
        ''' <returns></returns>
        Public Function Remove(pControlInformation As ControlInformation) As Boolean
            mHasException = False
 
            Try
                If List.FirstOrDefault(Function(item) item.Id = pControlInformation.Id) IsNot Nothing Then
                    List.Remove(pControlInformation)
                    Return True
                Else
                    Return False
                End If
            Catch ex As Exception
                mHasException = True
                mLastException = ex
            End Try
 
            Return IsSuccessful
 
        End Function
        ''' <summary>
        ''' Add a new ControlInformation
        ''' </summary>
        ''' <param name="pControlInformation"></param>
        Public Sub Add(pControlInformation As ControlInformation)
 
            mHasException = False
            Dim identifier As Integer = 1
 
            Try
                If List.Count > 0 Then
                    identifier = List.Select(Function(item) item.Id).Max() + 1
                End If
 
                pControlInformation.Id = identifier
                List.Add(pControlInformation)
 
            Catch ex As Exception
                mHasException = True
                mLastException = ex
            End Try
 
        End Sub
    End Class
End Namespace

Working with C#

The same extension methods above were first created in C# and that C# class project has been included in the source repository. Another use is for those VB.NET developers who walk between writing VB.NET projects and in other situations are writing C# projects.


Summary

Language extensions have been presented to provide the ability to obtain specific controls on a form or in containers on a form rather than writing this code into a form which may be needed in several events so everything is centralized and ready to use in more than one project since these extension methods are in a class project.

See also

Wiki: Visual Basic Portal 

Source code

https://github.com/karenpayneoregon/DescendantsVisalBasicWinForms