Windows Forms 2.0 Programming (Microsoft .NET Development Series)

By default, each EXE is an application that has an independent lifetime, even if multiple instances of the same application are running at the same time. However, it's common to want to limit an EXE to a single instance, whether it's an SDI application with a single top-level window, an MDI application, or an SDI application with multiple top-level windows. All these kinds of applications require that another instance detect the initial instance and then cut its own lifetime short.

Single-Instance Detection and Management

You could build a custom single-instance application using custom code that incorporates threading and .NET remoting. However, the VB.NET runtime library, Microsoft.VisualBasic.dll, contains a class that provides such an implementation for you: WindowsFormsApplicationBase, located in the Microsoft.VisualBasic.ApplicationServices namespace.[5] WindowsFormsApplicationBase does not inherit from the Application class in System.Windows.Forms, but WindowsFormsApplicationBase is designed to replace the use of the Application class to run and manage an application's lifetime, as you'll see shortly.

[5] It's difficult to determine why this nice feature wasn't folded into the .NET Framework, which would explicitly expose it to all languages. However, Microsoft.VisualBasic.dll ships with the .NET Framework, so it's available to any .NET language, in spite of its name.

If you are using C#, you add a reference to this assembly by right-clicking the project and selecting Add Reference from the context menu. From the .NET tab of the subsequently loaded Add Reference dialog, select Microsoft.VisualBasic.dll. When this DLL is referenced, you derive from WindowsFormsApplicationBase before extending your custom class with support for single-instance applications and passing command line arguments:

// SingleInstanceApplication.cs using Microsoft.VisualBasic.ApplicationServices; ... class SingleInstanceApplication : WindowsFormsApplicationBase {...}

Next, you configure SingleInstanceApplication to support single-instance applications. Set the SingleInstanceApplication class's IsSingleInstance property (implemented by the base WindowsFormsApplicationBase class) to true:

// SingleInstanceApplication.cs class SingleInstanceApplication : WindowsFormsApplicationBase { // Must call base constructor to ensure correct initial // WindowsFormsApplicationBase configuration public SingleInstanceApplication() { // This ensures the underlying single-SDI framework is employed, // and OnStartupNextInstance is fired this.IsSingleInstance = true; } }

IsSingleInstance is false by default, and the constructor is a great place to change this situation. To incorporate this into your application, replace the standard application startup logic from your application's entry point. Then, use the following code to create an instance of your custom WindowsFormsApplicationBase type:

// Program.cs static class Program { [STAThread] static void Main(string[] args) { Application.EnableVisualStyles(); SingleInstanceApplication application = new SingleInstanceApplication(); application.Run(args); } }

WindowsFormsApplicationBase exposes the Run methodthe Application.Run method analogwhich you invoke to open the main application form. Additionally, WindowsFormsApplicationBase.Run expects a string array containing command line arguments; passing null causes an exception to be thrown.

To specify which form is the main application form, you override WindowsFormsApplicationBase.OnCreateMainForm and set WindowsFormsApplicationBase.MainForm appropriately:

// SingleInstanceApplication.cs class SingleInstanceApplication : WindowsFormsApplicationBase { ... protected override void OnCreateMainForm() { this.MainForm = new MainForm(); } }

As a final flourish, you can expose your custom WindowsFormsApplicationBase type via a static instantiation-helper method and thereby cut down on client code:

// SingleInstanceApplication.cs class SingleInstanceApplication : WindowsFormsApplicationBase { static SingleInstanceApplication application; internal static SingleInstanceApplication Application { get { if( application == null ) { application = new SingleInstanceApplication(); } return application; } } ... } // Program.cs static class Program { ... [STAThread] static void Main(string[] args) { Application.EnableVisualStyles(); SingleInstanceApplication.Application.Run(args); } }

