(Print this page)

Passing arguments to an application startup
Published date: Wednesday, November 12, 2008
On: Moer and Éric Moreau's web site

It is unbelievable the number of time this question is asked into newsgroups. I finally decided to write an article on that topic.

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 and StartApplication are 2 methods that you need to write (more details on those later). But before getting there, we need to retrieve the arguments passed from the command line. There are at least 2 ways. The first one is the one I currently use and is also the one I prefer. By adding “ByVal pArgs() As String” in the method signature automatically feed the array of strings. Another method let you omit the parameters in the method signature and when you are ready to retrieve them, you can use that line:
GetCommandLineArgs(Environment.GetCommandLineArgs())
What I don’t like about this later syntax is that it always returns the full application path in the first array element.

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
As you can see, it is very not complex. It simply checks if expected words are found into the array of arguments. GlobalArguments is an instance of a structure that keeps the value of arguments:
'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
Now that I have parsed the arguments, I am ready to start the application in batch or interactive mode. I also detect that no arguments are provided and display a message specifying available parameters. This is the code that the StartApplication method contains:
'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
Before running this demo application, we are missing two other pieces.

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 purpose of this simple class is only to log a message to prove that something is being done when the application runs.

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
If you want to run and test the application from within the Visual Studio IDE, you need a way to provide arguments. To provide them, re-open the project properties and go to the Debug tab. In the “Start Options” section, there is a big textbox to let you enter whatever you want (and that your application is ready to receive) as command line arguments as shown in figure 2.

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
To this class, add the same GlobalArguments declaration and the 2 methods (StartApplication and GetCommandLineArgs) from the previous project. Finally, you need to add this method to get exactly the same results we got with the first project:
'The MAIN procedure
Public Shared Sub Main(ByVal pArgs() As String)
    GetCommandLineArgs(pArgs)
    StartApplication()
End Sub
It looks the same but it has an additional keyword: Shared. This also means that everything used by this method also need to be shared (because no instance of this class is created).

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
The second (and last) modification is to replace the code we are using to display the form. In the StartApplication method, search for these 2 lines of code:
'Launch the UIion.Run(fUI)
Comment them out and replace them with this:
'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)
Now build your application, open Windows Explorer and navigate to the folder containing your executable assembly of this project (somewhere like "....\Demo\Method2\bin\Debug\Method2.exe"). Run it by double clicking the .exe file. You should now have the beautiful form on the screen. Leave it open, return to your Windows Explorer and double-click the same .exe file. Instead of launching an instance of the same application, you will return to the one that is currently running. Isn’t it nice?

Yet another hint: Detecting keypress at load time

Instead of passing in an argument, you may want to detect a particular key press at load time. I use this trick in some application where I want special features to appear.

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
As you can see, I store the value to use it anywhere later. In this demo application, I simply show a message box if the Shift key is pressed.

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.


(Print this page)