In this demo, I will show you how you can use the same application that runs a regular Windows Forms interface and can also runs without any interface when a particular argument is passed. I often use this pattern for applications that I run from a scheduler. Even if those applications run quietly through a scheduler, I always provide a Windows interface for testing and debugging purpose.
This code you can download for this article was created using VB2008 SP1.
Method 1: The good old Sub Main
The method that is known by almost everybody is to start the application in a Sub Main method. This method needs to be public in a standard module. That means that the first thing you need to do is to add a new module to your application (I always call it modMain.vb so that I can find it very quickly). By the way, if you download the code for this article, you will find it the project titled Method1.
In that module, you then need to create a public sub method called Main. You also need to modify your project’s properties (see figure 1). To be able to set the Startup object to the method we just created, you need to uncheck the “Enable application framework”. Then in the Startup object combo, you will be able to select “Sub Main”.
Figure 1: Setting the Startup object
It is now time to write some code. Here is the code for my Sub Main:
'The MAIN procedure Public Sub Main(ByVal pArgs() As String) GetCommandLineArgs(pArgs) StartApplication() End Sub
GetCommandLineArgs(Environment.GetCommandLineArgs())
For this demo application, the GetCommandLineArgs method contains this code:
Private Sub GetCommandLineArgs(ByVal pArgs() As String) 'If no arguments are passed to the application, short-circuit the process If (pArgs.Length = 0) Then GlobalArguments.DisplayArgs = True End If 'Ensure all arguments are in upper case For intI As Int32 = 0 To pArgs.Length - 1 pArgs(intI) = pArgs(intI).ToUpper Next 'Search for expected arguments (all others being ignored) With GlobalArguments .DisplayArgs = (Array.IndexOf(pArgs, "/?") >= 0) .BatchMode = (Array.IndexOf(pArgs, "/BATCH") >= 0) If Array.IndexOf(pArgs, "/ENV_DEV") >= 0 Then .Environment = GlobalArgumentsStructure.enuEnvironment.Dev ElseIf Array.IndexOf(pArgs, "/ENV_QA") >= 0 Then .Environment = GlobalArgumentsStructure.enuEnvironment.QA ElseIf Array.IndexOf(pArgs, "/ENV_PRODUCTION") >= 0 Then .Environment = GlobalArgumentsStructure.enuEnvironment.Production Else .Environment = GlobalArgumentsStructure.enuEnvironment.Dev End If End With End Sub
'A little structure to group all argument settings togheter Public Structure GlobalArgumentsStructure Public Enum enuEnvironment Unknown = 0 Dev = 1 QA = 2 Production = 3 End Enum Public BatchMode As Boolean Public Environment As enuEnvironment Public DisplayArgs As Boolean End Structure 'An instance of the structure globally visible through the application Public GlobalArguments As New GlobalArgumentsStructure
'A method that branches depending on arguments received Public Sub StartApplication() If GlobalArguments.DisplayArgs Then 'Display the event args MessageBox.Show("This is a demo application from www.emoreau.com" & _ Environment.NewLine & _ "It is built to show how to use arguments to run an application." & _ Environment.NewLine & Environment.NewLine & _ "Available arguments are:" & _ Environment.NewLine & _ " /? : Display this message showing the available arguments." & _ Environment.NewLine & _ " /BATCH : To run the batch process (otherwise a menu is shown)." & _ Environment.NewLine & _ " /ENV_DEV : (default) To use the development database." & _ Environment.NewLine & _ " /ENV_QA : To use the QA database." & _ Environment.NewLine & _ " /ENV_PRODUCTION : To use the PRODUCTION databsae.", _ Application.ProductName, _ MessageBoxButtons.OK, _ MessageBoxIcon.Information) Application.Exit() ElseIf GlobalArguments.BatchMode Then 'Launch a process that has no UI cProcess.DoSomething("This line was added by a batch process.") Else 'Launch the UI Application.Run(fUI) End If End Sub
The first one is the cProcess class that has this very simple shared method:
Public Class cProcess 'This is a dummy method only to prove that the application ran Public Shared Sub DoSomething(ByVal pMessage As String) Using w As IO.StreamWriter = _ IO.File.AppendText(IO.Path.Combine(Application.StartupPath, "TraceFile.txt")) w.WriteLine(Date.Now.ToString("yyyy.MM.dd HH:mm:ss")) w.WriteLine(" " + pMessage) w.Close() End Using End Sub End Class
The last thing that is missing is the form used when the application starts in interactive mode (that I have named fUI). This form contains only a big button to launch the same process as in batch mode but with a different message:
Private Sub btnLaunchProcess_Click() Handles btnLaunchProcess.Click cProcess.DoSomething("This line was added by a process launched from the UI.") MessageBox.Show("Process completed", _ Application.ProductName, _ MessageBoxButtons.OK, _ MessageBoxIcon.Information) End Sub
Figure 2: Command line arguments
You can now play with the application changing the arguments and restarting the application. In the code you have seen here, the only arguments that really have impact are /? to display the help /batch to start the application in batch mode (no UI). You can see that I start all my arguments with a / and this is a very personal choice as it reminds me good old DOS days. Also be warned that the space character is considered a delimiter here.
You can also test your application from the command line or from the scheduler. It will run as expected (updating the little trace file).
Method 2: The Startup class
The second project uses a newer paradigm. We will reuse many pieces of code done in the previous project. If you download the code that goes with this article, you will find it in the project titled Method2. The cProcess.vb class and the fUI.vb form are exactly the same.
Instead of creating a module like we did in the previous method, this one requires us to create a class (which I always call cStartup) and this class must inherits from WindowsFormsApplicationBase as shown in the following code snippet:
Public Class cStartup Inherits Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase End Class
'The MAIN procedure Public Shared Sub Main(ByVal pArgs() As String) GetCommandLineArgs(pArgs) StartApplication() End Sub
You can now run and test the application from within the Visual Studio IDE exactly like you did in the first project. To this point both project gives the exact same results and have the exact same behaviour.
Another hint: Single instance application
So what’s the point of using the second method you will ask? The WindowsFormsApplicationBase class from which we inherited to create the cStartup class has some other nice features that the standard module makes difficult to achieve.
The example I will show you here is to limit the application to a single running instance. We have two easy modifications to do to our actual code to limit it.
The first modification is to set the IsSingleInstance property. This is normally done in the constructor of the class as shown in this code:
Public Sub New() Me.IsSingleInstance = True End Sub
'Launch the UIion.Run(fUI)
'Launch the UI - Take 2 Dim uri As String = Application.ExecutablePath Dim prog As New cStartup prog.MainForm = New fUI Dim para As String() = New String() {uri} prog.Run(para)
Yet another hint: Detecting keypress at load time
The trick is to check for the key you want (here I use the SHIFT) early when the application starts. I normally add this line as the first line in my Sub Main method:
GlobalArguments.ShiftedStartup = (System.Windows.Forms.Control.ModifierKeys And Keys.Shift) = Keys.Shift
Conclusion
With the help of some classed and a bit of imagination, you can start your application in different ways depending on command line arguments or special keys.
I hope you appreciated the topic and see you next month.