The effect of SingleInstanceApplication is to restrict an application to only one instance, no matter how many times it is executed. This single-instance scheme works fine as is, but it works better when the first instance of the application has a need to get command line arguments from any subsequent instances. Multiple-SDI and single-MDI applications are examples of applications that use this kind of processing.

Multiple-SDI Applications

A multiple-SDI application has multiple windows for content, although each window is a top-level window. Internet Explorer and Office 2003 are popular examples of multiple-SDI applications.[6] Figure 14.4 shows an example of a multiple-SDI application.

[6] Internet Explorer can be configured to show each top-level window in its own process, making it an SDI application, or to share all windows in a single process, making it a multiple-SDI application.

Figure 14.4. A Sample Multiple-SDI Application

A multiple-SDI application typically has the following features:

  • A single instance of the application is running.

  • Multiple top-level windows are running independently of each other.

  • It doesn't reopen files that are currently loaded.

  • When the last window goes away, the application does, too.

  • A Window menu allows a user to see and select from the currently available windows.

When a document is created or opened, it is loaded into a new window each time, whether the file was requested via the menu system or the command line. The first time the application is called, the first new instance of the top-level form is created and set as the main application form instance; if a file was requested, it is also opened by the form.

Subsequent requests to the application are routed to the custom WindowsFormsApplicationBase object located in the already-running application instance. Each request is handled to create a new form and build up the appropriate menu structures to support navigation between top-level instances, as well as opening and closing existing top-level instances. Figure 14.5 illustrates the work flow.

Figure 14.5. Work Flow of a Multiple-SDI Application with Support for Command Line Argument Passing

Multiple SDI requires single-instance support, which we acquire by deriving from WindowsFormsApplicationBase, as you saw earlier. We also need to ensure that the application stops running only after all top-level forms have been closed. We make the appropriate configurations from the constructor of the custom WindowsFormsApplicationBase class:

// MultiSDIApplication.cs class MultiSDIApplication : WindowsFormsApplicationBase { static MultiSDIApplication application; internal static MultiSDIApplication Application { get { if( application == null ) { application = new MultiSDIApplication(); } return application; } } public MultiSDIApplication() { // This ensures the underlying single-SDI framework is employed, // and OnStartupNextInstance is fired this.IsSingleInstance = true; // Needed for multiple SDI because no form is the main form this.ShutdownStyle = ShutdownMode.AfterAllFormsClose; } }

By default, the ShutdownStyle for a WindowsFormsApplicationBase object is AfterMainFormCloses, which refers to the form specified as the main form. However, with a multiple-instance SDI application, no form is the main form; therefore, no matter which form was created first, we want the application to close only after the last remaining top-level form is closed, and hence the need to explicitly set ShutdownStyle to AfterAllFormsClose.

Next, MultiSDIApplication must handle the first execution of the application. It does this by overriding OnCreateMainForm to create a new TopLevelForm object:

// MultiSDIApplication.cs class MultiSDIApplication : WindowsFormsApplicationBase { ... public MultiSDIApplication() {...} // Create first top-level form protected override void OnCreateMainForm() { this.MainForm = this.CreateTopLevelWindow(this.CommandLineArgs); } TopLevelForm CreateTopLevelWindow( ReadOnlyCollection<string> args) { // Get file name, if provided string fileName = (args.Count > 0 ? args[0] : null); // Create a new top-level form return TopLevelForm.CreateTopLevelWindow(fileName); } }

In this code, if a file argument was passed, a request is made to the main form to open it. Because all forms in a multiple-instance SDI application are top-level, however, no form is actually the main form. However, we must specify one if we override OnCreateMainForm, which helps later when the application needs to know which of the top-level forms is the active form. OnCreateMainForm passes the command line argssupplied by WindowsFormsApplicationBase.CommandLineArgsto the helper Create TopLevelWindow method, which parses the args for a file name, passing whatever it finds to the static CreateTopLevelWindow method that's implemented by TopLevelForm. CreateTopLevel Window is static because no specific form instance is responsible for creating another form.

