Monday, October 21, 2013

Managing Data Among Multiple Forms (Part 3)

Part 1 here
Part 2 here

Firstly, let me apologise for having taken so long to finish this three-part series.  Parts 1 and 2 showed how you CAN manage data among multiple forms but this part 3 will show how you SHOULD do it.  That’s rather important I’d say, so let’s get into it.

The first and most important point to note is that forms are objects like any other, so moving data between forms is done just like it is for any other objects.  How do you usually pass data into an object?  You either set a property or else call a method and pass an argument.  How do you usually get data out of an object?  You either get a property or else call a method and get the return value.  That’s exactly how you pass data into and get data out of a form because forms are objects.

The second point to note is that, generally speaking, a control should only be accessed by the form it is on.  While it’s legal to access a control from outside its form, good practice dictates that you should not do so.  With the first point in mind, that means that getting data from a control on a different form means getting data from the other form and the other form getting it from the control.  Likewise, passing data to a control on another form means passing data to the other form and the other form passing it to the control.

This can be demonstrated fairly easily by displaying a list of records in one form and editing the selected record in another form.  To build such an example, start by creating a new Windows Forms Application project.  To Form1, add a DataGridView, a Button and a BindingSource.  Now add the following code to populate the grid with a few records at startup.

C#

  1. private void Form1_Load(object sender, EventArgs e)
  2. {
  3.     var table = new DataTable();
  4.  
  5.     table.Columns.Add("ID", typeof(int));
  6.     table.Columns.Add("Name", typeof(string));
  7.  
  8.     table.Rows.Add(1, "Peter");
  9.     table.Rows.Add(2, "Paul");
  10.     table.Rows.Add(3, "Mary");
  11.  
  12.     this.bindingSource1.DataSource = table;
  13.     this.dataGridView1.DataSource = this.bindingSource1;
  14. }

VB

  1. Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
  2.     Dim table As New DataTable
  3.  
  4.     With table.Columns
  5.         .Add("ID", GetType(Integer))
  6.         .Add("Name", GetType(String))
  7.     End With
  8.  
  9.     With table.Rows
  10.         .Add(1, "Peter")
  11.         .Add(2, "Paul")
  12.         .Add(3, "Mary")
  13.     End With
  14.  
  15.     Me.BindingSource1.DataSource = table
  16.     Me.DataGridView1.DataSource = Me.BindingSource1
  17. End Sub

Add a second form to the project and add a TextBox and a Button to that form.  What we’re going to do is click the Button in Form1 to open Form2, get the record selected in the DataGridView in Form1 and edit its Name field in the TextBox in Form2.  Now remember, Form2 cannot access the DataGridView in Form1 and Form1 cannot access the TextBox in Form2.  How to move the data back and forth?  The answer is that Form1 will set a property of Form2 to pass the initial data in and get the same property to get the final data out while, internally, that property of Form2 will access the TextBox.  Which property?  Well, one that you define yourself.

C#

  1. public string TextBoxText
  2. {
  3.     get
  4.     {
  5.         return this.textBox1.Text;
  6.     }
  7.     set
  8.     {
  9.         this.textBox1.Text = value;
  10.     }
  11. }

VB

  1. Public Property TextBoxText As String
  2.     Get
  3.         Return Me.TextBox1.Text
  4.     End Get
  5.     Set(value As String)
  6.         Me.TextBox1.Text = value
  7.     End Set
  8. End Property

Back in Form1, we need to handle the Click event of the Button, open Form2 and pass it the Name from the record selected in the DataGridView.

C#

  1. private void button1_Click(object sender, EventArgs e)
  2. {
  3.     using (var dialogue = new Form2())
  4.     {
  5.         var selectedRecord = (DataRowView)this.bindingSource1.Current;
  6.  
  7.         // Pass the initial data into the dialogue.
  8.         dialogue.TextBoxText = (string)selectedRecord["Name"];
  9.  
  10.         // Display the modal dialogue.
  11.         if (dialogue.ShowDialog() == DialogResult.OK)
  12.         {
  13.             // The user clicked OK so get the final data from the dialogue.
  14.             selectedRecord["Name"] = dialogue.TextBoxText;
  15.             this.bindingSource1.EndEdit();
  16.         }
  17.     }
  18. }

VB

  1. Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
  2.     Using dialogue As New Form2
  3.         Dim selectedRecord = DirectCast(Me.BindingSource1.Current, DataRowView)
  4.  
  5.         'Pass the initial data into the dialogue.
  6.         dialogue.TextBoxText = CStr(selectedRecord("Name"))
  7.  
  8.         'Display the modal dialogue.
  9.         If dialogue.ShowDialog() = DialogResult.OK Then
  10.             'The user clicked OK so get the final data from the dialogue.
  11.             selectedRecord("Name") = dialogue.TextBoxText
  12.             Me.BindingSource1.EndEdit()
  13.         End If
  14.     End Using
  15. End Sub

All that’s left to do now is for Form2 to return a result of OK when the user clicks the Button.

C#

  1. private void button1_Click(object sender, EventArgs e)
  2. {
  3.     this.DialogResult = DialogResult.OK;
  4. }

VB

  1. Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
  2.     Me.DialogResult = DialogResult.OK
  3. End Sub

Run the project now and Form1 will appear displaying the three records.  Select one of the records and click the Button.  You will see Form2 open with the Name field value from the selected record in the TextBox.  Try editing the name and then click the Close button on the title bar of Form2.  The dialogue will close and you’ll see that the Name field of the selected record remains unchanged.  That’s because the DialogResult returned by ShowDialog was Cancel rather than OK.

