Real World Applications Sample, Part 1: Adding SDI, Single Instance, and a Forms Collection to Windows Forms

 

Chris Anderson
Software Architect
Microsoft Corporation

June 2002

Applies to:
   Microsoft® .NET
   Microsoft® Visual Studio® .NET

Summary: Details the conversion of a sample image browser to a real application in Microsoft .NET that runs as a single document interface (SDI) application similar to Microsoft Word; other features include a Window menu showing all the currently open windows, loading multiple images by running the application on the command line, and a reusable application framework library that you can use in your applications. (14 printed pages)

Download ImageBrowser.exe.

Note   The attached code consists of two solutions—SampleImageBrowser and ImageBrowser. The SampleImageBrowser is the initial sample that this application is built from, while ImageBrowser represents the application that we are building.

Contents

Introduction
The Application
Single Document Interface (SDI)
Application Model
Single-Instance Applications
Forms Collection
Window Menu
Conclusion

Introduction

When I was asked to write—nay, given the privilege of writing—a series of articles for MSDN, I spent days trying to think of what I should cover, and finally decided what people are really looking for is how to produce real applications. By "real" applications, I refer to the nitty-gritty details of producing the kind of software people are willing to pay for. These are applications that version over time, that can be maintained by multiple people, and support all the features that you expect from a smart client.

This article series documents the process that the fictitious company, Real World Applications, goes through to convert a sample image browser to a real application in Microsoft .NET.

The Application

Given the wealth of sample image browsers, the imaginary mega-corporation of Real World Applications (RWA) has decided that there must be a lack of real image browsers in the market today. To feed this need, the executives at RWA have formed a team to build a world-class image browser. In typical management fashion, they haven't yet defined all the requirements for the application; however, they know it has to be a lot more than a sample application. They want it to support file association, a Microsoft Word-style SDI window model, a setup program, application icon, custom plug-ins, etc.

Luckily, we happen to have the source from one of the many sample image browsers, so we can focus on the various application features that are beyond the simple sample. I won't be able to cover all the features in this one article, so look for future articles to cover the other aspects of this revolutionary image browser! However, as with all RWA products, the feature list continues to change, so you never know what will happen in the future. Also, the managers over at RWA are always looking for feature ideas, so feel free to send me mail with ideas and I'll forward them.

RWA intends to branch into other markets in addition to the lucrative image browser field. RWA wants all the work put into its first product to be leveraged for other applications. We have decided to produce a reusable application-authoring library. This library will fill in some of the holes in the Microsoft .NET Framework APIs to make re-using application authoring a lot easier.

This reusable library can be easily re-integrated in to your own applications as well. All the code that is attached to this series of articles is usable as-is with a license in each download. To summarize the license: you can do anything you want with the code. but I can't guarantee it works and you can't sue me if it doesn't!

For this and future articles, I will use the namespace Microsoft.Samples.ApplicationFramework ("ApplicationFramework" for short) for classes that are part of the reusable framework. These classes will always be in a separate Microsoft® Visual Studio® .NET project and produce a separate assembly. The Image Browser application that I will continue to work on will be in Microsoft.Samples.ImageBrowser ("ImageBrowser" for short) and produce an executable that depends on the ApplicationFramework.

Without further adieu, let's dig into the first feature we want to add to our image browser application.

Single Document Interface (SDI)

In many applications today, the familiar Multiple Document Interface (MDI) has fallen out of favor and SDI has taken over. Microsoft Word and Microsoft Outlook are great examples of this new model. Each document is given a new top-level window and an entry in the taskbar. This gives users consistent navigation and removes some of the confusion of MDI windows. However, the last thing you want is to start a completely new instance of your application for each top-level window. When writing .NET Framework code, there is a somewhat fixed overhead to load the Common Language Runtime and the associated class libraries. By showing multiple documents in a single process instance, you can better share this overhead and make the application consume less operating system resources.