To cope with subsequent requests to launch the application, we again override OnStartup NextInstance:

// MultiSDIApplication.cs class MultiSDIApplication : WindowsFormsApplicationBase { ... public MultiSDIApplication() {...} // Create first top-level form protected override void OnCreateMainForm() {...} // Create subsequent top-level form protected override void OnStartupNextInstance( StartupNextInstanceEventArgs e) { this.CreateTopLevelWindow(e.CommandLine); } TopLevelForm CreateTopLevelWindow( ReadOnlyCollection<string> args) {...} }

Here, the helper CreateTopLevelWindow is again passed command line arguments and called upon to create a new top-level window, opening a file if necessary.

Multiple-instance SDI applications also allow files to be opened from existing top-level forms via the File | Open menu, something we implement using the same static CreateTopLevelWindow method to open files from the command line:

// TopLevelForm.cs partial class TopLevelForm : Form { ... string fileName; ... public static TopLevelForm CreateTopLevelWindow(string fileName) { // Detect whether file is already open if( !string.IsNullOrEmpty(fileName) ) { foreach( TopLevelForm openForm in Application.OpenForms ) { if( string.Compare(openForm.FileName, fileName, true) == 0 ) { // Bring form to top openForm.Activate(); return openForm; } } } // Create new top-level form and open file TopLevelForm form = new TopLevelForm(); form.OpenFile(fileName); form.Show(); // Bring form to top openForm.Activate(); return form; } void openToolStripMenuItem_Click(object sender, EventArgs e) { // Open new window if( this.openFileDialog.ShowDialog() == DialogResult.OK ) { TopLevelForm.CreateTopLevelWindow(this.openFileDialog.FileName); } } ... void OpenFile(string fileName) { this.fileName = fileName; using( StreamReader reader = new StreamReader(fileName) ) { textBox.Text = reader.ReadToEnd(); } this.Text = this.Text + " (" + this.fileName + ")"; } string FileName { get { return this.fileName; } } }

CreateTopLevelWindow contains the code to check whether the desired file is already opened and, if it is, to bring the top-level window that contains it to the foreground; otherwise, the file is opened into a new top-level window.

Multiple-instance SDI applications also typically allow the creation of new files from the command line or from the File | New Window menu of a currently open top-level form. We tweak the OpenFile method to not open a file if null or if an empty string was passed as the file name:

// TopLevelForm.cs partial class TopLevelForm : Form { ... static int formCount = 0; public TopLevelForm() { InitializeComponent(); // Set form count ++formCount; this.Text += ": " + formCount.ToString(); } ... public static TopLevelForm CreateTopLevelWindow(string fileName) { ... // Create new top-level form and open file TopLevelForm form = new TopLevelForm(); form.OpenFile(fileName); form.Show(); ... } void newWindowToolStripMenuItem_Click( object sender, EventArgs e) { // Open new window TopLevelForm.CreateTopLevelWindow(null); } ... void OpenFile(string fileName) { this.fileName = fileName; if( !string.IsNullOrEmpty(fileName) ) { using( StreamReader reader = new StreamReader(fileName) ) { textBox.Text = reader.ReadToEnd(); } } else this.fileName = "Untitled" + formCount.ToString(); this.Text = this.Text + " (" + this.fileName + ")"; } ... }

Because a new file doesn't have a name, the top-level form gives it one; the standard naming convention for a new file is the concatenation of some default text with a version number. In this example, we use a combination of "Untitled" and an incremental count of the number of opened top-level forms, for uniqueness.

As mentioned before, a multiple-SDI application should implement a menu that allows users to navigate between open top-level forms as this is easier when files have unique names. MultiSDIApplication is an appropriate location for this logic because it manages the application:

