VB.NET: Passing Values Between Two Forms
Introduction
One of the first obstacles that people new to programming run into is how to communicate between multiple forms. How does a developer get the main form (a parent) to open and pass along data to a child form? This question typically arises when the programmer wants to create a dialog situation where they click a button to launch a dialog form, ask for some settings and close it to return to the main form they were working on. There are many options to accomplish form communication ranging from using properties of a form, passing information using an overload of a form constructor
Passing information using form properties
Since forms are classes the first option to pass information, in this case from parent to a child form is by setting a property in the child form during the creation of the child form in the parent form. Rather than pass a string, number, date etc in the follow code a instance of a class will be passed.
Source code in GitHub repository
Person consist of an integer, two string and one date property.
Public Class Person
Public Property Id() As Integer
Public Property FirstName() As String
Public Property LastName() As String
Public Property BirthDay() As Date
Public Overrides Function ToString() As String
Return $"{FirstName} {LastName}"
End Function
End Class
In the parent form a person is mocked up in a button click event responsible for creating the child form and passing the person to the child form.
Public Class Form1
Private _person As New Person
Private Sub ChildFormButton1_Click(sender As Object, e As EventArgs) Handles ChildFormButton1.Click
_person.Id = 100
_person.FirstName = FirstNameTextBox.Text
_person.LastName = LastNameTextBox.Text
_person.BirthDay = #9/23/1980#
Dim childForm As New ChildForm() With {.Person = _person}
childForm.ShowDialog()
End Sub
End Class
Then in the child form the public instance of a person is used to populate form controls.
Public Class ChildForm
Public Property Person() As Person
''' <summary>
''' Person property is populated via the following line from Form1
''' where the With clause sets the person
'''
''' Dim childForm As New ChildForm() With {.Person = _person}
'''
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Sub ChildForm_Shown(sender As Object, e As EventArgs) _
Handles Me.Shown
IdentifierLabel.Text = $"{Person.Id}"
FirstNameTextBox.Text = Person.FirstName
LastNameTextBox.Text = Person.LastName
BirthDayDateTimePicker.Value = Person.BirthDay
End Sub
Private Sub CloseButton_Click(sender As Object, e As EventArgs) _
Handles CloseButton.Click
' no action needed as DialogResult has
' been set in the button's property window
End Sub
End Class
At first glance this is a great solution until there is a need to pass information back to the parent form before closing the child form which is where using delegates and events or factory listeners comes into play. If the intent is to never pass information back this solution is perfect, if there is a need to collect information from the child form after the parent form has closed see the next section which also includes asserting that data is not empty.
Passing values using an overloaded form constructor
Information may be passed between forms using a simple type e.g. string, numeric, date etc although usually more than one piece of information is needed when communicating between forms.
The following example passes a Person between forms. To keep things simple the Person class has three properties, only two are used, if the person object was read from a database table the Id property would contain the primary key for that record which can be used in edit and delete processes.
Namespace Classes
Public Class Person
Public Property Id() As Integer
Public Property FirstName() As String
Public Property LastName() As String
Public Overrides Function ToString() As String
Return $"{FirstName} {LastName}"
End Function
End Class
End Namespace
Child form
To be able to share the person, a private variable of type person is declared along with a readonly property which is available to the calling form which is done to control the person object rather than make it public which would allow other processes to alter the properties of the person object. To set the person object an overload of the form constructor is created which accepts a person object. Its important to have InitializeComponent as the first line in the constructor so that controls are created at runtime.
There are two button, a Okay button which when pressed if the two TextBox controls are populates sets those values to the private form level variable which in turn allows the calling form access to the person.
The Cancel button has no code, in the property window of the button DialogResult is set to cancel which when clicking the button closes the form.
Public Class ChildForm
Private _person As New Person
Public ReadOnly Property Person() As Person
Get
Return _person
End Get
End Property
Public Sub New(person As Person)
InitializeComponent()
_person.FirstName = person.FirstName
_person.LastName = person.LastName
End Sub
Private Sub ChildForm_Shown(sender As Object, e As EventArgs) Handles Me.Shown
FirstNameTextBox.Text = _person.FirstName
LastNameTextBox.Text = _person.LastName
End Sub
Private Sub OkayButton_Click(sender As Object, e As EventArgs) Handles OkayButton.Click
If Not String.IsNullOrWhiteSpace(FirstNameTextBox.Text) AndAlso Not String.IsNullOrWhiteSpace(LastNameTextBox.Text) Then
_person.FirstName = FirstNameTextBox.Text
_person.LastName = LastNameTextBox.Text
DialogResult = DialogResult.OK
End If
End Sub
End Class
Main form
The main form has the exact same TextBox controls and a button. If when the button is clicked there is information in the TextBoxes a person object is populated and passed into the overloaded constructor for the child form. The form is shown modally, when the child form is closed the DialogResult is checked. If DialogResult is Okay the TextBox controls on the main form are populated with data collected from the child form.
Imports PassingValuesConventional.Classes
Public Class Form1
Private _person As New Person
Private Sub ChildFormButton1_Click(sender As Object, e As EventArgs) Handles ChildFormButton1.Click
If Not String.IsNullOrWhiteSpace(FirstNameTextBox.Text) AndAlso Not String.IsNullOrWhiteSpace(LastNameTextBox.Text) Then
_person.FirstName = FirstNameTextBox.Text
_person.LastName = LastNameTextBox.Text
Dim childForm As New ChildForm(_person)
If childForm.ShowDialog() = DialogResult.OK Then
FirstNameTextBox.Text = childForm.Person.FirstName
LastNameTextBox.Text = childForm.Person.LastName
End If
End If
End Sub
End Class
Custom events
Custom events are very useful with common operations but only when a developer understands delegates and events, otherwise developers gravitate to actions that are half-baked, violate OOP concepts.
Source code for the following is in the following GitHub repository.
Example 1 (form communications)
Simple example, the task is to open a new window with a countdown timer where the calling (main) form can start and stop the countdown. The common path is to access the timer component and invoke the Start and Stop method e.g. childForm.Timer1.Start. This is possible since the modifier property of the Timer is Friend. Implementing an event using a delegate the calling form can invoke an event to start/stop the timer without referencing the Timer in the main form.
Delegate and event in main form (Form1)
Public Class Form1
' Our delegate (which "points" at any method which takes an object and EventArgs)
' Look familiar? This is the signature of most control events on a form
Public Delegate Sub SendMessage(obj As Boolean, e As EventArgs)
' Here is the event we trigger to send messages out to listeners
' in this case a Boolean which is used to start/stop a Timer.
Public Event OnSendMessage As SendMessage
In the child form create an event of type SendMessage defined in the main form. This has one line to start/stop the Timer based on the Checked state of a CheckBox in the main (Form1) form.
Public Class Form2
Public Sub MessageReceived(sender As Boolean, e As EventArgs)
Timer1.Enabled = sender
End Sub
In the main form there is a button with the following click event. Which subscribes to MessageReceived in the child form.
Private Sub ShowChildFormButton_Click(sender As Object, e As EventArgs) _
Handles ShowChildFormButton.Click
Dim child As New Form2()
AddHandler OnSendMessage, AddressOf child.MessageReceived
child.Show()
child.Location = New Point(Left + (Width + 10), Top + 5)
End Sub
Note That the first argument to SendMessage delegate is not a Object as in common events found in events like a Button Click event, the developer can make this what is needed. |
Screenshots are always helpful to visualize, the following shows the above in action.
Example 2 (forms communication)
In the first example the delegate and event were placed in the main form, a better idea is to place delegates in a code module, public if the module is in a different project such as a class project.
In the example the task is to open a child form and perform mocked data entry on two properties of a class, pressing ENTER will raise an event passing an instance of a concrete class (Person) to the calling form were the calling form will Validate the two properties are not empty and that this instance of a person is not present in a DataGridView with a BindingSource as the DataSource.
The following are part of this example
- Delegates are declared in a code module.
- Custom EventArgs are used and stored in a class.
- An anonymous method is used as a predicate to a Lambda statement to keep an line of assertion easy to read
- Child form overrides ProcessCmdKey to assist with moving from TextBox to TextBox.
The following delegate in a code module is used to pass information from child form to calling form.
Namespace Modules
Public Module DelegatesModule
Public Delegate Sub FormMessageHandler(
sender As Object, args As FormMessageArgs)
End Module
End Namespace
The above delegate FormMessageHandler has a custom event args.
Namespace Classes
Public Class FormMessageArgs
Inherits EventArgs
Protected _person As Person
''' <summary>
''' Expects a valid Person object with
''' First and Last name populated
''' </summary>
''' <param name="person"></param>
Public Sub New(person As Person)
_person = person
End Sub
''' <summary>
''' Returns a Person object
''' </summary>
''' <returns></returns>
Public ReadOnly Property Person() As Person
Get
Return _person
End Get
End Property
''' <summary>
''' Determines if the person object is populated
''' </summary>
''' <returns></returns>
Public ReadOnly Property IsValidPerson() As Boolean
Get
Return _
Not String.IsNullOrWhiteSpace(_person.FirstName) AndAlso
Not String.IsNullOrWhiteSpace(_person.LastName)
End Get
End Property
End Class
End NameSpace
Definition of the Person class used in the FormMessageArgs above.
Namespace Classes
Public Class Person
Public Property FirstName() As String
Public Property LastName() As String
Public Overrides Function ToString() As String
Return $"{FirstName} {LastName}"
End Function
End Class
End Namespace
In the child form the following method determines if both TextBox controls have data and if so raises OnMessageInformationChanged event using the custom event arguments of type FormMesageArgs.
Private Sub ConditionallyAddPerson()
Dim hasFirstName = Not String.IsNullOrWhiteSpace(FirstNameTextBox.Text)
Dim hasLastName = Not String.IsNullOrWhiteSpace(LastNameTextBox.Text)
If hasFirstName AndAlso hasLastName Then
'
' Both TextBox controls have data, push to the main form
' which has subscribed to this event.
'
RaiseEvent OnMessageInformationChanged(Me, New FormMessageArgs(
New Person() With
{
.FirstName = FirstNameTextBox.Text,
.LastName = LastNameTextBox.Text
}))
End If
End Sub
The method above is triggered in ProcessCmdKey if the ENTER key was depressed while in the last name TextBox.
Protected Overrides Function ProcessCmdKey(
ByRef msg As Message, keyData As Keys) As Boolean
If msg.WParam.ToInt32() = Keys.Enter AndAlso ActiveControl.Name = "LastNameTextBox" Then
ConditionallyAddPerson()
SendKeys.Send("{Tab}")
Return True
ElseIf msg.WParam.ToInt32() = Keys.Enter AndAlso ActiveControl.Name = "FirstNameTextBox" Then
SendKeys.Send("{Tab}")
Return True
End If
Return MyBase.ProcessCmdKey(msg, keyData)
End Function
In the calling form when creating an instance of the child form the main form subscribes to OnMessageInformationChanged which is triggered in the child form CondiionallyAddPerson invoked in processCmdKey. A standard event is subscribed too also, form closing event of the child form as when the child form is shown the button which shows the child form is disabled so to reenable FormClosed event is used to re-enable the form show button.
Private Sub DisplayChildForm()
If Not My.Application.IsFormOpen("ChildForm") Then
_childForm = New ChildForm With {
.Owner = Me
}
AddHandler _childForm.OnMessageInformationChanged, AddressOf GetNewPerson
AddHandler _childForm.FormClosing, AddressOf ChildFormClosed
_firstTime = True
End If
_childForm.Show()
ShowChildFormButton.Enabled = False
If _firstTime Then
_childForm.Location = New Point(Left + (Width + 10), Top + 5)
_firstTime = False
End If
End Sub
Note When reviewing code note Person FirstName and person LastName properties are checked for empty strings in several places, one is sufficient, each developer will want this as they see fit so this allows the reader to use one of the several assertions. |
Back in the main form, directly after creating the child form a AddHandler was done
AddHandler _childForm.OnMessageInformationChanged, AddressOf GetNewPerson
Where GetNewPerson event receives a person sent from the child form.
Private Sub GetNewPerson(sender As Object, args As FormMessageArgs)
Dim people = CType(_bindingSource.DataSource, List(Of Person))
If people.FirstOrDefault(Function(p) PersonValid(p, args.Person)) Is Nothing Then
_bindingSource.Add(args.Person)
Else
MessageBox.Show($"{args.Person} exists.")
End If
End Sub
Note the predicate in the above FirstOrDefault method which is shown below in the main form which shows how to create a predicate for one of two or both reasons.
- Reusable
- Takes a large assertion out of business code, now the code is easier to read
' Anonymous method
Public PersonValid As ValidPerson =
Function(person1 As Person, person2 As Person)
Return Not String.IsNullOrWhiteSpace(person1.FirstName) AndAlso
Not String.IsNullOrWhiteSpace(person1.LastName) AndAlso
person1.FirstName = person2.FirstName AndAlso
person1.LastName = person2.LastName
End Function
Person1 is an instance of a iteration againsts the List of Person object in a BindingSource component which is the DataSource of a DataGridView while Person2 contains data from the child form.
Here is the entire code for the main form.
Imports FollowParent.Classes
Imports FollowParent.Modules
Public Class Form1
Private ReadOnly _bindingSource As New BindingSource
Private _childForm As ChildForm
Private _firstTime As Boolean = True
' Anonymous method
Public PersonValid As ValidPerson =
Function(person1 As Person, person2 As Person)
Return Not String.IsNullOrWhiteSpace(person1.FirstName) AndAlso
Not String.IsNullOrWhiteSpace(person1.LastName) AndAlso
person1.FirstName = person2.FirstName AndAlso
person1.LastName = person2.LastName
End Function
Private Sub ShowChildFormButton_Click(sender As Object, e As EventArgs) _
Handles ShowChildFormButton.Click
DisplayChildForm()
End Sub
Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
If _childForm IsNot Nothing Then
_childForm.Dispose()
End If
End Sub
Private Sub DisplayChildForm()
If Not My.Application.IsFormOpen("ChildForm") Then
_childForm = New ChildForm With {
.Owner = Me
}
AddHandler _childForm.OnMessageInformationChanged, AddressOf GetNewPerson
AddHandler _childForm.FormClosing, AddressOf ChildFormClosed
_firstTime = True
End If
_childForm.Show()
ShowChildFormButton.Enabled = False
If _firstTime Then
_childForm.Location = New Point(Left + (Width + 10), Top + 5)
_firstTime = False
End If
End Sub
Private Sub ChildFormClosed(sender As Object, e As FormClosingEventArgs)
ShowChildFormButton.Enabled = True
End Sub
''' <summary>
''' Person passed in from child form
''' </summary>
''' <param name="sender"></param>
''' <param name="args"></param>
''' <remarks>
'''
''' </remarks>
Private Sub GetNewPerson(sender As Object, args As FormMessageArgs)
Dim people = CType(_bindingSource.DataSource, List(Of Person))
If people.FirstOrDefault(Function(p) PersonValid(p, args.Person)) Is Nothing Then
_bindingSource.Add(args.Person)
Else
MessageBox.Show($"{args.Person} exists.")
End If
End Sub
Private Sub Form1_LocationChanged(sender As Object, e As EventArgs) Handles Me.LocationChanged
If Not _FirstTime Then
_ChildForm.Location = New Point(Left + (Width + 10), Top + 5)
End If
End Sub
Private Sub ExitApplicationButton_Click(sender As Object, e As EventArgs) _
Handles ExitApplicationButton.Click
Close()
End Sub
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
DataGridView1.AutoGenerateColumns = False
_bindingSource.DataSource = New List(Of Person)
DataGridView1.DataSource = _bindingSource
End Sub
End Class
Entire code for child form.
Imports FollowParent.Classes
Imports FollowParent.Modules
Public Class ChildForm
Public Event OnMessageInformationChanged As FormMessageHandler
Private Sub cmdClose_Click(sender As Object, e As EventArgs) Handles cmdClose.Click
Close()
End Sub
''' <summary>
''' Event triggered in ProcessCmdKey event when ENTER is pressed
''' in the LastNameTextBox and both TextBox controls have values
''' </summary>
Private Sub ConditionallyAddPerson()
Dim hasFirstName = Not String.IsNullOrWhiteSpace(FirstNameTextBox.Text)
Dim hasLastName = Not String.IsNullOrWhiteSpace(LastNameTextBox.Text)
If hasFirstName AndAlso hasLastName Then
'
' Both TextBox controls have data, push to the main form
' which has subscribed to this event.
'
RaiseEvent OnMessageInformationChanged(Me, New FormMessageArgs(
New Person() With
{
.FirstName = FirstNameTextBox.Text,
.LastName = LastNameTextBox.Text
}))
End If
End Sub
''' <summary>
''' Ensures no beep on enter key pressed
''' Not practical for a typical data entry form.
''' </summary>
''' <param name="msg"></param>
''' <param name="keyData"></param>
''' <returns></returns>
Protected Overrides Function ProcessCmdKey(ByRef msg As Message, keyData As Keys) As Boolean
If msg.WParam.ToInt32() = Keys.Enter AndAlso ActiveControl.Name = "LastNameTextBox" Then
ConditionallyAddPerson()
SendKeys.Send("{Tab}")
Return True
ElseIf msg.WParam.ToInt32() = Keys.Enter AndAlso ActiveControl.Name = "FirstNameTextBox" Then
SendKeys.Send("{Tab}")
Return True
End If
Return MyBase.ProcessCmdKey(msg, keyData)
End Function
End Class
Note There is a good deal to consume in the above code sample, it's highly recommended to take time to study the code rather than simply attempting to implement in an existing project. |
See also
VB.NET: Windows Forms Custom delegates and events
Summary
Several options have been presented to communicate forms where which option to use depends on business requirements.