New information has been added to this article since publication.
Refer to the
Editor's Update below.
Advanced Basics
Synchronizing Multiple Windows Forms
Ken Spencer
Code download available at:
AdvancedBasics0404.exe
(135 KB)
Browse the Code Online
Q I'm creating a Windows® Forms application with multiple instances of a single form. I'd like to write it so that any operation performed on any one instance of the form will be reflected on all other instances. How can I do this?
Q I'm creating a Windows® Forms application with multiple instances of a single form. I'd like to write it so that any operation performed on any one instance of the form will be reflected on all other instances. How can I do this?
A This is an interesting question. I guarantee that some smart code jockey is going to suggest that I use delegates. Before that happens, let's explore several solutions to this problem.
A This is an interesting question. I guarantee that some smart code jockey is going to suggest that I use delegates. Before that happens, let's explore several solutions to this problem.
Say I have two forms, each with two textbox controls: txt1stData and txt2ndData. How can I keep those controls in sync on each of the two forms? For the purposes of this discussion, it does not matter whether I have two forms or ten, the problem is the same.
The first approach is rather simple. In fact, it's even simpler than using delegates directly, which I think is sometimes overkill. First, I created a class that contains properties that I want to share with all the forms in the application (see Figure 1). For instance, MyData and MoreData hold data that each form can display. I will come back to this class in a minute.

Figure 1 Class with Shared Properties
Public Class DataClass
Private privateMyData As String = ""
Private privateMoreData As String = ""
Public Event MyDataChanged As EventHandler
Public Event MoreDataChanged As EventHandler
Public Property MyData() As String
Get
Return privateMyData
End Get
Set(ByVal Value As String)
privateMyData = Value
RaiseEvent MyDataChanged(Me, New EventArgs)
End Set
End Property
Public Property MoreData() As String
Get
Return privateMoreData
End Get
Set(ByVal Value As String)
privateMoreData = Value
RaiseEvent MoreDataChanged(Me, New EventArgs)
End Set
End Property
Public Sub Reset()
MyData = ""
MoreData = ""
End Sub
End Class
Second, I created two forms with the same controls (txt1stData and txt2ndData) as mentioned earlier. You can see the layout of the form in Figure 2. Both forms have exactly the same data, and I'll explain why in a moment.
Figure 2 Form Layout
Next, I created a module named modGeneral and added the following line of code:
Friend DataStuff As DataClass
This line creates a friend variable for my new DataClass that you can access throughout the assembly, which for this simple example is the entire application. Then I added the following code to the Form1 Load event:
DataStuff = New DataClass
Me.txt1stData.DataBindings.Add("Text", DataStuff, "MyData")
Me.txt2ndData.DataBindings.Add("Text", DataStuff, "MoreData")
The first line creates a new instance of DataClass. The second two lines of code set up the data binding to the textbox controls. That's it for this form!
Now, how do you synchronize them with the data on Form2 and all other forms? Add the following two lines to the form load event of Form 2:
Me.txt1stData.DataBindings.Add("Text", _
DataStuff, "MyData")
Me.txt2ndData.DataBindings.Add("Text", _
DataStuff, "Moredata")
That's one easy way to make sure all forms are in sync with most any type of data. You can simply data bind the controls to the same instance of a class and you're finished.
Now, for another approach. I created a new form named frmBase. Then I placed a textbox (txtNextData) and label on it. I wanted each form in my application to share the textbox and label and I wanted them all to stay in sync with each other, so I rebuilt the project. I created Form1 and Form2 by inheriting from the new frmBase so they could inherit all the new controls. But how can I keep the controls in sync? This time a little coding is required to make it work, but the code is in a single class where it's reused by simply calling a function.
The code sample in Figure 3 shows the module called modGeneral. Its first task is to define two variables: MyForms and localNextData. MyForms is a collection that will contain a list of all the forms I want to synchronize. The variable that I called localNextData will store all the data that I want to display in the forms. Keep in mind that both of these variables could reside in a class instead of a module.

Figure 3 modGeneral
Module modGeneral
Friend DataStuff As DataClass
Friend MyForms As New Collection
Friend localNextData As String
Sub AddForm(ByVal ThisForm As Form)
MyForms.Add(ThisForm)
UpdateControlsNextData(localNextData)
End Sub
Friend Property NextData() As String
Get
Return localNextData
End Get
Set(ByVal Value As String)
localNextData = Value
UpdateControlsNextData(localNextData)
End Set
End Property
Private Sub UpdateControlsNextData(ByVal Value As String)
Dim frm As frmBase
Dim obj As Object
For Each obj In MyForms
frm = CType(obj, frmBase)
If Not IsNothing(frm.txtNextData) Then
frm.txtNextData.Text = Value
End If
Next
End Sub
End Module
[
Editor's Update - 12/6/2004:
The code in Figure 3 has been updated to prevent multiple form instances from being added to the MyForms collection with the same key.]
I will use this collection in the UpdateControlsNextData procedure to determine which forms to update. AddForm also calls UpdateControlsNextData to ensure a new form is updated with the correct data.
The other code in modGeneral is the NextData property. This property's set accessor updates localNextData and also calls UpdateControlsNextData to synchronize all of the forms. Now all I need to do is set NextData whenever I want to change it and all forms will be updated by the call to UpdateControlsNextData.
The third approach, custom linking, is a refinement of the second one. I created it to get a bit more flexibility when processing controls on a form. For instance, I only want to track and process certain forms, those with the controls that must be in sync. This method also lets me define which controls I want to synchronize and then process only those forms.
I added another module for this approach (modGeneralv2) which is shown in
Figure 4. This module includes a collection (MyFormsToUpdate) which will hold all the forms I want to sync. The module also has a new array (ControlsToUpdate) which provides a list of the controls I want to sync. The definition for the array looks like this:
Private ControlsToUpdate() As String = _
{"txtCustomer", "txtAddress", "txtName"}

