Sunday, April 1, 2012

Managing Data Among Multiple Forms (Part 1)

Lots of people ask questions about how to pass data between forms.  It’s a slightly tricky question to answer because there are several ways to do it, all variations on a theme, and the “proper” way is the most complex.  “Complex” is a relative term though, as none of them are particularly difficult.  I’m going to dedicate a separate post to each of the various options, using the same basic example in each case.  That example will involve two forms, each with a TextBox.  The user will enter text into the TextBox on the first form and click a Button.  That will open the second form and transfer the text to be displayed in the TextBox on that.  The user can then edit the text on the second form and, when they close it, the new text will be transferred back to the TextBox on the first form.

The first example is VB-specific, but can be simulated in C#.  I have previously posted about default instances here, so for more general information you should start there.  Here we will concentrate on actually passing data between two default instances.  So, first let’s set up the project as described above.  Create a new VB Windows Forms Application project and add a TextBox and a Button to the Form1 that’s created by default.  Add a second form and add a TextBox to that as well.

There are two options for passing the data around: push and pull.  The data producer can push the data to the consumer or the consumer can pull it from the producer.  We’ll construct this example that uses both in two different combinations.  First, let’s make Form1 push the initial data to Form2 and then pull the new data back again.

So, on Form1, double-click the Button you added and add the following code:

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    'Push the data to the TextBox on the second form from the TextBox on this form.
    Form2.TextBox1.Text = Me.TextBox1.Text

    'Show the second form as a modal dialogue.
    Form2.ShowDialog()

    'Pull the data from the TextBox on the second form to the TextBox on this form.
    Me.TextBox1.Text = Form2.TextBox1.Text
End Sub

The first line gets the text from the TextBox on the current form and displays it in the TextBox on the default instance of Form2.  The second line displays the default instance of Form2 as a modal dialogue.  The third line gets the text from the TextBox on the default instance of Form2 and displays it in the Textbox on the current form.  Now, run the project, enter some text into the TextBox on Form1 and click the Button.  You’ll see that whatever you entered into the TextBox on Form1 displayed in the TextBox on Form2.  Edit the text in the TextBox and close Form2 and you’ll then see the new text displayed in the TextBox on Form1.

In that example, Form1 does all the work, pushing data one way and pulling it the other.  Let’s change things up a bit and let Form2 do the work.  Change your Button’s Click event handler to this:

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    'Show the second form as a modal dialogue.
    Form2.ShowDialog()
End Sub

Form1 is still displaying Form2 but it is not moving any of the data around anymore.  Double-click Form2 to create a Load event handler and add the following code:

Private Sub Form2_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    'Pull the data from the TextBox on the first form to the TextBox on the this form.
    Me.TextBox1.Text = Form1.TextBox1.Text
End Sub

That will get the data from the TextBox on the default instance of Form1 and display it in the TextBox on the current form just before Form2 is displayed.  It’s worth mentioning at this point that the startup form in a VB Windows Forms Application project is always the default instance of its type.  Using the drop-down lists at the top of the code window, create a FormClosed event handler and add this code:

Private Sub Form2_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
    'Push the data to the TextBox on the first form from the TextBox on this form.
    Form1.TextBox1.Text = Me.TextBox1.Text
End Sub

That will get the data from the TextBox on the current form and display it in the TextBox on the default instance of Form1 just after Form2 closes.  Now, as before, run the project, enter text in the TextBox on Form1, click the Button, edit the text in the TextBox on Form2 and then close Form2.  As before, you’ll see whatever you entered on Form1 transferred to Form2 and whatever you change it to on Form2 transferred back to Form1.

That’s basically it.  You can access default instances anywhere in your project so you can always access any and all public members of that instance whenever you want.  In VB, when you add a control or component to a form in the designer they will be public by default, so you can access those controls directly from other forms whenever you want.  I don’t really recommend using default instances or public controls, but it’s the easy option for beginners.

Now, as I said earlier, default instances are a VB-specific feature but, if you want, you can simulate them in C#.  Given that default instances are aimed at beginners and this C# implementation requires use of a custom generic class, it defeats the purpose somewhat, but it’s an interesting case study nonetheless.

Here is a relatively simple class that will partially simulate default instances in C#:

///
/// Provides VB-style default instance behaviour for forms.
///
///
/// The type of form for which a default instance is provided.
///
public class DefaultInstance where TForm : Form, new()
{
    ///
    /// Refers to the current default instance.
    ///
    private static TForm _instance;

    ///
    /// Gets the default instance of the form type.
    ///
    public static TForm Instance
    {
        get
        {
            // Check whether there is no current default instance or that instance has been displayed.
            if (_instance == null || _instance.IsDisposed)
            {
                // Create a new default instance.
                _instance = new TForm();
            }

            return _instance;
        }
    }
}
Just like default instances in VB, this class requires that the form class have a parameterless constructor.  Unlike the default instances in VB, the instance maintained by this class is not thread-specific.  It could be reimplemented to provide that behaviour but, to be honest, I’m not sure that that would be an improvement.  As I said in the post on default instances that I linked to earlier, the fact that they are thread-specific is actually an encumbrance at times.  Presumably there is a reason that Microsoft chose to implement them that way though, so maybe there are other issues that I’m not aware of.