This model raises some interesting questions: First, given that the user may double-click the application more than once, how do you get a single process to handle all of the instances of the application? In the Win16 days, you could check the HINSTANCE variable in your WndMain, but that hasn't worked for some time. Secondly, once you have all these documents up and running, how do they communicate? You need to have the application terminate when the last document is closed, and you also want to provide a Window menu that lists all the open documents.

Application Model

Before we can dive into either the multiple top-level window or the single instance problems, we first need to understand a bit about the application model for Windows Forms. We know that we are going to continue to add more application services to our framework for other people to use, so we want to start by creating a better idea of what an "application" is.

Let's start with the most basic Windows Forms application:

using System.Windows.Forms;
using System;
public class Form1 : Form
{
   public Form1()
   {
      Text = "Hello World";
   }
   [STAThread]
   static int Main(string[] args)
   {
      Application.Run(new Form1());
      return 0;
   }
}

This application will run with a single Form that displays the text "Hello World" in the caption. This will look like:

Figure 1. Application model in a Windows Form

What is the application model here? What controls the lifespan of the application?

We know that .NET Framework client applications always start with a Main method. That method is called when the application starts up, and when the method exits, the program is terminated. In this case you could say that the Main method defines the application model. However, if we look closer at the body of Main, we see that it calls Application.Run.

Application.Run has three variants:
   Application.Run()
   Application.Run(Form mainForm)
   Application.Run(ApplicationContext context)

Beyond just having different parameters, each variant also behaves very differently. The version with no arguments will never exit, unless you call Application.Exit. The version that takes a Form argument will run until that form is closed. The version that takes an ApplicationContext will exit when the context is disposed, or ExitThread is called on the context.

For each Application.Run variant, an ApplicationContext is always created for each UI thread in an application. The lifespan of the application is determined by the ApplicationContext. The version of Application.Run that takes a form associates that form with the mainForm property of the ApplicationContext and then begins execution. The default implementation of ApplicationContext will terminate when the mainForm is closed.

Windows Forms actually has a very lightweight application model that is controlled by application context objects. These objects are typically created behind the scenes for you. However, we can leverage this infrastructure to build a richer set of application services.

The first class we will create in the ApplicationFramework will be UserApplicationContext. All of the framework classes will be prefixed with "User" to avoid any name conflicts with Windows Forms built-in types. This application context will be the central point for much of the infrastructure for building application services and extensions to the Windows Forms application model. For example, you could use the application context to store global application state (like a Forms collection).

