Working with Multiple Forms in Visual Basic .NET: Upgrading to .NET
In Microsoft® Visual Basic® 6.0, if you had a second form (Form2) in your project, then displaying it was as easy as Form2.Show. That code is no longer correct in Visual Basic .NET due to some changes in how forms are handled. For programmers moving from an earlier version of Visual Basic to .NET, these changes can make the simple task of displaying a second form seem very difficult. In this article, I will illustrate how Visual Basic .NET forms differ from previous versions and how you can work with multiple forms within the new model.
Forms, in either version of Visual Basic, are essentially the same as any other class; they have properties, methods, and events, and you can create multiple instances of them. So, assuming you have a form in your project called Form2, the Visual Basic 6.0 code shown here creates three instances of that form and displays them all:
Dim myFirstForm As Form2 Dim mySecondForm As Form2 Dim myThirdForm As Form2 Set myFirstForm = New Form2 Set mySecondForm = New Form2 Set myThirdForm = New Form2 myFirstForm.Show mySecondForm.Show myThirdForm.Show
Now, other than the use of the keyword Set to assign new Form2 instances to your three variables, this code will also work in Visual Basic .NET, and both languages will display three copies of Form2 if this code is run. In this example, Form2 is behaving as a class, and you have to create an instance of a class before you can use it, but a special feature of Visual Basic prior to .NET allows you to work with Forms without creating an instance. This change in behavior in Visual Basic .NET can cause a great deal of confusion. In Visual Basic 6.0 and earlier versions, a special default instance of each form is automatically created for you, and allows you to use the form's name to access this instance. What this means is that the Visual Basic 6.0 code "Form2.Show" has the effect of showing the "default" instance of Form2, but it doesn't work at all in Visual Basic .NET. In .NET there is no default instance; Form2 refers only to the class that represents your form, and this class cannot be used without creating an instance.
That is the key difference between .NET and previous versions of Visual Basic—you need an instance of a form before you can display it or work with any of its controls or properties—but the second part of the problem is that these special default form instances are global to your entire project in Visual Basic 6.0. Taking these two facts together means that (in Visual Basic 6.0 or earlier) you can refer to any form in your project from anywhere in your code and you will always be referring to the same instance of that form. With the button's Click event on one of your forms you could show Form2 with "Form2.Show," and then set the text of a text box on Form2 from code in a module just like this:
Form2.TextBox1.Text = "Fred"
If you try to do the same type of code in Visual Basic .NET, you will run into the error message Reference to a Non-Shared Member Requires an Object Reference, which means that you tried to call a method or use a property of a class instead of an instance. A quick solution to this problem is to create an instance in every case where you are receiving the error message, turning:
Dim myForm2 As New Form2() myForm2.Show()
This will work in many cases, but if you had code somewhere else in your project that accessed that same default instance of Form2 and you fix it in the same way by turning:
Form2.TextBox1.Text = "Fred"
Dim myForm2 As New Form2() myForm2.TextBox1.Text = "Fred"
then you will have trouble, because this code has created a new instance of Form2; you are not working with the same instance you created earlier. You won't get any errors (there is nothing wrong with the preceding code), but you won't see any change on the instance of Form2 that you called Show() on earlier.
If you upgrade a Visual Basic 6.0 project, the Upgrade Wizard adds some special code to each of your forms to provide you with the default instance functionality that you were able to use in earlier versions of Visual Basic. This code, wrapped in a region labeled "Upgrade Support," adds a Shared property that returns an instance of the underlying form:
Private Shared m_vb6FormDefInstance As Form1 Private Shared m_InitializingDefInstance As Boolean Public Shared Property DefInstance() As Form1 Get If m_vb6FormDefInstance Is Nothing _ OrElse m_vb6FormDefInstance.IsDisposed Then m_InitializingDefInstance = True m_vb6FormDefInstance = New Form1() m_InitializingDefInstance = False End If DefInstance = m_vb6FormDefInstance End Get Set(ByVal Value As Form1) m_vb6FormDefInstance = Value End Set End Property
As a shared property, DefInstance is accessible using just the form's name, and the same instance of the form is returned for everyone using this class within a single application. With this code added to your form(s), you can write code that mimics the behavior of the Visual Basic 6.0 examples shown earlier by specifying Form2.DefInstance instead of just Form2.
With this code added, you can use Form2.DefInstance.Show() and then Form2.DefInstance.TextBox1.Text = "Fred" instead of directly referencing Form2. If you are not using the Upgrade Wizard you can achieve similar results by adding the code just shown (and the code from an upgraded form's New procedure, as that is also required) to your own Visual Basic .NET forms. Alternatively, though, if you are not upgrading existing code, you will be better off if you adjust your programming style to handle the lack of default instances for forms. The remainder of this article will focus on showing you how to do that.
If you are coming from earlier versions of Visual Basic it can be difficult to work without default instances of your forms. The following sections illustrate how to handle several specific scenarios and should help you with your own programming tasks.
Keeping a Reference Around
As mentioned earlier, the most important concept to understand about programming with forms is that you need an instance of a form before you can do anything with it, and if you want to use the same instance in multiple places then you will need to pass a reference to that instance around. For many Visual Basic 6.0 programmers, this is a totally new problem; the default instance provided for each form is available to all the code in your project. There are two main ways to handle your form reference(s): either make it globally available or pass it to each form, class, module, or procedure that requires it.
Global data in .NET
Earlier I mentioned that global variables are not available in Visual Basic .NET, and now I'm going to tell you how to make something global. How will you ever trust me after this? Actually, I am not being dishonest in either case; global variables are not allowed in Visual Basic .NET, but you can achieve similar functionality using Shared (called static in C#) class members. A Shared class member, as used by the Visual Basic Upgrade Wizard when it adds the DefInstance property to your forms, is available without creating an instance of a class and, if it is a property, its value is shared across your entire application. You could therefore create a class like this:
Public Class myForms Private Shared m_CustomerForm As CustomerForm Public Shared Property CustomerForm() As CustomerForm Get Return m_CustomerForm End Get Set(ByVal Value As CustomerForm) m_CustomerForm = Value End Set End Property End Class
When you first create an instance of your form you could store it into this class:
Dim myNewCust As New CustomerForm() myNewCust.Show() myForms.CustomerForm = myNewCust
After the CustomerForm property has been populated with an instance of your form, you could then use this class to access that same instance from anywhere in your code:
Module DoingStuffWithForms Sub DoExcitingThings() myForms.CustomerForm.Text = _ DateTime.Now().ToLongTimeString End Sub End Module
Storing your form in this manner comes as close to Visual Basic 6.0's Global variables as you are going to get. The next level of variable scope below this level is class (module, class or form, actually) scope, where a variable is declared within a class and is available to any code in that class, and below that is procedure scope, where a variable is local to a single routine.
Passing your form around
As an alternative to making your form global, you could instead keep a reference to the form as a variable in your form or class and then pass that reference to any code that needs access to your form. If you had a form (called Form1 in this example) and you wanted to open a second form (Form2) when a button was clicked, and then do some calculations on this new form in response to another button being clicked, your code could be written within Form1, like this:
Public Class Form1 Inherits System.Windows.Forms.Form Dim myForm2 As Form2 Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click myForm2 = New Form2() myForm2.Show() End Sub Private Sub Button2_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button2.Click Calculations.CompoundInterestCalc(myForm2) End Sub End Class
Either method—global references or passing form instances as arguments—will work, but you should pick a method best suited for your particular project. In a situation where only a few procedures will need access to your form after creating it, I would tend towards having a form parameter on those procedures and passing your form reference in as needed. If a form is being used by many procedures throughout your project, a global reference might be in order, but consider restructuring your application so that only a single class or procedure actually needs access to your form. If you were using your form as a place to write out logging information, for instance, you could create a class that exposed the form as a shared member, but also provided a shared WriteToLogWindow method that handled the actual interaction with the form. All of your code would refer to that WriteToLogWindow method instead of accessing the form directly:
Public Class Log Private Shared m_LogForm As Form2 Public Shared Property LogForm() As Form2 Get Return m_LogForm End Get Set(ByVal Value As Form2) m_LogForm = Value End Set End Property Public Shared Sub WriteToLogWindow(ByVal Message As String) Dim sb As New _ StringBuilder(m_LogForm.txtLogInfo.Text) sb.Append(Environment.NewLine) sb.Append(Message) m_LogForm.txtLogInfo.Text = sb.ToString() End Sub End Class
Getting Information into and out of Forms
Thus far in this article I have covered how to obtain and keep track of a form instance, but I haven't discussed how you would get information into and out of form. If you have a form instance, and if your code and your form are from the same project, you can access the controls on a Form directly, but I don't think that is the best idea. Instead of working with the text boxes, buttons, and other controls on your form, I suggest creating properties that are explicitly made Public and allow you to set and retrieve the values you wish to access. Follow me through a quick example if you wish to try out this method of working with a form:
- Create a new Windows Application project in Visual Basic .NET.
- One form, Form1, will be created automatically. Add another by right-clicking your project in the Solution Explorer and selecting Add Windows Form. Accept the default name of Form2.vb and click OK.
- Add two buttons to Form1, leave them with their default names of Button1 and Button2, and reposition them enough so that they are not overlapping.
- Add a single text box to Form2, leaving it with the default name of TextBox1.
- Now add this code to Form2 (by right-clicking Form2 in the Solution Explorer and selecting View Code) immediately before "End Class."
Public Property CustomerName() As String Get Return TextBox1.Text End Get Set(ByVal Value As String) TextBox1.Text = Value End Set End Property
To do this:
- View the code for Form1 and add the following line after "Inherits System.Windows.Forms.Form":
Dim myForm2 As New Form2()
- Double-Click Button1 on Form1 to access the button's Click event handler and enter this code:
myForm2.CustomerName = "Fred" myForm2.Show()
- Double-Click Button2 on Form1 and enter this code:
MessageBox.Show(myForm2.CustomerName) myForm2.CustomerName = "Joe"
- Run the project (F5), and then click Button1 and Button2 to see the code in action.
- View the code for Form1 and add the following line after "Inherits System.Windows.Forms.Form":
Having a CustomerName property might not seem like a big deviation from accessing the text box on Form2 directly, but you gain a few benefits from interacting with your forms in this way. The main benefit is abstraction; you don't have to know anything about the controls on Form2 (there might not even be a text box on it), just set and retrieve the CustomerName property. Having this layer of abstraction allows Form2 to change its implementation without any effect on the rest of your code, making future modifications much easier. In our example, the property-based version wasn't any easier to use, but if you are dealing with a more complex user interface, the code behind your properties could handle all the details while the code to use the form stayed very simple. Of course, one final benefit of this model is less tangible but certainly has value to many developers: Using properties instead of directly accessing the controls is much more elegant and results in code that is easier to understand.
Visual Basic .NET gets rid of the "default instance" used by forms in earlier versions, and that change can cause a great deal of confusion to those trying to learn how to program in .NET. You need an instance of a form before you can access its properties, methods, or any of its controls, and you need that instance stored in such a way that it is available throughout your code. The .NET system for handling forms is more consistent and more powerful than the system in Visual Basic 6.0, but even changes for the better can cause confusion for those trying to make the transition.