An application is anything with an EXE extension that can be started from the shell. However, applications are also provided for directly in WinForms by the Application class from the System. Windows .Forms namespace: NotInheritable Class Application ' Properties Shared Property AllowQuit() As Boolean Shared Property CommonAppDataPath() As String Shared Property CommonAppDataRegistry() As RegistryKey Shared Property CompanyName() As String Shared Property CurrentCulture() As CultureInfo Shared Property CurrentInputLanguage() As InputLanguage Shared Property ExecutablePath() As String Shared Property LocalUserAppDataPath() As String Shared Property MessageLoop() As Boolean Shared Property ProductName() As String Shared Property ProductVersion() As String Shared Property SafeTopLevelCaptionFormat() As String Shared Property StartupPath() As String Shared Property UserAppDataPath() As String Shared Property UserAppDataRegistry() As RegistryKey ' Events Shared Event ApplicationExit As EventHandler Shared Event Idle As EventHandler Shared Event ThreadExcpetion As ThreadExceptionEventHandler Shared Event ThreadExit As EventHandler ' Methods Shared Sub AddMessageFilter(value As IMessageFilter) Shared Sub DoEvents() Shared Sub Exit() Shared Sub ExitThread() Shared Function OleRequired() As ApartmentState Shared Sub OnThreadException(t As Exception) Shared Sub RemoveMessageFilter(value As IMessageFilter) Shared Overloads Sub Run() Shared Overloads Sub Run(context As ApplicationContext) Shared Overloads Sub Run(mainForm As Form) End Class Notice that all of the members of the Application class are Shared. Although there is per-application state in WinForms, there is no instance of an Application class. Instead, the Application class is a scoping mechanism for the various services that the class provides, including lifetime control, message handling, and settings. Application LifetimeA WinForms application starts when the Main method is called. However, to initialize a WinForms application fully and start it routing WinForms events, you need a call to Application.Run. There are three ways to call the Application class's Run method. The first is to simply call Run with no arguments at all. This is useful only if other means have already been used to show an initial UI: <STAThread()> _ Shared Sub Main() ' Create and show the main form modelessly Dim myform As Form = New MainForm() Myform.Show() ' Run the application Application.Run() End Sub When you call Run with no arguments, the application runs until explicitly told to stop, even when all its forms are closed. This puts the burden on some part of the application to call the Application class's Exit method: Sub MainForm_Closed(sender As Object, e As EventArgs) ' Close the application when the main form goes away ' Only for use when Application.Run is called without ' any arguments Application.Exit() End Sub Typically, you call Application.Run without any arguments only when the application needs a secondary UI thread. A UI thread is one that calls Application.Run and can process the events that drive a Windows application. Because the vast majority of applications contain a single UI thread and because most of those have a main form that, when closed, causes the application to exit, another overload of the Run method is used far more often. This overload of Run takes as an argument a reference to the form designated as the main form. When Run is called this way, it shows the main form and doesn't return until the main form closes : <STAThread()> _ Shared Sub Main() ' Create the main form Dim myform As Form = New MainForm() ' Run the application until the main form is closed Application.Run(myform) End Sub In this case, there is no need for explicit code to exit the application. Instead, the Application watches for the main form to close and then exits itself. Application ContextInternally, the Run method creates an instance of the ApplicationContext class from the System.Windows.Forms namespace. It's this class that subscribes to the main form's Closed event and exits the application as appropriate: Class ApplicationContext ' Constructors Public Overloads Sub New() Public Overloads Sub New(mainForm As Form) ' Properties Property MainForm() As Form ' Events Event ThreadExit As EventHandler ' Methods Sub ExitThread() Protected MustOverride Sub OnMainFormCloased( _ sender As Object, e As EventArgs) End Class In fact, the Run method allows you to pass an ApplicationContext yourself: <STAThread()> _ Shared Sub Main() ' Run the application with a context Dim ctx As ApplicationContext = _ New ApplicationContext(New MainForm()) Application.Run(ctx) End Sub This is useful if you'd like to derive from the ApplicationContext class and provide your own custom context: Class MyTimedContext Inherits ApplicationContext Dim mytimer As Timer = New Timer() Public Sub New(myform As Form) MyBase.New(myform) AddHandler mytimer.Tick, New EventHandler(AddressOf TimesUp) mytimer.Interval = 300000 ' 5 minutes = 300,000 milliseconds mytimer.Enabled = True End Sub Sub TimesUp(sender As Object, e As EventArgs) mytimer.Enabled = False mytimer.Dispose() Dim res As DialogResult = _ MessageBox.Show("OK to charge your credit card?", _ "Time's Up!", _ MessageBoxButtons.YesNo) If res = DialogResult.No Then ' See ya... MyBase.MainForm.Close() End If ... End Sub End Class <STAThread()> _ Shared Sub Main() ' Run the application with a custom context Dim ctx As ApplicationContext = New MyTimedContext(New MainForm()) Application.Run(ctx) End Sub This custom context class waits for five minutes after an application has started and then asks to charge the user 's credit card. If the answer is no, the main form of the application will be closed (available from the MainForm property of the base ApplicationContext class), causing the application to exit. Conversely, if you'd like to stop the application from exiting when the main form goes away, you can override the OnMainFormClosed method from the ApplicationContext base class: Class RemotingServerContext Inherits ApplicationContext Public Sub New(myform As Form) MyBase.New(myform) End Sub Protected Overrides Sub OnMainFormClosed(sender As Object, _ e As EventArgs) ' Don't let base class exit application ' NOTE: Remember to call Application.Exit ' later when the remoting service ' is finished servicing its clients If ServicingRemotingClient() Then Exit Sub ' Let base class exit application MyBase.OnMainFormClosed(sender, e) End Sub Protected Function ServicingRemotingClient() As Boolean ... End Function End Class This example assumes an application that is serving .NET Remoting [1] clients and so needs to stick around even if the user has closed the main form.
Application EventsDuring the lifetime of an application, several application events will be fired : idle, thread exit, application exit, and sometimes a thread exception. You can subscribe to application events at any time, but it's most common to do it in the Main function: Shared Sub App_Exit(sender As Object, e As EventArgs) Shared Sub App_Idle(sender As Object, e As EventArgs) Shared Sub App_ThreadExit(sender As Object, e As EventArgs) <STAThread()> _ Shared Sub Main() AddHandler Application.Idle, New EventHandler(AddressOf App_Idle) AddHandler Application.ThreadExit, _ New EventHandler(AddressOf App_ThreadExit) AddHandler Application.ApplicationExit, New EventHandler(App_Exit) End Sub The idle event happens when all events in a series of events have been dispatched to event handlers and no more events are waiting to be processed . The idle event can sometimes be used to perform concurrent processing in tiny chunks , but it's much more convenient and robust to use worker threads for those kinds of activities. This technique is covered in Chapter 14: Multithreaded User Interfaces. When a UI thread is about to exit, it receives a notification via the thread exit event. When the last UI thread goes away, the application's exit event is fired. UI Thread ExceptionsOne other application-level event that can be handled is a thread exception event. This event is fired when a UI thread causes an exception to be thrown. This one is so important that WinForms provides a default handler if you don't. The typical .NET unhandled exception on a user's machine behavior yields a dialog box as shown in Figure 11.1. Figure 11.1. Default .NET Unhandled-Exception Dialog Box
This kind of exception handling tends to make the user unhappy . This dialog is confusing, and worse , there is no way to continue the application to attempt to save the data being worked on at the moment. On the other hand, by default, a WinForms application that experiences an exception during the processing of an event shows a dialog like that in Figure 11.2. Figure 11.2. Default WinForms Unhandled-Exception Dialog Box
Although this dialog may look functionally the same as the one in Figure 11.1, there is one major difference: The WinForms version has a Continue button. What's happening is that WinForms itself catches exceptions thrown by event handlers; in this way, even if that event handler caused an exception ”for example, if a file couldn't be opened or there was a security violation ”the user is allowed to continue running the application with the hope that saving will work, even if nothing else does. This is a safety net that makes WinForms applications more robust in the face of even unhandled exceptions than Windows applications of old. However, if a user has triggered an exception and it's caught, the application could be in an inconsistent state, so it's best to encourage your users to save their files and restart the application. If you'd like to replace the WinForms unhandled-exception dialog with something application-specific, you can do so by handling the application's thread exception event: Imports System.Threading Shared Sub App_ThreadException(sender As Object,e as _ ThreadExceptionEventArgs) Dim msg As String = _ "A problem has occurred in this application:" & vbCrLf & _ vbTab & e.Exception.Message & vbCrLf & _ "Would you like to continue the application so that" & _ vbCrLf & "you can save your work?" Dim res As DialogResult = _ MessageBox.Show(msg, "Unexpected Error", _ MessageBoxButtons.YesNo) ' Returning continues the application If res = DialogResult.Yes Then Exit Sub ' Must exit the application manually if handling ' the thread exception event Application.Exit() End Sub <STAThread()> _ Shared Sub Main ' Handle unhandled thread exceptions AddHandler Application.ThreadException, _ New EventHandler(AddressOf App_ThreadException) ' Run the application Application.Run(New MainForm()) End Sub Notice that the thread exception handler takes a ThreadExceptionEvent object, which includes the exception that was thrown. This is handy if you want to tell the user what happened , as shown in Figure 11.3. Figure 11.3. Custom Unhandled-Exception Dialog
If you provide a thread exception handler, the default exception handler will not be used, so it's up to you to let the user know that something bad has happened. If the user decides not to continue with the application, calling Application.Exit will shut down the application: Shared Sub App_ThreadException(sender As Object, _ E As ThreadExceptionEventArgs) System.Diagnostics.Debug.WriteLine("App_ThreadException") Dim msg As String = _ "A problem has occurred in this application:" & vbCrLf & _ vbTab & e.Exception.Message & vbCrLf & _ "Would you like to continue the application so that" & _ vbCrLf & "you can save your work?" Dim res As DialogResult = _ MessageBox.Show(msg, "Unexpected Error", _ MessageBoxButtons.YesNo) If res = DialogResult.Yes Then Exit Sub ' Shut 'er down, Clancy, she's a'pumpin' mud! Application.Exit() End Sub Single-Instance ApplicationsBy default, each EXE is an application and 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 a Single Document Interface (SDI) application with a single top-level window, a Multiple Document Interface (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. You can do this using an instance of the Mutex class from the System.Threading namespace: [2]
Imports System.Threading ... Shared Sub Main() ' Check for existing instance Dim firstInstance As Boolean = False Dim safeName As String = _ Application.UserAppDataPath.Replace("\", "_") Dim mymutex As Mutex = New Mutex(True, safeName, _ ByRef firstInstance) If Not(firstInstance) Then Exit Sub Application.Run(New MainForm()) End Sub This code relies on a named kernel object , that is, an object that is managed by the Windows kernel. The fact that it's a mutex doesn't really matter. What matters is that we create a kernel object with a systemwide unique name and that we can tell whether an object with that same name already exists. When the first instance is executed, that kernel object won't exist, and we won't return from Main until Application.Run returns. When another instance is executed, the kernel object will already exist, so Main will exit before the application is run. One interesting note on the name of the mutex is worth mentioning. To make sure we have a unique name for the mutex, we need something specific to the version of the application but also specific to the user. It's important to pick a string that's unique per application so that multiple applications don't prevent each other from starting. If there are multiple users, it's equally important that each user get his or her own instance, especially in the face of Windows XP, fast user switching, and terminal services. [3] Toward that end, we use the UserAppDataPath property of the Application object. It's a path in the file system where per-user settings for an application are meant to be stored, and it takes the following form:
C:\Documents and Settings\ csells \Application Data\ SingleInstance\SingleInstance.0.1121.38811 What makes this string useful is that it contains the application name, the version number, and the user name ”all the things we need to make the mutex unique per version of an application and per user running the application. Also, because the back slashes are illegal in mutex names , those must be replaced with something else (such as underscores). Passing Command Line ArgumentsThis single-instance scheme works fine until the first instance of the application needs to get the command line arguments from any subsequent instance. For example, if the first instance of an MDI application needs to open the file passed to the other instance of the MDI application, the other instance needs to be able to communicate with the initial instance. The easiest solution to this problem is to use .NET Remoting and threading: Imports System.Threading Imports System.Runtime.Remoting Imports System.Runtime.Remoting.Channels Imports System.Runtime.Remoting.Channels.Tcp ... ' Make main form accessible from other instance event handler Shared mymainForm As MainForm = New MainForm() ' Signature of method to call when other instance is detected Delegate Sub OtherInstanceCallback(args() As String) <STAThread()> _ Shared Sub Main(args() As String) ' Check for existing instance Dim firstInstance As Boolean = False Dim safeName As String = _ Application.UserAppDataPath.Replace("\", "_") Dim mymutex As Mutex = New Mutex(True, safeName, _ ByRef firstInstance) If Not(firstInstance) Then ' Open remoting channel exposed from initial instance ' NOTE:port (1313) and channel (mainForm) must match below Dim formUrl As String = "tcp://localhost:1313/mainForm" Dim otherMainForm As MainForm = _ CType(RemotingServices.Connect(GetType(MainForm), _ formUrl), MainForm) ' Send arguments to initial instance and exit this one otherMainForm.OnOtherInstance(args) Exit Sub End If ' Expose remoting channel to accept arguments from other instances ' NOTE: port (1313) and channel name (mainForm) must match above ChannelServices.RegisterChannel(New TcpChannel(1313)) RemotingServices.Marshal(mainForm, "mainForm") ' Open file from command line If args.Length = 1 Then mainForm.OpenFile(args(0)) ' Show main form Application.Run(mainForm) End Sub Public Sub OnOtherInstance(args() As String) ... End Sub The details of .NET Remoting are beyond the scope of this book, and threading isn't covered until Chapter 14: Multithreaded User Interfaces, but the details are less important than the concepts. Basically what's happening is that the first instance of the application opens a named communication channel (mainForm) on a well-known, unique port (1313) in case another instance of the application ever comes along. If one does, the other instance opens the same channel and retrieves the MainForm type from it so that it can call the OnOtherInstance method. For this to work, the .NET Remoting assembly, System.Runtime.Remoting, must be referenced in the project. The Remoting infrastructure then requires that the type being retrieved by the other instance from the first instance be marshal-by-reference . This means that it can be called from another application domain , which is basically the .NET equivalent of a process. Because the MainForm class, along with all UI classes in .NET, derives from the MarshalByRef base class, this condition is met. Another .NET Remoting requirement is that the methods called on the marshal-by-ref type called from another app domain be instance and public, and that's why the OnOtherInstance method is defined as it is: ' Called via remoting channel from other instances ' NOTE: This is a member of the MainForm class Sub OnOtherInstance(args() As String) ' Transition to the UI thread If mainForm.InvokeRequired Then Dim callback As OtherInstanceCallback = _ New OtherInstanceCallback(AddressOf OnOtherInstance) mainForm.Invoke(callback, New Object() { args }) Exit Sub End If ' Open file from command line If args.Length = 1 Then Me.OpenFile(args(0)) ' Bring window to the front mainForm.Activate() End Sub When OnOtherInstance is called, it will be called on a non-UI thread. This requires a transition to the UI thread before any methods on the MainForm are called that access the underlying window (perhaps to create an MDI child to show the file being opened or to activate the window). That's why we include the check on the InvokeRequired required property and the call to BeginInvoke. [4]
Finally, OnOtherInstance does what it likes with the command line arguments and then activates the main form to bring it to the foreground. The details of this are somewhat complicated for the service that they provide, and this makes it a good candidate for encapsulation. The only real variables that can't be handled automatically are which method to call and what the main form is. This means that we can boil down the code to use the InitialInstanceActivator class provided in the sample code included with this book: ' Make main form accessible from other instance event handler Shared mymainForm As MainForm = New MainForm() <STAThread()> _ Shared Sub Main(args() As String) ' Check for initial instance Dim callback As OtherInstanceCallback = _ New OtherInstanceCallback(AddressOf OnOtherInstance) If InitialInstanceActivator.Activate(mymainForm, callback, args) Then Exit Sub End If ' Open file from command line If args.Length = 1 Then mymainForm.OpenFile(args(0)) ' Show main form Application.Run(mymainForm) End Sub ' Called from other instances Share Sub OnOtherInstance(args() As String) ' Open file from command line If args.Length = 1 Then mymainForm.OpenFile(args(0)) ' Activate the main window mymainForm.Activate() End Sub InitialInstanceActivator takes three things: a reference to the main form, a delegate indicating which method to call when another instance is detected, and the arguments from Main in case this is another instance. If the Activate method returns true, it means that this is another instance and the arguments have been passed to the initial instance. The application bails, safe in the knowledge that the initial instance has things well in hand. When another instance activates the initial instance, the delegate is invoked, letting the initial instance handle the command line arguments and activate itself as appropriate. The underlying communication and threading requirements are handled by InitialInstanceActivator using other parts of .NET that have nothing whatever to do with WinForms. This is one of the strengths of WinForms. Unlike forms packages of old, WinForms is only one part of a much larger, integrated whole. When its windowing classes don't meet your needs, you've still got all the rest of the .NET Framework Class Library to fall back on. Multi-SDI ApplicationsA multi- SDI application is like an MDI application in that it has multiple windows for content, but, unlike an MDI application, each window in a multi-SDI app is a top-level window. The Internet Explorer and Office XP applications are popular examples of multi-SDI applications. [5] Figure 11.4 shows a multi-SDI sample.
Figure 11.4. A Sample Multi-SDI Application
A multi-SDI application typically has the following features:
The single-instance stuff we've already got licked with the InitialInstanceActivator class. Having multiple top-level windows running independently of each other is a matter of using the modeless Form.Show method: Class TopLevelForm Inherits Form ... Sub fileNewWindowMenuItem_Click(sender As Object, e As EventArgs) NewWindow(Nothing) End Sub Shared Sub NewWindow(fileName As String) ' Create another top-level form Dim myform As TopLevelForm = New TopLevelForm() If Not (fileName Is Nothing) AndAlso (fileName.Length > 0) Then myform.OpenFile(fileName) End If myform.Show() End Sub End Class Because the default application context depends on a there being only one main window, managing the lifetime of a multi-SDI application requires a custom application context. One simple way to leverage the ApplicationContext base class is to derive from it, swapping the "main" form in our multi-SDI application until there are no more top-level windows left in the application, thereby causing the application to shut down: Class MultiSdiApplicationContext Inherits ApplicationContext Sub AddTopLevelForm(myform As Form) ' Initial main form may add itself twice, but that's OK If topLevelForms.Contains(myform) Then Exit Sub ' Add form to collection of forms and ' watch for it to activate and close topLevelForms.Add(myform) AddHandler myform.Activated, _ New EventHandler(AddressOf Form_Activated) AddHandler myform.Closed, _ New EventHandler(AddressOf Form_Closed) ' Set initial main form to activate If topLevelForms.Count = 1 Then MyBase.MainForm = myform End Sub Sub Form_Activated(sender As Object, e As EventArgs) ' Whichever form activated last is the "main" form MyBase.MainForm = CType(sender, Form) End Sub Sub Form_Closed(sender As Object, e As EventArgs) ' Remove form from the list topLevelForms.Remove(sender) ' Set a new "main" if necessary If (CType(sender, Form) = MyBase.MainForm) And _ Me.topLevelForms.Count > 0) Then Me.MainForm = CType(topLevelForms(0), Form) End If End Sub ... Public Property TopLevelForms() As Form() ' Expose list of top-level forms for building Window menu Get Return CType(topLevelForms.ToArray(GetType(Form)), _ Form()) End Get End Property Dim topLevelForms As ArrayList = New ArrayList() End Class The MultiSdiApplicationContext 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 Closed events. When a top-level form is activated, it becomes the new "main" form, which is the one that the base ApplicationContext class will watch for the Closed event. 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 main. When the last form goes away, the base ApplicationContext class notices and exits the application. With this basic functionality in place, we can use the application context in Main as the argument to Application.Run: ' Need application context to manage top-level forms Shared context As MultiSdiApplicationContext = _ New MultiSdiApplicationContext() <STAThread()> _ Share Sub Main(args() As String) ' Add initial form Dim initialForm As TopLevelForm = New TopLevelForm() context.AddTopLevelForm(initialForm) ' Let initial instance show another top-level form (if necessary) Dim callback As OtherInstanceCallback = _ New OtherInstanceCallback(AddressOf OnOtherInstance) If InitialInstanceActivator.Activate(context, callback, args) Then Exit Sub End If ' Open file from command line If args.Length = 1 Then initialForm.OpenFile(args(0)) ' Run application Application.Run(context) End Sub Because we're using the application context instead of the initial form as the argument to Application.Run, it will be used to control the lifetime of the application, even as the "main" form cycles. Similarly, we're using a context to the Activate method of the InitialInstanceActivator helper class, and this means that if another instance of the application starts, the activator can ask the context for the "current" main form to use in transitioning to the UI thread, even if the initial form has been closed. 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: Public Sub New() ' Required for Windows Form Designer support InitializeComponent() ' Add new top-level form to the application context context.AddTopLevelForm(Me) End Sub The only thing left to do is to designate and populate the Window menu with the current list of top-level forms. The forms themselves can do this by handling the pop-up event on the Window MenuItem object, using that opportunity to build the list of submenu items based on the names of all the forms (as exposed via the TopLevelForms property of the MultiSdiApplicationContext helper object). However, this code is pretty boilerplate , so it's a good candidate to be handled by the custom application context in the AddWindowMenu method: Class MultiMdiApplicationContext Inherits ApplicationContext Sub AddWindowMenu(menu As MenuItem) ' Add at least one dummy menu item to get pop-up event If menu.MenuItems.Count = 0 Then menu.MenuItems.Add("dummy") ' Subscribe to pop-up event AddHandler menu.Popup, _ New EventHandler(AddressOf WindowMenu_Popup) End Sub ... End Class Each top-level form with a Window menu can add it to the context, along with itself, when it's created: Public Sub New() ' Required for Windows Form Designer support InitializeComponent() ' Add new top-level form to the application context context.AddTopLevelForm(Me) ' Add Window MenuItem to the application context context.AddWindowMenu(Me.windowMenu) End Sub Now, when the Window menu is shown on any top-level window, the pop-up event fires and a new menu is built on-the-fly to show the current list of top-level menus : ' Current Window menu items and the map to the appropriate form Dim windowMenuMap As Hashtable = New Hashtable() Sub WindowMenu_Popup(sender As Object, e As EventArgs) ' Build menu from list of top-level windows Dim menu As MenuItem = CType(sender, MenuItem) menu.MenuItems.Clear() windowMenuMap.Clear() Dim theform As Form For Each theform In Me.topLevelForms Dim item As MenuItem = _ menu.MenuItems.Add(theform.Text) AddHandler item.Click, _ New EventHandler(AddressOf WindowMenuItem_Click) ' Check currently active window If theform = Form.ActiveForm Then item.Checked = True ' Associate each menu item back to the form windowMenuMap.Add(item, theform) Next End Sub 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. Because a MenuItem doesn't have a Tag property, we're using a Hashtable collection to map menu items to each form. The hash table is used in the Click handler to find the form that corresponds to the selected menu item: Sub WindowMenuItem_Click(sender As Object, e As EventArgs) ' Activate top-level form based on selection CType(windowMenuMap(sender), Form).Activate() End Sub That's it. The extensible lifetime management of WinForms 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 multi-SDI application in only a few lines of code. |