This is the simplest interface you can present to the user. In a complicated program, however, it can also be the most confusing.
In Chapter 2 I showed you an employee editing form. It was a single-screen program where all data was entered on just one screen. Suppose this Employee screen was part of a bigger HR program that also included screens for training, payroll, and scheduling. An SDI program requires that you close one screen before opening another. There would be no way to just flip back and forth between screens. This is the classic definition of an SDI program. Here are some common examples:
Notepad: You must close one document before editing another.
WordPad: You must close one document before editing another.
Calculator: You can use only one calculator at a time.
In a classic SDI program such as Notepad, if you want to edit two documents at once you need to run two instances of the program. Neither instance of Notepad knows the other exists, and they do not interact. Figure 3-1 shows what a classic SDI HR application that includes an Employee screen might look like.
The Employee screen in Figure 3-1 is rather simplistic. In order for the user to go to the training screen, the program would have to replace the Employee screen you see here. Although this works, it is limiting.
Another type of SDI program is common these days. I call it the scatter effect program. In the case of the hypothetical HR program shown in Figure 3-1, if the user chose the Training module while in the Employee screen, the program would not replace the Employee screen. It would instead bring up another form on the desktop, which would look like a separate program altogether. However, it is not. The different screens can interact with each other and exchange information.
In this scenario, the user can have four forms open on the desktop, one for each module (Employee, Training, Payroll, and Scheduling). Can you imagine a program that could have a dozen or more forms open on the desktop at once? To me, this is chaos, but I bet you have already used a program that does this.
VB 6.0 has an option to work in an SDI environment or an MDI environment. The default is MDI, and this keeps things organized. I have a coworker, though, who likes the SDI environment. This means that each window is a separate screen, including all the forms, classes, and modules he has open. I frequently see him with a few dozen forms scattered all over his desktop.
If you come from the MSVC++ world or you have not programmed yet, you may not have seen this effect in VB 6.0. However, you do see it in Microsoft Word. Word used to have an MDI interface. It used to be that you could open up Word and then open as many documents as you wanted inside Word. Word is arguably the most ubiquitous program in the world, and I bet many people like to edit many documents at once. Word is now an SDI application, which means that each document is in a separate window. In my opinion, all this does is clutter up the taskbar.
This next example shows you quite a bit about how SDI programs work. It includes five screens. There is a main screen that lets you choose to edit different parts of an HR program. I will be working from the example shown in Figure 3-1.
Before you attack the coding of an SDI program, you need to come up with a plan for how it will work. My plan for this one is based loosely on how Microsoft Word works:
The program has a main form with a menu.
The menu allows the user to choose different parts of the HR program to work on.
Each part of the HR program is a new form that appears on the desktop in a random place.
The main form has a Window menu option that shows all the HR screens the user has open.
The Window option has a submenu item called "Close All Windows" that closes all the currently open windows.
The Window menu option of the main screen denotes with a check mark the child form that is currently in focus.
If the user chooses one of the forms listed under the Window menu option, that form gains focus.
Only one instance of any of the HR forms can be running at a time.
If the user chooses to edit a form that exists on the desktop, that form will gain focus.
If the user closes an existing form, that form is deleted from the Window menu option.
If the user closes the main form, the open windows automatically shut down.
This list does not even include any data entry or validation requirements. This example is just to show you what is involved in keeping track of SDI child forms in an SDI environment.
Note |
Because I write examples in this book in both VB and C#, I append my example names with either –vb or –c. You do not have to do this, of course. |
Start a new C# or VB Windows project. Mine is called "SDISample." You will need to follow these steps before you add any code:
Add a MainMenu to the form.
Type File in the MainMenu item and call it mnuFile.
Below mnuFile, type in Close and call this item mnuClose.
Next to mnuFile, type in Edit and call this item mnuEdit.
Below mnuEdit, type in Employee and call this item mnuEmp.
Below mnuEmp, type in Training and call this item mnuTrain.
Below mnuTrain, type in Payroll and call this item mnuPayRoll.
Below mnuPayRoll, type in Scheduling and call this item mnuSked.
Next to mnuEdit, type in Window and call this item mnuWindow.
Next to mnuWindow, type in Help and call this item mnuHelp.
Add a status bar to the form.
Add a Panel to the Panels collection in the status bar and make it read Employee Screen. Change the AutoSize property to Spring.
Add a Panel to the Panels collection in the status bar and make it read Operator:. Change the AutoSize property to Contents.
Add a Panel to the Panels collection in the status bar and type in the date. Change the AutoSize property to Contents.
Set the ShowPanels property to true.
Make the form start in the center of the screen.
Your form should look like the one shown in Figure 3-2.
OK, you have this form—now for the others. First up is the Employee form. This one looks just like the form shown in Figure 3-1. Follow these steps:
Add a CheckedListBox with several items in the Items collection.
Add a Button whose text reads Add Employee.
Add a Button whose text reads Fire Employee.
Add a Button whose text reads Randomly Demote.
Add a Button whose text reads OK.
Add a Button whose text reads Cancel.
That's it for this form; you will not add any code to this form. The form should look like the one shown in Figure 3-3.
The other three forms are really easy to create. They do not include any controls. Although it may seem boring, this example is meant to show an SDI project as a whole. Besides, as you will see, a form without any controls behaves slightly differently from one with controls.
Perform the following steps to finish the form setup for this project:
Add a Windows form called Payroll and change the text to Payroll.
Add a Windows form called Training and change the text to Training.
Add a Windows form called Scheduling and change the text to Scheduling.
That's it. Now it's time to add the code.
There are a few ways you can accomplish the objectives set out for this SDI project. For instance, you can give each child form a reference to the main form whenever it is called. This way, you can communicate back and forth to see what the state of the child form is. The states you are interested in are as follows:
Is the child form active?
Is the child form in focus?
Is the parent form active?
It would not be too hard to have the child form tell the main form about its state. There is a better way, though. It is possible for the main form to keep track of everything about the child form without having to write any code at all in the child form. You do this using delegates.
Before I get into the code, I want you to look in the online help for the Form class. Look at all the events that can occur when just about anything happens to the form. Decide which events you can use and see if you come up with the same list as I have.
Note |
It is very important that you understand how delegates work. You will see me adding new event handlers to events that happen in the child forms. You must understand that I am not replacing the event handler for these forms with my own delegates. I am extending the list of delegates that get called when this event happens. The base event handler for any event always occurs first, and you can override this if you want to. |
There is something else I would like to point out before I start with the code. Usually you will see delegates being assigned as part of the initialization of a form. Most of the time, a programmer knows which delegates need to be assigned to which events at compile time.
I have done things a little differently in this project. The delegates get assigned to events dynamically. Because the main form can call up any child form at any time, this program needs to be a bit more reactive.
Anyway, here is the code. Get into the code pane of the main form. You will need to assign some class local variables, and you will put this section just above the form's constructor. The code for this follows.
C#
#region Class Local Variables Payroll PayForm; Employee EmpForm; Scheduling SkedForm; Training TrainForm; #endregion
VB
#Region "Class Local Variables" Dim PayForm As Payroll Dim EmpForm As Employee Dim SkedForm As Scheduling Dim TrainForm As Training #End Region
As you can see, you are keeping track of the forms via these variables. Basically, if the variable is null, the form does not exist—yet.
Before you code the constructor, you will need to add the delegates. Listings 3-1a and 3-1b show the delegate code for this project.
![]() |
#region Menu delegates private void CloseMe(object sender, EventArgs e) { this.Close(); } private void OpenWindow(object sender, EventArgs e) { MenuItem m; if(sender is MenuItem) m = (MenuItem)sender; else return; if(m == mnuEmp) { if(EmpForm == null) { EmpForm = new Employee(); EmpForm.Load += new EventHandler(this.ListWindows); EmpForm.GotFocus += new EventHandler(this.ListWindows); EmpForm.Disposed += new EventHandler(this.ByByWindow); EmpForm.Show(); } else EmpForm.Focus(); } if(m == mnuTrain) { if(TrainForm == null) { TrainForm = new Training(); TrainForm.Load += new EventHandler(this.ListWindows); TrainForm.GotFocus += new EventHandler(this.ListWindows); TrainForm.Disposed += new EventHandler(this.ByByWindow); TrainForm.Show(); } else TrainForm.Focus(); } if(m == mnuPayroll) { if(PayForm == null) { PayForm = new Payroll(); PayForm.Load += new EventHandler(this.ListWindows); PayForm.GotFocus += new EventHandler(this.ListWindows); PayForm.Disposed += new EventHandler(this.ByByWindow); PayForm.Show(); } else PayForm.Focus(); } if(m == mnuSked) { if(SkedForm == null) { SkedForm = new Scheduling(); SkedForm.Load += new EventHandler(this.ListWindows); SkedForm.GotFocus += new EventHandler(this.ListWindows); SkedForm.Disposed += new EventHandler(this.ByByWindow); SkedForm.Show(); } else SkedForm.Focus(); } } private void ListWindows(object sender, EventArgs e) { mnuWindow.MenuItems.Clear(); mnuWindow.MenuItems.Add("Close All", new EventHandler(this.CloseAllWindows)); mnuWindow.MenuItems.Add("-"); if(EmpForm != null) mnuWindow.MenuItems.Add(EmpForm.Text, new EventHandler(this.FocusForm)); if(PayForm != null) mnuWindow.MenuItems.Add(PayForm.Text, new EventHandler(this.FocusForm)); if(TrainForm != null) mnuWindow.MenuItems.Add(TrainForm.Text, new EventHandler(this.FocusForm)); if(SkedForm != null) mnuWindow.MenuItems.Add(SkedForm.Text, new EventHandler(this.FocusForm)); foreach(MenuItem mnu in mnuWindow.MenuItems) { if(EmpForm != null && sender == EmpForm && mnu.Text == EmpForm.Text) { mnu.Checked = true; break; } if(PayForm != null && sender == PayForm && mnu.Text == PayForm.Text) { mnu.Checked = true; break; } if(TrainForm != null && sender == TrainForm && mnu.Text == TrainForm.Text) { mnu.Checked = true; break; } if(SkedForm != null && sender == SkedForm && mnu.Text == SkedForm.Text) { mnu.Checked = true; break; } } } private void FocusForm(object sender, EventArgs e) { MenuItem m; if(sender is MenuItem) m = (MenuItem)sender; else return; foreach(MenuItem mnu in mnuWindow.MenuItems) mnu.Checked = false; if(EmpForm != null && m.Text == EmpForm.Text) EmpForm.Focus(); if(PayForm != null && m.Text == PayForm.Text) PayForm.Focus(); if(TrainForm != null && m.Text == TrainForm.Text) TrainForm.Focus(); if(SkedForm != null && m.Text == SkedForm.Text) SkedForm.Focus(); m.Checked = true; } private void CloseAllWindows(object sender, EventArgs e) { if(EmpForm != null) EmpForm.Dispose(); if(PayForm != null) PayForm.Dispose(); if(TrainForm != null) TrainForm.Dispose(); if(SkedForm != null) SkedForm.Dispose(); } private void ByByWindow(object sender, EventArgs e) { if(sender == EmpForm) EmpForm = null; if(sender == PayForm) PayForm = null; if(sender == TrainForm) TrainForm = null; if(sender == SkedForm) SkedForm = null; ListWindows(null, null); } #endregion
![]() |
![]() |
#Region "Menu delegates" Private Sub CloseMe(ByVal sender As Object, ByVal e As EventArgs) Me.Close() End Sub Private Sub OpenWindow(ByVal sender As Object, ByVal e As EventArgs) Dim m As MenuItem If sender.GetType() Is GetType(MenuItem) Then m = CType(sender, MenuItem) Else Return End If If m Is mnuEmp Then If EmpForm Is Nothing Then EmpForm = New Employee() AddHandler EmpForm.Load, AddressOf Me.ListWindows 'AddHandler EmpForm.GotFocus, AddressOf Me.ListWindows AddHandler EmpForm.Click, AddressOf Me.ListWindows AddHandler EmpForm.Disposed, AddressOf Me.ByByWindow EmpForm.Show() Else EmpForm.Focus() End If End If If m Is mnuTrain Then If TrainForm Is Nothing Then TrainForm = New Training() AddHandler TrainForm.Load, AddressOf Me.ListWindows 'AddHandler TrainForm.GotFocus, AddressOf Me.ListWindows AddHandler TrainForm.Click, AddressOf Me.ListWindows AddHandler TrainForm.Disposed, AddressOf Me.ByByWindow TrainForm.Show() Else TrainForm.Focus() End If End If If m Is mnuPayroll Then If PayForm Is Nothing Then PayForm = New Payroll() AddHandler PayForm.Load, AddressOf Me.ListWindows 'AddHandler PayForm.GotFocus, AddressOf Me.ListWindows AddHandler PayForm.Click, AddressOf Me.ListWindows AddHandler PayForm.Disposed, AddressOf Me.ByByWindow PayForm.Show() Else PayForm.Focus() End If End If If m Is mnuSked Then If SkedForm Is Nothing Then SkedForm = New Scheduling() AddHandler SkedForm.Load, AddressOf Me.ListWindows 'AddHandler SkedForm.GotFocus, AddressOf Me.ListWindows AddHandler SkedForm.Click, AddressOf Me.ListWindows AddHandler SkedForm.Disposed, AddressOf Me.ByByWindow SkedForm.Show() Else SkedForm.Focus() End If End If End Sub Private Sub ListWindows(ByVal sender As Object, ByVal e As EventArgs) Dim mnu As MenuItem mnuWindow.MenuItems.Clear() mnuWindow.MenuItems.Add("Close All", _ New EventHandler(AddressOf Me.CloseAllWindows)) mnuWindow.MenuItems.Add("-") If Not EmpForm Is Nothing Then mnuWindow.MenuItems.Add(EmpForm.Text, _ New EventHandler(AddressOf Me.FocusForm)) End If If Not PayForm Is Nothing Then mnuWindow.MenuItems.Add(PayForm.Text, _ New EventHandler(AddressOf Me.FocusForm)) End If If Not TrainForm Is Nothing Then mnuWindow.MenuItems.Add(TrainForm.Text, _ New EventHandler(AddressOf Me.FocusForm)) End If If Not SkedForm Is Nothing Then mnuWindow.MenuItems.Add(SkedForm.Text, _ New EventHandler(AddressOf Me.FocusForm)) End If For Each mnu In mnuWindow.MenuItems If Not EmpForm Is Nothing And sender Is EmpForm AndAlso _ mnu.Text = EmpForm.Text Then mnu.Checked = True Exit For End If If Not PayForm Is Nothing And sender Is PayForm AndAlso _ mnu.Text = PayForm.Text Then mnu.Checked = True Exit For End If If Not TrainForm Is Nothing And sender Is TrainForm AndAlso _ mnu.Text = TrainForm.Text Then mnu.Checked = True Exit For End If If Not SkedForm Is Nothing And sender Is SkedForm AndAlso _ mnu.Text = SkedForm.Text Then mnu.Checked = True Exit For End If Next End Sub Private Sub FocusForm(ByVal sender As Object, ByVal e As EventArgs) Dim m As MenuItem Dim mnu As MenuItem If sender.GetType() Is GetType(MenuItem) Then m = CType(sender, MenuItem) Else Return End If For Each mnu In mnuWindow.MenuItems mnu.Checked = False Next If Not EmpForm Is Nothing AndAlso m.Text = EmpForm.Text Then EmpForm.Focus() End If If Not PayForm Is Nothing AndAlso m.Text = PayForm.Text Then PayForm.Focus() End If If Not TrainForm Is Nothing AndAlso m.Text = TrainForm.Text Then TrainForm.Focus() End If If Not SkedForm Is Nothing AndAlso m.Text = SkedForm.Text Then SkedForm.Focus() End If m.Checked = True End Sub Private Sub CloseAllWindows(ByVal sender As Object, ByVal e As EventArgs) If Not EmpForm Is Nothing Then EmpForm.Dispose() End If If Not PayForm Is Nothing Then PayForm.Dispose() End If If Not TrainForm Is Nothing Then TrainForm.Dispose() End If If Not SkedForm Is Nothing Then SkedForm.Dispose() End If End Sub Private Sub ByByWindow(ByVal sender As Object, ByVal e As EventArgs) If sender Is EmpForm Then EmpForm = Nothing End If If sender Is PayForm Then PayForm = Nothing End If If sender Is TrainForm Then TrainForm = Nothing End If If sender Is SkedForm Then SkedForm = Nothing End If ListWindows(Nothing, Nothing) End Sub #End Region
![]() |
Now that you have entered the delegate code, you can enter the code needed in the constructor.
C#
public Form1() { InitializeComponent(); mnuClose.Click += new EventHandler(this.CloseMe); mnuEmp.Click += new EventHandler(this.OpenWindow); mnuSked.Click += new EventHandler(this.OpenWindow); mnuPayroll.Click += new EventHandler(this.OpenWindow); mnuTrain.Click += new EventHandler(this.OpenWindow); }
Public Sub New() MyBase.New() 'This call is required by the Windows Form Designer. InitializeComponent() AddHandler mnuClose.Click, New EventHandler(AddressOf Me.CloseMe) AddHandler mnuEmp.Click, New EventHandler(AddressOf Me.OpenWindow) AddHandler mnuSked.Click, New EventHandler(AddressOf Me.OpenWindow) AddHandler mnuPayroll.Click, New EventHandler(AddressOf Me.OpenWindow) AddHandler mnuTrain.Click, New EventHandler(AddressOf Me.OpenWindow) End Sub
This code is exceeding simple considering what this program can do. As I said before, the bulk of the code is dynamic.
If you can hang on a minute before you run the program, I need to explain what is going on in the code. You will get a much better appreciation of it that way. First, look at the OpenWindow delegate. This method is meant to be called by a menu item only.
Note |
Because I programmed the OpenWindow delegate, I know this method is meant to be called by a menu item only. You cannot always count on this, however. Someone else may accidentally copy and paste a section of code that uses this delegate for a Button Click event. (The delegate signature is the same.) It is best to test the Sender object before you actually do anything. |
The first thing I do is test to make sure that the delegate was called by a MenuItem. Once I know that it is, I assign an internal variable that I can work with by casting the Sender object. Here is the code to do this:
MenuItem m; if(sender is MenuItem) m = (MenuItem)sender; else return;
Tip |
This code should never fail. If it does fail, you have a programming error. Instead of quietly exiting like I do here, you should throw an exception for debugging purposes. |
Now for the meat of this delegate. The following code is repeated in this method for each child form:
if(m == mnuEmp) { if(EmpForm == null) { EmpForm = new Employee(); EmpForm.Load += new EventHandler(this.ListWindows); EmpForm.GotFocus += new EventHandler(this.ListWindows); EmpForm.Disposed += new EventHandler(this.ByByWindow); EmpForm.Show(); } else EmpForm.Focus(); }
I check to see if the user clicked the Employee form menu item. (Reflection in .NET is a wonderful thing.)
Note |
I show the use here of combining several events into a single delegate. In some cases, this is appropriate, but in others, it is not. I prefer to handle each event with its own delegate. If there is common code, I break it out into a helper function that gets called by each delegate. Among other things, doing this makes the delegate code shorter and easier to read. |
Once I know this, I test to see if the form already exists on the desktop. If it does, I bring it into focus. This is the first of two ways this example programmatically brings a form into focus.
If the form does not exist yet, I instantiate it, assign a few delegates, and show the form nonmodally. Notice that I use the same delegate for the Load event and the GotFocus event. The ListWindows delegate tears down and reconstructs the window list according to the open forms.
I chain the disposed event of the form so I can reset the form-variable to null. This is how I keep only one instance of a particular form open at a time.
Now let's look at the ListWindows delegate. For clarity's sake, I don't bother testing for the correct Sender object here. The first thing I do is clear the window list of submenu items. I then add the standard Close All menu item to close all windows and a separator for the actual list of windows.
For each of the forms that is open (as determined by the form references), I add a menu item list to the window list. While I do this, I also dynamically assign the FocusForm delegate to each item in the list.
Once I have the list, I check the Sender object to see which form was instantiated (remember, I chain the Load event for each form to this delegate). The form that was instantiated (or received focus) gets its list item checked. This is how most window lists work. It allows you to see which window is currently active.
This delegate is called anytime a form is instantiated via the form's Load event. It is also called each time the form comes in focus via the GotFocus event. So in theory, if I click a form on the desktop, the window list will reflect this by placing a check mark next to that form.
Notice the delegate that I assign to each menu item in the form window list. It is the FocusForm delegate. Here it is again:
private void FocusForm(object sender, EventArgs e) { MenuItem m; if(sender is MenuItem) m = (MenuItem)sender; else return; foreach(MenuItem mnu in mnuWindow.MenuItems) mnu.Checked = false; if(EmpForm != null && m.Text == EmpForm.Text) EmpForm.Focus(); if(PayForm != null && m.Text == PayForm.Text) PayForm.Focus(); if(TrainForm != null && m.Text == TrainForm.Text) TrainForm.Focus(); if(SkedForm != null && m.Text == SkedForm.Text) SkedForm.Focus(); m.Checked = true; }
Read the code and see if you can guess what it does. It is simple enough. This delegate is called when a user clicks a form name in the window list. It unchecks all items in the list, tests which name was clicked, and brings that form into focus. This is the second way to bring an existing form into focus via code.
Let's recap what happens in this example:
A user brings up a form via the Edit menu.
The form is put into the window list and is defined as being in focus by the check mark next to its name.
The user brings up the rest of the forms via the menu list.
All the open forms are listed in the window list. The last one up is in focus.
The user tries to bring up a form that already exists on the desktop. This is not allowed, but the form the user wanted is brought into focus. This is reflected in the window list.
The user chooses another form in the window list that is not checked. This other form is brought into focus, and the window list now reflects this.
The user clicks a form on the window, bringing it into focus. The window list reflects this.
The user closes a form on the desktop. The window list reflects this, and the user is now able to instantiate this form again via the Edit menu.
The user chooses the Close All menu item and all the open windows close. The window list reflects this and the user is able to instantiate all forms again via the Edit menu.
The user closes the main menu and the open child forms also close.
Step 10 is accomplished via garbage collection because the form reference variables I have get disposed during the Dispose event for the main form.
OK, now you can run the program and try this out. Test all the items in the previous list and see if you can find the bug. Found it yet?
Open all the forms via the Edit menu. Click each form and check the window list on the main form. The window list should reflect the current child form that is in focus. This works for all forms except the Employee form.
What gives? I assigned the GotFocus event to the correct delegate. Every other form works. The problem is one of the chicken or the egg. The only difference between the Employee form and the others is that the Employee form has controls. Try this: Put a button on the Payroll form and run the program again. You will see that now the Payroll form acts like the Employee form.
The reason is that the form never gets focus. As soon as there is a single control on the form, that control receives the focus. This is, of course, provided that the control can receive focus. So how do you get around this?
Replace the code that assigned the GotFocus event of each form to the ListWindows delegate with the form's click event. Your code should look like this:
EmpForm.Activated += new EventHandler(this.ListWindows);
Do this for each form. This simple change will make the whole system work as it should. Figure 3-4 shows my desktop with all windows open.
So that is the SDI project. In this section I covered one of the two ways you can present data entry forms to a user. I explain the other way in the next section.
I would like you to note one thing about the VB code as opposed to the C# code. Consider these lines of code:
C#
if(EmpForm != null && m.Text == EmpForm.Text) EmpForm.Focus();
VB
If Not EmpForm Is Nothing AndAlso m.Text = EmpForm.Text Then EmpForm.Focus() End If
In standard VB 6.0 there is no such thing as the keyword AndAlso. In fact, most VB 6.0 programmers would use the keyword And instead. But then again, anyone who has programmed in VB knows that using the keyword And in this case creates a bug.
VB has the unfortunate tendency to evaluate a complete If statement before determining its Boolean value. C++, C, and C# do not do this. These languages allow you to short-circuit a multistatement line. What I mean by this is that if you are testing two statements on one line such as "if A is false and B is false," C# will not test for "B is false" if A turns out to be false. This allows the programmer to write code that, if evaluated, would fail under certain circumstances but not others. This is OK if the first test always fails and if the second test cannot be evaluated.
VB tests both statements before deciding if it should go on. So in this case, if EmpForm equaled nothing, the program would crash on testing the text value of EmpForm, even though technically the If statement already failed on the first test.
This was incredibly annoying[2] in VB 6.0 because it forced you to nest If-Then statements. For years I have been waiting for VB to change to the way C++ worked in this manner. Well, the original beta of VB .NET did change the AND keyword to work properly, but VB programmers complained and Microsoft capitulated. So Microsoft added the keyword AndAlso, which I guess satisfies both camps.
Use the AndAlso keyword often. It makes for more compact and faster running code.
[2]Soapbox time!