다음을 통해 공유


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

Screenshot

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.

Source code for this section.

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.

Here is our visual.

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.