WinForms get all controls of a specific type VB.NET
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
Source code
https://github.com/karenpayneoregon/DescendantsVisalBasicWinForms