Tuesday, April 3, 2012

Managing Data Among Multiple Forms (Part 2)

Part 1 here

In part 1 of this series, I looked at using default instances in VB and how that gives you direct access to controls on any form, anywhere, any time.  I also showed how you could simulate default instances in C#.  Default instances are a handy feature for beginners but I wouldn’t recommend their use to anyone who’s serious about programming.  They’re an easy way to start out though.

This instalment will describe the use of what’s commonly as global variables.  This is another technique that makes life easier for beginners but should generally be avoided because it’s not really architecturally sound.  I’ll cover it here though for completeness and because it is also an easy way to start out, when most of what you do will not be architecturally sound.

As the name suggests, global variables are intended to give global access, i.e. you can access them anywhere, any time.  Just like default form instances, the idea is that you don’t have to create an object explicitly in order to access them, which means that you’re not limited to accessing that object only where it’s created.  There are various ways that that can be achieved, but the simplest is to use a module in VB or the equivalent in C#, which is a static class.

We’ll use basically the same example as last time, transferring text from a TextBox on one form to a TextBox on another form and then back again.  This time though, instead of one form passing data to and retrieving data from the other form, each will deal only with the global variable.

So, first things first, let’s declare that global variable.  In VB, add a new module to your project and add the following code:

Module Module1

    'The text that will be transferred between TextBoxes.
    Public textBoxText As String

End Module
In C#, add a new class to your project and add the following code:

static class Class1
{
    // The text that will be transferred between TextBoxes.
    public static string textBoxText;
}
Notice the addition of the ‘static’ keyword to both the class and the field.  A static member is one that is a member of the class itself rather than each specific instance and a static class is one that can only have static members.  The class itself doesn’t actually have to be static but it’s the correct thing to do.
Set up the user interface as before, with a TextBox and a Button on Form1 and a TextBox on Form2.  Create a Click event handler for the Button on Form1 and add the following code:

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

    'Show the second form as a modal dialogue.
    Using dialogue As New Form2
        dialogue.ShowDialog()
    End Using

    'Pull the data from the global variable to the TextBox on this form.
    Me.TextBox1.Text = textBoxText
End Sub
C#
private void button1_Click(object sender, EventArgs e)
{
    // Push the data to the global variable from the TextBox on this form.
    Class1.textBoxText = this.textBox1.Text;

    // Show the second form as a modal dialogue.
    using (Form2 dialogue = new Form2())
    {
        dialogue.ShowDialog();
    }

    // Pull the data from the global variable to the TextBox on this form.
    this.textBox1.Text = Class1.textBoxText;
}
Note that in both VB and C# I have included a ‘Using’/’using’ statement.  This is to create the dialogue and destroy it again after it’s been used.  I’m not using a default instance in the VB code to show that this technique of using global variables doesn’t depend on them.  Also note that the global variable is qualified with the class name in the C# code but it is not qualified with the module name in the VB code.  You can qualify module members in VB if you want to but the module name can be omitted, which is behaviour consistent with modules in VB6.

Now, create handlers for the Load and FormClosed events in Form2 and add this code:

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

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

C#
private void Form2_Load(object sender, EventArgs e)
{
    // Pull the data from the global variable to the TextBox on the this form.
    this.textBox1.Text = Class1.textBoxText;
}

private void Form2_FormClosed(object sender, FormClosedEventArgs e)
{
    // Push the data to the global variable from the TextBox on this form.
    Class1.textBoxText = this.textBox1.Text;
}
That’s all we need.  You can now run the project and do as for the previous example.  Enter some text in the TextBox on Form1 and then click the Button and you’ll see that text appear in the TextBox on Form2.  Edit that text and close the form and you’ll see the new text appear in the TextBox on the first form.

Looking back at the code, you can see that neither form refers directly to the TextBox on other.  In fact, there’s no reference to Form1 at all in Form2.  Both forms use the global variable as an intermediary, with each form having no specific dependency on the other.

That’s it for this instalment on global variables.  In the next instalment, I’ll start looking at how to do things the “proper” way.

Part 3 here

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