Click the Button on Form1 again and this time, after editing the name in the TextBox, click the Button on Form2.  This time, you’ll see that Form2 closes and the Name field of the selected record is updated to the value that you entered in the TextBox.  Congratulations!  You just passed data between two forms the right way.

That’s nice and all but what if, in our example, you wanted to do something back in Form1 without closing Form2?  As it stands, ShowDialog returning is Form1’s notification that it should get some data from Form2 and update its own DataGridView.  If we don’t call ShowDialog though, it can’t return and we can’t use it as a notification.  What to do?  Well, how are you usually notified that something has happened in .NET code?  You handle an event.

If you want to learn all the details about custom events, I suggest that you check out my blog post here.  I’m going to do it quick and dirty here because the point of this post is how to handle the event and use that notification rather than the details of how to generate it in the first place.

In Form2, we need to add an event that will notify anyone listening that the text in the TextBox has changed and, instead of closing the form when the Button is clicked, we need to raise that event.

C#

  1. public event EventHandler TextBoxTextChanged;
  2.  
  3. private void button1_Click(object sender, EventArgs e)
  4. {
  5.     if (this.TextBoxTextChanged != null)
  6.     {
  7.         this.TextBoxTextChanged(this, EventArgs.Empty);
  8.     }
  9. }

VB

  1. Public Event TextBoxTextChanged As EventHandler
  2.  
  3. Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
  4.     RaiseEvent TextBoxTextChanged(Me, EventArgs.Empty)
  5. End Sub

We now need to handle that event in Form1.  When it’s raised, we need to do as before and get the data from Form2 to update the selected record.

C#

  1. private Form2 dialogue;
  2. private DataRowView selectedRecord;
  3.  
  4. private void button1_Click(object sender, EventArgs e)
  5. {
  6.     if (this.dialogue == null || this.dialogue.IsDisposed)
  7.     {
  8.         this.selectedRecord = (DataRowView)this.bindingSource1.Current;
  9.  
  10.         this.dialogue = new Form2();
  11.         this.dialogue.TextBoxTextChanged += dialogue_TextBoxTextChanged;
  12.         this.dialogue.FormClosed += dialogue_FormClosed;
  13.  
  14.         // Pass the initial data into the dialogue.
  15.         this.dialogue.TextBoxText = (string)this.selectedRecord["Name"];
  16.  
  17.         this.dialogue.Show();
  18.     }
  19.  
  20.     this.dialogue.Activate();
  21. }
  22.  
  23. private void dialogue_TextBoxTextChanged(object sender, EventArgs e)
  24. {
  25.     // Get the final data from the dialogue.
  26.     this.selectedRecord["Name"] = dialogue.TextBoxText;
  27.     this.bindingSource1.EndEdit();
  28. }
  29.  
  30. private void dialogue_FormClosed(object sender, FormClosedEventArgs e)
  31. {
  32.     // Remove event handlers when the form closes.
  33.     this.dialogue.TextBoxTextChanged -= dialogue_TextBoxTextChanged;
  34.     this.dialogue.FormClosed -= dialogue_FormClosed;
  35. }

VB

  1. Private WithEvents dialogue As Form2
  2. Private selectedRecord As DataRowView
  3.  
  4. Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
  5.     If Me.dialogue Is Nothing OrElse Me.dialogue.IsDisposed Then
  6.         Me.selectedRecord = DirectCast(Me.BindingSource1.Current, DataRowView)
  7.  
  8.         Me.dialogue = New Form2()
  9.  
  10.         'Pass the initial data into the dialogue.
  11.         Me.dialogue.TextBoxText = CStr(selectedRecord("Name"))
  12.  
  13.         Me.dialogue.Show()
  14.     End If
  15.  
  16.     Me.dialogue.Activate()
  17. End Sub
  18.  
  19. Private Sub dialogue_TextBoxTextChanged(sender As Object, e As EventArgs) Handles dialogue.TextBoxTextChanged
  20.     'Get the final data from the dialogue.
  21.     Me.selectedRecord("Name") = dialogue.TextBoxText
  22.     Me.BindingSource1.EndEdit()
  23. End Sub

If you run the project again you will see that you can open the dialogue and edit the selected record multiple times without closing the dialogue.  If you close the dialogue and select another record then you can open a new dialogue and edit that as well.

This is a slightly contrived example but hopefully you get the idea.  If you want to update a control in a form then only do it in that form.  If you need to push and/or pull data between forms then you do so by getting or setting properties and/or calling methods of that form.  If you need to notify a form that data is available to get then you do so with an event.

Happy trails!

Post compiled using Windows Live Writer with Paste as Visual Studio Code plug-in.

5 comments:

Dangdut said...

nice tutorial jmchilney ;)

NssB said...

I've always used Public Properties to pass info between forms on the GUI thread (Main).......but could you possibly detail the best approach to passing data between open forms and background threads? (using the BackgroundWorker tool)

At this point i'm calling a delegate function to invoke the form and it's properties......which has been a challenge when the forms are dynamically generated and can be open/closed during the background operation.

I can provide all my sample code.......however I have an example already posted on VBNETFORUMS if you'd like a bash:

http://www.vbdotnetforums.com/windows-forms/61177-multithreaded-dynamic-form-generation-handling.html#post169256

Anonymous said...

What if we have a let's say 20 columned dgv to edit.

Should we create 20 properties to edit?

jmcilhinney said...

@NssB, sorry it took me so long to address this. Your question actually goes beyond the scope of this post so I won't answer it here. It's certainly a legitimate question but not for here.

jmcilhinney said...

@Anonymous, with regards to your question on editing a 20-columned DataGridView, what you do depends on the specific situation but the first thing that comes to my mind is a single read-only property that exposes the currently selected item in the grid.