The general design for any application that is going to use the ApplicationFramework will be to define a private class called App that derives from UserApplicationContext and always contains the entry point (Main function in C#). The default App would look like:

namespace Microsoft.Samples.ImageBrowser
{
   using Microsoft.Samples.ApplicationFramework;
   using System.Windows.Forms;
   // other using statements...
   class App : UserApplicationContext
   {
      [STAThread]
      static int Main(string[] args)
      {
         return UserApplicationContext.Run(args, typeof(App));
      }
   }
}

As we progress and add more features to the UserApplicationContext, it will become clear why we are using this pattern. For now, just trust me.

Single-Instance Applications

We will tackle the harder problem first—that of having an application that only "starts" once. In the world of Microsoft Win32® this is typically done with a mailslot, named pipe, or other globally named kernel object. This may sound very complex; however, all existing applications and tools (Microsoft Visual Basic® 6.0, MFC, etc) do this work under the covers.

Unfortunately the .NET Framework doesn't provide wrappers for any of these objects—at least not in a way usable for this type of work. I spent a bit of time scouring the various .NET Framework APIs and found RemotingServices.Connect in the System.Runtime.Remoting namespace. The documentation states: "Takes in the Type and URL of the well-known object to which you want to connect to, and creates a proxy object for it," which sounds just like what we are looking for.

It was quite an effort to eventually figure out how to publish this "well-known" object, and how to generate a unique URL for it. Luckily, we are creating an application framework that can encapsulate this logic.

The simplest way to allow the application framework to handle the single instance logic is to have all applications delegate the entry point (Main in C#) to the framework. The application's Main method will always delegate to UserApplicationContext.Run, and we will provide some events for the application to use. In addition, since Run may be called more than once, some of these events may fire more than once. Right now we are ready to define several events on the UserApplicationContext class:

Startup—fires once per application invocation. A Boolean NewInstance property on the event argument instance allows the application author to determine if this is the first instance (NewInstance = true) or a secondary instance.

StartupArguments—fires once per application invocation. As with startup, the NewInstance property allows the application to detect the first instance. The Arguments property provides all the command line arguments passed to the application's executable.

Shutdown—fires once per application. This will only occur when the process is about to terminate.

This allows for very simple application logic.

namespace Microsoft.Samples.ImageBrowser
{
   // using statements...
   class App : UserApplicationContext
   {
      [STAThread]
      static int Main(string[] args)
      {
         return UserApplicationContext.Run(args, typeof(App));
      }
      App()
      {
         StartupArguments += new StartupArgumentsEventHandler(Args); 
      }
      void Args(object sender, StartupArgumentsEventArgs e)
      {
         // special case... first run of application
         // with no command args – show an empty window
         //
         if (e.NewInstance && e.Arguments.Count == 0)
         {
            ImageViewForm f = new ImageViewForm();
            f.Show();
         }
         else
         {
            foreach (string arg in e.Arguments)
            {
               ImageViewForm f = new ImageViewForm();
               f.DisplayFile = arg;
               f.Show();
            }
         }
      }
   }
}

Behind the Scenes

For those of you that really want to understand what is happening here, I'll go into the details; however, these are implementation details that don't affect the application model.

The first application instance registers an object for the local machine and lets the remoting system generate a unique name, which we stuff into the registry. The second instance uses that name from the registry to connect back to the main process. Once the two processes are connected, we can easily pass the command line arguments from the second process to the first.

To show a little more detail, here is the step-by-step process of what happens:

  1. Generate a named Mutex based upon the .exe that we start in.
  2. Lock on this Mutex—this ensures that this is really the first instance. If we fail to get the lock, then this is the second instance and we will execute the code path outlined below.
  3. Now that we have the lock, we instantiate the SingletonCommunicator class. This is a MarshalByRefObject derived class that we can remote between machines or, in this case, between processes.
  4. Call RemotingServices.Marshal to publish the object.
  5. Create a Hashtable and fill in bindTo to 127.0.0.1 (localhost) and port to 0. This Hashtable will be passed into the remoting system to configure a channel for remoting objects. The port of 0 will instruct the system to find any currently open port.
  6. Create a TcpChannel configured with the Hashtable and call ChannelServices.RegisterChannel.
  7. Ask the TcpChannel for the name of the object and stuff that name into a registry entry.

The second instance of the application will go through a different code path:

  1. Generate a named Mutex based upon the .exe that we start in.
  2. Lock on this Mutex—this will fail for the second instance.
  3. In the registry, look up the name of the "well-known" object to connect to.
  4. Call RemotingServices.Connect to get a proxy to the object from the first instance.
  5. Call ProcessRun on the SingletonCommunicator, which will handle passing the command line arguments to the main process.

Forms Collection

Once our application is up and running, we will immediately notice a problem—it never terminates. Since the lifespan of the application is determined by the application context, and that is controlled by the MainForm property (which we never set), the context is never terminated. The correct behavior is to have the application terminate when the last form is closed. In addition, we want to provide a Window menu similar to Microsoft Word that shows all the currently opened images. All of these rely on having a list somewhere of all the open forms.

In Visual Basic 6.0 and earlier, there was a simple Forms property that contained the list of all open forms. However, Windows Forms doesn't expose a similar list. We should add it to our application framework. First, we need to find a place to put the forms collection. If you remember, we have the single instance of the UserApplicationContext for the application, so this is a logic place to put the forms collection. Secondly, we want a strongly typed collection so I used the collection generator from https://www.gotdotnet.com to produce a class called FormsCollection that provides a nice strongly typed object model.

namespace Microsoft.Samples.ApplicationFramework
{
   // using statements...
   public class UserApplicationContext : ApplicationContext
   {
      private static UserApplicationContext m_current;
      private FormCollection m_runningForms;
      public static FormCollection Forms
      {
         get
         {
            return UserApplicationContext.Current.RunningForms;
         }
      }
      public static UserApplicationContext Current
      {
         get
         {
            return m_current;
         }
      }
      public FormCollection RunningForms
      {
         get
         {
            return m_runningForms;
         }
      }
      // rest of implementation...
   }
   public class FormCollection : CollectionBase
   {
      // class created by Collection Generator
   }

   public class UserForm : Form
   {
      public UserForm()
      {
         if (UserApplicationContext.Current != null)
         {
            UserApplicationContext.Forms.Add(this);
         }
      }
   }
}

Now, if I make all of my forms derive from UserForm, I can easily use the FormsCollection.Forms field to see all the available forms. Hmm, but there is a subtle bug here. Since Forms is a static field, and I add each created form into that list, the garbage collector will never clean up my forms. As I create more and more forms, my working set will get bigger and bigger. To fix this, let's add another method to UserForm.

namespace Microsoft.Samples.ApplicationFramework
{
   ...
   public class UserForm : Form
   {
      ...
      protected override void OnClosed(EventArgs e)
      {
         base.OnClosed(e);
         if (UserApplicationContext.Current != null)
         {
            UserApplicationContext.Forms.Remove(this);
         }
      }
   }
}

Much better! Now, when the form is closed we remove it from the collection and it can be cleaned up. However, there is another problem with this application. It never exits. You remember from before that the application context determines the lifespan of the application. In the case of this SDI application there is no main form, so the application context doesn't know when to quit. In a future article, we will have a better solution for this problem; however, for the time being we will add some code to the Closed event for the form in our application.

namespace Microsoft.Samples.ImageBrowser
{
   // using statements...
   class ImageViewerForm : UserForm
   {
      ...
      private void ImageViewerForm_Closed(object sender, EventArgs e)
      {
         if (App.Forms.Count == 1)
         {
            App.Current.ExitThread();
         }
      }
   }
}

This code can call access the App.Forms and App.Current properties because the class App derives from UserApplicationContext. Notice this checks for a single form and not zero forms. The reason for this is that in our UserForm override of OnClosed, the base class method is called before removal from the forms collection. This causes the event handlers to be called before removal; thus, we must check that the last form is closed.

Window Menu

Normal MDI applications have a nice menu that lists all the currently open windows. This is easy to implement—set the MdiList property to true on a MenuItem. However, in the case of SDI, the MdiList property is useless. The UserMenuItem derived menu type will support a window list associated with the forms collection on UserApplicationContext and make writing this type of SDI application easy.

Before we create our SDI window list, we need to understand how the MdiList property works. When you use the default implementation, the MenuItem with MdiList set to true catches the Popup event on the item, and then dynamically populates the list of menus. It also needs to track which items it created the last time it was displayed, so it can clean up the old menu items. In addition to creating the list of items, it limits to nine the number of menu items it creates, and if there are more, it creates a tenth item labeled "More Windows..." that displays a dialog. All of this is done each time you click the top-level menu item, in that item's Popup event.

Unfortunately, none of this built-in logic is extensible, so we will have to reproduce all of this behavior. However, we want to correct this by having our implementation support a more generic mechanism. It seems that there will be other cases where we want dynamically populated menu items in the future, perhaps for a most recently used document list or something of the sort. In addition, we need to allow people to derive from our new base type so that they can do custom painting and other customizations to the menu items. To allow for the generic dynamic children, as well as the default case for SDI and MDI window lists, I added five properties and one event to our UserMenuItem class.

Property DynamicMenuItemsAbsolutePosition—Determines the starting position of all the dynamic menu items when the DynamicMenuItemPosition property is set to Absolute.

Property DynamicMenuItemsRelativePosition—Determines the relative starting position of the dynamic menu items when the DynamicMenuItemPosition is set to Relative. All the dynamic items are placed immediately after this menu item.

Property DynamicMenuItemPosition—Determines what method should be used to determine dynamic menu item position. Valid values are First, Last, Relative, and Absolute.

Property DynamicMenuItems—Determines what kind of dynamic menu items to use. Valid values are None, FormsCollection, MdiChildren, and Custom.

Property DynamicMenuItemsSeparator—Determines where to place separators when dynamic menu items are added. Valid values are None, Before, After, or Both.

Event DynamicMenuItemPopulate—Raised when the dynamic menu items are needed. This is only called if the DynamicMenuItems property is set to Custom.

The positioning of the dynamically created items is controlled by three properties—DynamicMenuItemsAbsolutePosition, DynamicMenuItemsRelativePosition, and DynamicMenuItemsPosition. The DynamicMenuItemsPosition property determines if the absolute or relative property, (or neither) are used to calculate the desired location. For example, when the position is set to Relative, then the menu item referred to by DynamicMenuItemsRelativePosition is used to determine the position of the dynamic items.

All the code for UserMenuItem is in the attached code sample, and is a bit much to list out here. A final note of interest is the UserDialog base class that I added to the application framework. One of the requirements for the Window menu is to place the final More Windows… menu item when you have too many items. When this item is invoked, you want to display a dialog. Creating a modal dialog is a very common operation, and unfortunately it isn't as easy as it should be. The UserDialog base class changes the default values for various Form properties to make the dialog look like a standard Windows modal dialog, making this task simple.

Using these menus in our application is simple.

namespace Microsoft.Samples.ImageBrowser
{
   ...
   class ImageViewerForm : UserForm
   {
      private UserMenuItem m_windowMenu;
      private UserMenuItem m_newWindowMenu;

      ...

      private void InitializeComponent()
      {
         ...
         m_windowMenu.DynamicMenuItems = DynamicMenuItems.FormsCollection;
         m_windowMenu.MenuItems.AddRange(
            new System.Windows.Forms.MenuItem[] { m_newWindowMenu });
         m_windowMenu.Text = "&Window";

         m_newWindowMenu.Text = "&New Window";
         m_newWindowMenu.Click += 
            new System.EventHandler(this.newWindowMenu_Click);
         ...
      }

      private void newWindowMenu_Click(object sender, System.EventArgs e)
      {
         ImageViewerForm f = new ImageViewerForm();
         f.m_viewer.DisplayMode = m_viewer.DisplayMode;
         f.Show();
      }
      ...
   }
}

Since ImageViewerForm derives from UserForm, each new form is automatically added to the forms collection on UserApplicationContext. When m_windowMenu is displayed, the implementation of UserMenuItem will populate it dynamically with a list of all the currently open forms.

Conclusion

In this article, we have taken the first steps to converting our simple sample image browser to a real application. Our application now runs as an SDI application similar to Microsoft Word. The Window menu shows all the currently open windows, and you can load multiple images by running the application on the command line. The application framework is a reusable library that anyone can use in their own applications. In future articles, we will expand on this application framework and continue to add improvements to the image browser application.

Chris Anderson is a member of the Microsoft .NET Client team that works on Windows Forms and other client technologies for the Microsoft .NET Framework. While the .NET Framework version 1.0 was under development, he worked on several different areas of the class libraries, including Windows Forms, Microsoft ASP.NET, and the Base Class Libraries (BCL). Prior to that, Chris worked on Visual Basic controls and on the Windows Foundation Classes (WFC) as part of the Microsoft Visual J++ 6.0 team.

The example companies, organizations, products, domain names, e-mail addresses, logos, people, places, and events depicted herein are fictitious. No association with any real company, organization, product, domain name, email address, logo, person, places, or events is intended or should be inferred.