Anyway, what I’ve presented there is enough for our purposes here.  Where you would normally just use the form class name in VB code to refer to the default instance, e.g.

SomeFormClass.Show()

you would use the DefaultInstance class in C# code like this:

DefaultInstance<SomeFormClass>.Instance.Show();

A little more verbose but not a big deal.

So, create a new C# Windows Forms Application project and add the same forms and controls as specified previously.  Again, double-click the Button on Form1 to create a Click event handler and add the following code:

private void button1_Click(object sender, EventArgs e)
{
    // Push the data to the TextBox on the second form from the TextBox on this form.
    DefaultInstance<Form2>.Instance.textBox1.Text = this.textBox1.Text;

    // Show the second form as a modal dialogue.
    DefaultInstance<Form2>.Instance.ShowDialog();

    // Pull the data from the TextBox on the second form to the TextBox on this form.
    this.textBox1.Text = DefaultInstance<Form2>.Instance.textBox1.Text;
}

Run the project as before and you’ll see the text transferred from Form1 to Form2 and back again.
That works when Form1 does all the work but there is an extra step required when Form2 is doing the work.  As I said earlier, the startup form is always the default instance of its type in VB applications.  That’s what allowed us to refer to the default instance of Form1.  In our C# project, we need to edit the Main method so that it uses our DefaultInstance class to create the startup form.  That means that the Main method, found in the Program.cs code file, must look like this:

///
/// The main entry point for the application.
///
[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(DefaultInstance<Form1>.Instance);
}

Because the startup form is now the default instance of its type, Form2 can access it directly via the DefaultInstance class.  Note that another difference between VB and C# is that controls and components added to a form in the designer are private by default.  That really is the proper way to go but then we’re using default instances here so we’re not doing things the proper way.  With that in mind, you’ll need to change the access modifier of the controls on the two forms from Private to Public.

Change the Button.Click event handler in Form1 to this:

private void button1_Click(object sender, EventArgs e)
{
    // Show the second form as a modal dialogue.
    DefaultInstance<Form2>.Instance.ShowDialog();
}

and add a Load event handler and a FormClosed event handler to Form2 like this:

private void Form2_Load(object sender, EventArgs e)
{
    // Pull the data from the TextBox on the first form to the TextBox on the this form.
    this.textBox1.Text = DefaultInstance<Form1>.Instance.textBox1.Text;
}

private void Form2_FormClosed(object sender, FormClosedEventArgs e)
{
    // Push the data to the TextBox on the first form from the TextBox on this form.
    DefaultInstance<Form1>.Instance.textBox1.Text = this.textBox1.Text;
}

Run the project as before and again see that the data is transferred from Form1 to Form2 and back again.

That’s really all there is to it.  If you use default instances every time you display a form then you can use that default instance to access the form from anywhere and at any time in your application.  You can access any public members of that form so, if your controls are public, you can manipulate them directly from other forms.

The techniques shown here make transferring data between forms easy for those who don’t have a lot of experience.  In the second part of this series, I’ll demonstrate another option that makes it easy for beginners but is not conducive to writing applications properly.  That technique is the use of so-called global variables.

Part 2 here
Part 3 here

4 comments:

Andrew said...

I just want to thank you so much for your post on VBForums in regards to creating tab delimited files.

I'm a school IT tech (also in Australia!) who needed to quickly create a VB.Net script to output data from AD into a tab delimited file - you saved my bacon, and you write fantastic code!

Sorry to post on an unrelated blog post - couldn't find any contact details for you.

Keep up the good work.

Jeroen said...

Hi Jim,

it's allready a whole time ago you posted this topic, and it worked for me, however on my form1 I have a print button, which prints form 2. All the data is entered in form 1 and saved in form 2 and printed.

However the form2.showdialog() shows form 2 with the new data, however I can't push the print button on form 1, can you help me with that?

Jeroen

jmcilhinney said...

Hi Jeroen. Thanks for your comment.

ShowDialog is specifically intended to display a modal dialogue. A modal dialogue, by definition, prevents you from accessing its caller until it is closed. You need to either not display the second form as a modal dialogue or else put the Print button on the second form.

If you want to go with the first option then you could display a modeless dialogue, which is an owned form in .NET parlance. The VS Find & Replace window is an example of a modeless dialogue, i.e. it always stays in front of its owner but doesn't prevent access to it and it is also minimised, restored and closed along with its owner. To display a modeless dialogue, call Show and pass the owner, which is generally Me, as an argument.

Meg Winters said...

Hi. I know this is a really old thread but I needed to have two peer controls communicate and your reply on VBForums was brilliant. Clear, complete, easy to follow.... Thank you!

NOT a member so I can't post or rate so... here I am.