Console Application Basics
Like VB6 Windows applications, a console application needs a Sub Main as the starting point for the application. In Visual Basic .NET, console applications have no graphical user interface (GUI), so there is no form to designate as the main form. Also different from Windows applications is the means by which a console application gets input from the operator. Generally,console applications are not interactive, so a console application is generally a linear application. The operator starts the program passing the name of a file or files or command-line arguments to the console application, and it runs from beginning to end without interrupt.
You certainly could define user interaction with a console application by using Console.ReadLine and Console.Writeline, but if you need much more than initial input values, you might want to implement a Windows application. We won't talk about interactive input and output in this chapter. All of the input will be derived from the command line. Let's begin by implementing a simple console application's Sub Main procedure.
Implementing Sub Main in a Module
You can select the Console Application template from the New Project dialog box as shown in Figure 13.1. When you select the Console Application template, provide a name for the project. The name you enter will be the namespace for the application. Click OK to generate the template solution containing a module containing a default Sub Main, References, and the AssemblyInfo.vb file (see Figure 13.2).
Figure 13.1. Select the Console Application template applet to start a formless, standalone executable project.
Figure 13.2. Basic elements of a console application include a default Module1.vb file containing the starting point, Sub Main.
The default module, named Module1.vb, contains a single module definition with one procedure, Sub Main.
Module Module1 Sub Main() End Sub End Module
When your console application runs, it begins by executing the first line of code after the line Sub Main().
Modules exist for familiarity in Visual Basic .NET. Essentially, a module is a class where all of the members are implicitly defined as Shared members . Add Console. WriteLine("Hello World!") between the Sub Main and End Sub lines and press F5 to define and run a basic console application.
Fortunately, after you have started your console application, you can add as much or as little as necessary to solve your problem. Two added benefits are that you can define and implement classes and use inheritance in console applications, and you can implement the starting point for your console application in a class rather than a module.
Implementing Sub Main in a Class
The Sub Main starting point can be implemented as a Shared method in a class. The reason that Sub Main has to be shared is that the application begins running at Sub Main and as a consequence there are no existing objects. Of course, you can create an object from the Shared Main method, allowing you to use good object-oriented programming even in console applications. Listing 13.1 demonstrates a revised console application that uses a Shared Sub Main as the application starting point.
Listing 13.1 Define a class with a Shared Sub Main() to start console execution in a class
1: Public Class Main 2: 3: Public Shared Sub Main() 4: Console.WriteLine("Hello Console World!") 5: Threading.Thread.Sleep(2500) 6: End Sub 7: 8: End Class
The call to Thread.Sleep in line 5 of Listing 13.1 was added to provide time to observe output. There is no additional benefit of Thread.Sleep here.
The very short program defines a Shared procedure named Main in a class with the same name. Before the application will begin executing in Main.Main, we will need to tell the IDE that Main is our startup object. To make Main.Main our startup object, open the Solution Explorer as shown in Figure 13.2. Right-click on the project name and select Properties from the context menu. In the Property Pages dialog box (see Figure 13.3), select the General page. Pick the Main class as the startup object as shown in the figure.
Figure 13.3. Use the General page of the Project Property Pages to indicate the startup object in a console application.
Now when you run the demo application, the program will begin executing in Main.Main. Keep a few things in mind at this point. First, the startup point for the program is a shared method in the class. There is no instance of Main when the startup procedure is running. You can elect to create an instance of the Main class if the class contains primary capabilities, or you can create an instance of any other class defining your application's primary capabilities. Second, the procedure must be a procedure named main having no parameters, and the procedure may be in a module or a shared procedure in a class. Finally, command-line arguments can be retrieved by calling the Command function.
Overloading Sub Main
If you implement the startup code in a class, you can overload the Sub Main. Add the Overload directive and any parameters you want to distinguish to the overloaded Main procedure. An empty procedure overloading the Main method in Listing 13.1 is defined as follows :
Sub Overloads Shared Sub Main(ByVal Message As String) End Sub
If you added the Shared procedure to the Main class from Listing 13.1, you would need to add the Overloads modifier to the original startup procedure, too. You will not be able to designate the new Main as the startup procedure, but you could call the overload Main that takes a string parameter from the parameterless Main.
Retrieving Command-Line Arguments
Some languages allow you to define the startup routine in such a way as to indicate that command-line values are passed to the startup procedure. Visual Basic .NET returns command-line values via the Command function. If you are familiar with the Command$ function from VB6, this section will seem familiar to you.
Command in VB6 returns a String ValueType, which provides us with some additional capabilities you might find useful. Read the remainder of the section for a demonstration of parsing command-line arguments.
Using the Command Function
The Command function is defined in the Microsoft.VisualBasic namespace. The important thing to note is that the Command function returns a String, which means that you can use the capabilities of the String class to manipulate command-line arguments.
Splitting Command-Line Arguments into an Array
Often when you are creating a console application, you will read one or more arguments from the command line. Because the Command function returns the command-line arguments as a String object, we'll use String methods to manage command-line arguments rather than handcrafting custom command-line management tools.
In our sample applicationFileSort.sln, not listed yetwe will be retrieving a list of files containing text to sort . In advance we will designate a comma as the file delimiter for the command line and use the String.Split method to split each comma-delimited filename passed by the user. Listing 13.2 demonstrates the Main startup procedure and the code that processes the command-line arguments.
Listing 13.2 Using the String.Split method to parse the command-line arguments
9: Private Shared Sub ProcessEach(ByVal CommandLine As String) 10: 11: Dim Files() As String = CommandLine.Split(",") 12: Dim Enumerator As IEnumerator = Files.GetEnumerator 13: While (Enumerator.MoveNext()) 14: If (ValidCommandLine(Enumerator.Current)) Then 15: Sorter.Sort(Enumerator.Current) 16: End If 17: End While 18: 19: End Sub 20: 21: Public Shared Sub Process(ByVal CommandLine As String) 22: If (Help.WantsHelp(CommandLine)) Then 23: Help.Show() 24: 25: Else 26: ProcessEach(CommandLine) 27: End If 28: 29: End Sub 30: 31: Public Shared Sub Main() 32: Process(Command()) 33: End Sub 34: 35: End Class
Listing 13.2 is an excerpt from FileSort.sln and is listed using the line numbers in the order they appeared in the original listing before the revisions in Listing 13.3. Lines 31 to 32 define the Main procedure; keeping Main short is a convention employed in many languages and tools. In the listing, Main delegates processing to a procedure named Process, passing the command-line arguments as a string. Process is defined to scan the CommandLine argument to see if the user wants the help information. (Providing command-line help is not a requirement of any system, but you will see it employed via the -?, -h, or -help command-line arguments in many console applications. If the user does not want help (implementation of the help class is not shown), ProcessEach takes over and processes each argument in the command line.
From the section " Employ Refactoring: Replace Temp with Query" in Chapter 3, "Basic Programming in Visual Basic .NET," you know that it is a good idea to replace the temporary Files in Listing 13.2 with a query. Because String.Split plays the role of query, we can and should consolidate lines 11 and 12 of Listing 13.2 to Dim Enumerator As IEnumerator = CommandLine.Split(",").GetEnumerator.
After we have compiled and retested the code with the refactoring, we can remove the temporary Files.
The code was listed in its verbose form for clarity during presentation but the refactored version is preferable.
ProcessEach is defined on lines 9 to 19. ProcessEach calls String.Split on line 11 to parse the CommandLine string into an array of strings, passing the command (,) character as the delimiter. Line 12 requests an IEnumerator from the array returned by Split. Line 11 creates a temporary array, Files, and line 12 returns the Enumerator from the array.
Lines 13 to 17 use the Enumerator and after making sure each argument is valid for the FileSort.exe, Sorter.Sort is called for each filename passed to the command-line.
Working with Delimited Arguments
The FileSort.exe application anticipates the command line to take the following form: FileSort text1.txt,text2.txt,text3.txt. However, for practical applications, you can reasonably anticipate a desire for more flexibility. When building practical applications, you have a couple of choices: try to strangle the user into a perfect syntax, or relax the syntax requirement to make the application easier to use.
If you are the only user and are writing a utility for yourself, you may be aware of the syntax, and the cost of adding flexibility outweighs the benefit. In this case, write the simplest implementation possible. A real user might not appreciate having to provide a perfect syntax, though. If you are writing for other users, add some flexibility.
What would happen if the user typed something like FileSort text1.txt, text2.txt, text3.txt? (Note just the reasonable introduction of spaces after the comma.) The ProcessEach method would pass " text2.txt" to the Sorter.Sort method; that is, the filename would be preceded with a space and might not be a valid filename. To resolve this problem, you could trim the filename when it is passed to the ValidCommandLine and Sort methods. As an alternative, you could refine the call to Split, indicating that spaces might reasonably be in the command line.
Condensing and refining the call to Split from Listing 13.2 yields a replacement for lines 11 and 12:
11: Dim Enumerator As IEnumerator = _ 12: CommandLine.Split(", ".ToCharArray()).GetEnumerator()
Keep in mind that the literal ", " is converted to an implicit String object. ToCharArray() is being invoked on the implicit String created from the literal, and GetEnumerator is being invoked on the array of strings returned by Split. The new statement resolves the problem of additional delimiters, but the code is verbose and might be difficult to maintain because it is not very readable. Instead of removing the verbose code, introducing a lot of temporaries, or adding lengthy comments, we can introduce a query method that indicates what the code does and means. Query methods are preferable to monolithic code, temporary variables , and comments. Listing 13.3 demonstrates a revision that wraps the monolithic code in a method and removes all temporaries.
Listing 13.3 A revision of Listing 13.2 that removes temporaries and wraps monolithic statements into a well-named function method
9: Private Shared Function GetEnumerator(ByVal CommandLine As String) As IEnumerator 10: Return Command.Split(", ".ToCharArray()).GetEnumerator 11: End Function 12: 13: Private Overloads Shared Sub ProcessEach(ByVal CommandLine As String) 14: ProcessEach(GetEnumerator(CommandLine)) 15: End Sub 16: 17: Private Overloads Shared Sub ProcessEach(ByVal Enumerator As IEnumerator) 18: While (Enumerator.MoveNext()) 19: If (ValidCommandLine(Enumerator.Current)) Then 20: Sorter.Sort(Enumerator.Current) 21: End If 22: End While 23: End Sub 24: 25: Public Shared Sub Process(ByVal CommandLine As String) 26: If (Help.WantsHelp(CommandLine)) Then 27: Help.Show() 28: 29: Else 30: ProcessEach(CommandLine) 31: End If 32: 33: End Sub 34: 35: Public Shared Sub Main() 36: Process(Command()) 37: End Sub
Listing 13.3 introduces a query method that clearly indicates we are deriving an IEnumerator from the CommandLine. GetEnumerator is a private method. Producers will be the only ones modifying private methods; any producer that wants to modify GetEnumerator can determine its behavior from the name and parameter and can take some initiative in understanding how it works. Lines 13 to 15 and 17 to 23 provide two overloaded methods (ProcessEach) for processing each command-line argument. Process is unmodified and Sub Main remains unmodified.
Keep in mind that we could have used temporaries, comments, or left the verbose statement (line 10) in a single ProcessEach method. Our motivation for choosing a query method is straightforward: no temporaries, no comments, and the verbose statement is commented by the encapsulating query method. The final benefit of the query method GetEnumerator is that methods are reusable, but statements, comments, and temporaries are not.
There is no enforcing body that requires you to program in a way consistent with Listing 13.3, but there are supporting reasons for doing so. Until there is a standards body enforcing how code is written, you can write code any way that you want. (Such an enforcement body might ultimately base qualitative assessments on refactorings.) In the absence of an enforcement body, you are protecting yourself from digititis or carpal tunnel syndrome by writing less code and more reusable code.