// MultiSDIApplication.cs class MultiSDIApplication : WindowsFormsApplicationBase { ... public void AddTopLevelForm(Form form) { // Add form to collection of forms and // watch for it to activate and close form.Activated += Form_Activated; form.FormClosed += Form_FormClosed; // Set initial top-level form to activate if( this.OpenForms.Count == 1 ) this.MainForm = form; } void Form_Activated(object sender, EventArgs e) { // Set the currently active form this.MainForm = (Form)sender; } void Form_ FormClosed(object sender, FormClosedEventArgs e) { // Set a new "main" if necessary if( ((Form)sender == this.MainForm) && (this.OpenForms.Count > 0) ) { this.MainForm = (Form)this.OpenForms[0]; } form.Activated -= Form_Activated; form.FormClosed -= Form_FormClosed; } }

The MultiSDIApplication class uses the AddTopLevelForm method to keep track of a list of top-level forms as they are added. Each new form is kept in a collection and is watched for Activated and FormClosed events. When a top-level form is activated, it becomes the new "main" form, which is the one whose closure is detected by the base ApplicationContext class. When a top-level form closes, it's removed from the list. If the closed form was the main form, another form is promoted to that lofty position. When the last form goes away, the base ApplicationContext class notices and exits the application.

To keep the context up-to-date with the current list of top-level forms, the custom context watches for the Closed event on all forms. In addition, the custom context needs to be notified when a new top-level form has come into existence, a task that is best handled by the new form itself:

// TopLevelForm.cs partial class TopLevelForm : Form { ... public TopLevelForm() { ... // Add new top-level form to the application context MultiSDIApplication.Application.AddTopLevelForm(this); ... } ... }

The only remaining task is to designate and populate the Window menu with one menu item for each top-level form. The forms themselves can do this by handling the DropDownOpening event on the ToolStripMenuItem's Window object, using that opportunity to build the list of submenu items based on the names of all the forms. However, this code is boilerplate, so it's a good candidate to be handled by MultiSDIApplication on behalf of all top-level windows, from the AddWindowMenu method:

// MultiSDIApplication.cs class MultiSDIApplication : WindowsFormsApplicationBase { ... public void AddWindowMenu(ToolStripMenuItem windowMenu) { // Handle tool strip menu item's drop-down opening event windowMenu.DropDownOpening += windowMenu_DropDownOpening; } }

Each top-level form with a Window menu can add it to the context, along with itself, when it's created:

// TopLevelForm.cs partial class TopLevelForm : Form { ... public TopLevelForm() { ... // Add Window ToolStripMenuItem to the application context MultiSDIApplication.Application.AddWindowMenu( this.windowToolStripMenuItem); ... } ... }

Now, when the Window menu is shown on any top-level window, the DropDownOpening event fires. This constructs a new menu showing the currently open top-level forms during the time gap between mouse click and menu display:

// MultiSDIApplication.cs class MultiSDIApplication : WindowsFormsApplicationBase { ... void windowMenu_DropDownOpening(object sender, EventArgs e) { ToolStripMenuItem menu = (ToolStripMenuItem)sender; // Clear current menu if( menu.DropDownItems.Count > 0 ) { menu.DropDown.Dispose(); } menu.DropDown = new ToolStripDropDown(); // Populate menu with one item for each open top-level form foreach( Form form in this.OpenForms ) { ToolStripMenuItem item = new ToolStripMenuItem(); item.Text = form.Text; item.Tag = form; menu.DropDownItems.Add(item); item.Click += WindowMenuItem_Click; // Check menu item that represents currently active window if( form == this.MainForm ) item.Checked = true; } } }

As each menu item is added to the Window menu, a handler is added to the Click event so that the appropriate form can be activated when it's selected. The form associated with the ToolStripMenuItem's Tag property is extracted and activated:

// MultiSDIApplication.cs class MultiSDIApplication : WindowsFormsApplicationBase { ... void WindowMenuItem_Click(object sender, EventArgs e) { // Activate top-level form based on selection ((Form)((ToolStripMenuItem)sender).Tag).Activate(); } ... }