Figure 4 modGeneralv2
Friend MyFormsToUpdate As New Collection
Private ControlsToUpdate() As String = _
{"txtCustomer", "txtAddress", "txtName"}
Friend Sub AddFormToUpdate(ByVal ThisForm As Form)
Dim ctrl As Control
Dim i As Integer
Dim AddThisForm As Boolean = False
Try
For Each ctrl In ThisForm.Controls
For i = 0 To ControlsToUpdate.GetUpperBound(0)
If ctrl.Name = ControlsToUpdate(i) Then
AddThisForm = True
Exit For
End If
If AddThisForm Then
MyFormsToUpdate.Add(ThisForm, ThisForm.Name)
Exit For
End If
Next
Next
If AddThisForm Then
If MyFormsToUpdate.Count > 1 Then
UpdateControlsOnAllForms( CType(MyFormsToUpdate(1), Form))
End If
End If
Catch ex As Exception
Throw New Exception( _
"AddForm generated this error: " & ex.Message, ex)
End Try
End Sub
Friend Sub UpdateControlsOnAllForms(ByVal MasterForm As Form)
Dim frm As Form
Dim ctrlMaster, ctrlClient As Control
Dim i As Integer
Try
For Each frm In MyFormsToUpdate
For Each ctrlMaster In MasterForm.Controls
For i = 0 To ControlsToUpdate.GetUpperBound(0)
If ctrlMaster.Name = ControlsToUpdate(i) Then
For Each ctrlClient In frm.Controls
If ctrlClient.Name = ControlsToUpdate(i) Then
ctrlClient.Text = ctrlMaster.Text
Exit For
End If
Next
End If
Next
Next
Next
Catch ex As Exception
Throw New Exception( _
"UpdateControlsNextData generated this error: " & ex.Message, ex)
End Try
End Sub
Instead of AddForm, this module has a new and improved version called AddFormToUpdate. This method works in a fashion similar to AddForm but now it only adds the forms that actually contain one or more of the controls in my ControlsToUpdate array, so only those forms with the specific controls are in the update collection. This allows me to call this function from every form. If I decide to add one of the special controls later, it will automatically be added to the forms list. I only need to make one small change to the forms code to hook it up.
This module also contains the UpdateControlsOnAllForms procedure, which performs the update. Instead of using an application-level variable as shown in the last method, I now use the concept of a master form. Thus I can copy the values from that form to all other forms in the collection. UpdateControlsOnAllForms is really a set of simple For...Nexts that loop through the controls on a form, find the ones to update, and update them.
To implement this functionality in my forms, I add this line to the form's Load event:
Alternately, I can add it into the constructor. This line will add the current form instance to the collection to update.
Now let's hook up a single event procedure:
Private Sub txt_Leave(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles txtAddress.Leave, _
txtCustomer.Leave, txtName.Leave
UpdateControlsOnAllForms(Me)
End Sub
This wires up the Leave event of all three controls (txtAddress, txtCustomer, and txtName) that I want to synchronize to one event handler. Then I can add the single line to call UpdateControlsOnAllForms. Me is passed to this procedure call to cause the other forms to be synchronized with this form.
Now I have three versions of code that can synchronize controls on forms, so I have a choice. I could have used custom events, defining an event in the DataClass and having each form subscribe to it. Then when the event fires, the forms could pick up the new data from each event handler and set the appropriate controls. But that's at least as much code as just data binding the controls to the class like I did in the first method. I could create a single procedure that performs the update and place this procedure in a module. I would need to pass the form instance to the procedure for it to perform the update. I could fire this procedure with an event handler from the class. The procedure would look like this one:
Sub UpdateControls(ByVal ThisForm As frmBase)
With ThisForm
.txtNextData.Text = localNextData
End With
End Sub
The parameter to ThisForm is typed as frmBase so that it can have access to IntelliSense® and pick up properties that are custom to the form. Simply typing it as a Form will not cause the properties that are in frmBase and its derived forms to be shown.
Another option is to use delegates. A delegate would, of course, let me redirect an invocation of the delegate to a method in each form. If I used multicasting I could let each form handle the event and update the appropriate controls. It sounds really simple to set up this functionality with delegates, but to me it's more trouble than it's worth in practice. Plus, other than the nested For...Next loops in the third method, the code is not difficult to understand. And after all, the most costly part of an app is still its maintenance.
Send your questions and comments for Ken to basics@microsoft.com.
Ken Spencer works for
32X Tech, where he provides training, software development, and consulting services on Microsoft technologies.