That's it. The extensible lifetime management of Windows Forms applications via a custom application context, along with a helper to find and activate application instances already running, provides all the help we need to build a multiple-SDI application in only a few lines of code. The result is shown in Figure 14.6.

Figure 14.6. Multiple-Instance SDI Application in Action

Multiple-SDI applications share much in common with MDI applications, although each document in an MDI application is loaded into a child window rather than a new main window. The key similarities include the requirement for MDI applications to be managed from a single executable and the ability to handle command line parameters.

Single-MDI Applications

Consider an MDI application like Microsoft Excel; files opened from the file system (by double-clicking) are all opened as separate child windows within the parent Excel window.[7] For the first instance of an MDI application to open a new child window to display the file that was passed to the second instance of the application, the second instance must be able to communicate with the initial instance.

[7] The fundamentals of building an MDI application in Windows Forms are described in Chapter 2: Forms.

A single-MDI application exhibits the characteristics we described in Chapter 2: Forms, as well as the following features:

  • A single instance of the application is running.

  • Multiple MDI child windows are running within the same MDI parent window.

  • Currently opened files are not reopened.

  • When the last MDI child window goes away, the application remains.

  • When the MDI parent window goes away, the application exits.

  • A Window menu allows a user to see and select from the currently available windows.

The work flow for a single-MDI application ensures that a new MDI child form is opened each time the application is called, whether or not a file was requested for opening.

The first time the application is called, the MDI parent is created and set as the main application form instance; if a file was requested, it is also opened into a new MDI child form. Subsequent requests to the application are routed through the MDI parent form to create a new MDI child form and build up the appropriate menu structures to support navigation between top-level instances, as well as opening and closing existing top-level instances. Figure 14.7 illustrates the work flow.

Figure 14.7. Work Flow of a Single-MDI Application with Support for Passing Command Line Arguments

With WindowsFormsApplicationBase ensuring that only one instance of the application executes, we need to handle two specific scenarios: first, when arguments are passed from the command line directly when the first instance loads and, second, when the first instance is passed command line arguments from a second instance.

Handling the first scenario requires a main application form that's an MDI parent and can open a new or existing file into an MDI child form:

// MDIParentForm.cs partial class MDIParentForm : Form { ... // This is necessary to bring the MDI parent window to the front, // because Activate and BringToFront don't seem to have any effect. [DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr hWnd); public void CreateMDIChildWindow(string fileName) { SetForegroundWindow(this.Handle); // Detect whether file is already open if( !string.IsNullOrEmpty(fileName) ) { foreach( MDIChildForm openForm in this.MdiChildren ) { if( string.Compare(openForm.FileName, fileName, true) == 0 ) { openForm.Activate(); return; } } } // If file not open, open it MDIChildForm form = new MDIChildForm(); form.OpenFile(fileName); form.MdiParent = this; form.Show(); } void newToolStripMenuItem_Click(object sender, EventArgs e) { this.CreateMDIChildWindow(null); } void openToolStripMenuItem_Click(object sender, EventArgs e) { if( this.openFileDialog.ShowDialog() == DialogResult.OK ) { this.CreateMDIChildWindow(this.openFileDialog.FileName); } } ... }

This code allows users to open a file using a menu strip item, and it lays the foundation for opening a file from the command line, including preventing the reopening of an already open file. We continue using WindowsFormsApplicationBase to achieve this, updating the earlier sample to acquire the command line arguments and pass them to the application main form's CreateMDIChildWindow method to open a file:

// SingleMDIApplication.cs class SingleMDIApplication : WindowsFormsApplicationBase { static SingleMDIApplication application; internal static SingleMDIApplication Application { get { if( application == null ) { application = new SingleMDIApplication(); } return application; } } public SingleMDIApplication() { // This ensures the underlying single-SDI framework is employed, // and OnStartupNextInstance is fired this.IsSingleInstance = true; } // Load MDI parent form and first MDI child form protected override void OnCreateMainForm() { this.MainForm = new MDIParentForm(); this.CreateMDIChildWindow(this.CommandLineArgs); } void CreateMDIChildWindow(ReadOnlyCollection<string> args) { // Get file name, if provided string fileName = (args.Count > 0 ? args[0] : null); // Ask MDI parent to create a new MDI child // and open file ((MDIParentForm)this.MainForm).CreateMDIChildWindow(fileName); } }

During construction, we specify that this application is a single-instance application. Unlike with multiple-SDI applications, however, we don't need to set the ShutdownStyle property because its value defaults to AfterMainFormClosesexactly what is needed for an MDI application.

OnCreateMainForm creates the MDI parent form and sets it as the application's main form and the one responsible for creating MDI child windows. Then, the command line arguments are passed to the helper CreateMDIChildWindow method, which parses them for a file name. Either a file name or null is passed to the MDI parent form's version of CreateMDIChildWindow, which creates the new MDI child window, into which it loads a file; then CreateMDIChildWindow establishes the MDI parent-child relationship and shows the requested file. CreateMDIChildWindow also activates the MDI parent form to bring the application to the foreground.

In the second scenario, the desired processing is for the command line arguments to be passed from the second instance to the first, to which the first instance responds by processing the command line arguments and, if required, creating a new MDI child form. WindowsFormsApplicationBase handles the underlying mechanics of passing arguments from the second instance to the first, but it is up to you to process the command line arguments accordingly. You can achieve this by overriding WindowsFormsApplicationBase.OnStartupNextInstance, which passes the command line arguments via the CommandLine property of a StartupNextInstanceEventArgs object. The following code shows the OnStartupNextInstance override implementation:

// SingleMDIApplication.cs class SingleMDIApplication : WindowsFormsApplicationBase { ... // Must call base constructor to ensure correct initial // WindowsFormsApplicationBase configuration public SingleMDIApplication() {...} // Load MDI parent form and first MDI child form protected override void OnCreateMainForm() {...} // Load subsequent MDI child form protected override void OnStartupNextInstance( StartupNextInstanceEventArgs e) { this.CreateMDIChildWindow (e.CommandLine); } void CreateMDIChildWindow(ReadOnlyCollection<string> args) {...} }

As you can see, centralizing CreateMDIChildWindow into a single helper method greatly simplifies the implementation of OnStartupNextInstance.

That's the complete solution, so let's look at how it operates. Suppose we start the application for the first time by executing the following statement from the command line:

C:\SingleInstanceSample.exe C:\file1.txt

The result is to load the application, configure the single-instance command line argument (passing support from our derivation of WindowsFormsApplicationBase), load the main MDI parent form, and, finally, open an MDI child form, displaying the file specified from the command line arguments. Figure 14.8 illustrates the result.

Figure 14.8. Result of Creating a First Instance of a Single-Instance Application

Now, consider the next statement being called while the first instance is still executing:

C:\SingleInstanceSample.exe C:\file2.txt

This time, a second instance of the application is created, butthanks to SingleMDIApplication, our WindowsFormsApplicationBase derivationthe second instance passes its command line arguments to the first instance before closing itself down. The first instance processes the incoming command line arguments from OnStartupNextInstance, requesting the MDI parent form to open a new MDI child and display the specified file. The result is shown in Figure 14.9.

Figure 14.9. Result of Creating a Second Instance of a Single-Instance Application

Although it would be difficult to code single-instance applications such as single MDI and multiple SDI by hand, the presence of support in the Visual Basic runtime assembly makes life a lot easier. This is one of the strengths of Windows Forms; unlike forms packages of old, Windows Forms is only one part of a much larger, integrated whole. When its windowing classes don't meet your needs, you still have all the rest of the .NET Framework Class Library to fall back on.

Категории