Chapter 2: Language Enhancements for Visual Basic 2005
Summary: Simplify and clarify your source code using the new keywords in the Visual Basic language. In addition, learn about the added functionality in the My namespace that was not available in previous releases of the .NET Framework. (34 printed pages)
Consuming Generic Types
Creating Generic Types and Methods
Application: The My Namespace
Scenarios for Using My.Application When Developing Applications
Scenarios for Using My.Computer When Developing Applications
Scenarios for Using My.User When Developing Applications
IsTrue, IsFalse, and Not Operators
Using the Using Keyword
Visual Basic 2005 introduces a number of enhancements designed to make your coding simpler and more efficient. In this chapter, you're introduced to four of those enhancements. Generics provide a means for you to design classes that can handle objects of different kinds efficiently and in a type-safe way. the My Namespace offers convenient shortcuts for accessing Base Class Library classes that refer to the currently running application, the local machine, the application's collection of forms, the logged-on user, and Web services associated with the application. By overloading operators such as = (equal to) and > (greater than), you can now define what happens when two instances of your class are compared. And finally, you'll review some new keywords.
This application illustrates the new Microsoft Visual Basic 2005 support for generics.
Before getting into the implementation of generics, it's worth spending a minute to analyze why this feature has been added to Visual Basic 2005. Generics come from the need to deal with potentially any type of object in a "generic" way. For example, you might want to create a class that can accept only Employee objects or Customer objects, and can process each one differently by distinguishing between them.
Even without generics, Visual Basic has always provided some mechanisms for dealing with objects, regardless of their type. For example, with Visual Basic 6.0, you can store objects of any type in a Collection object. This technique has some disadvantages, which are described in the next section. Generics provide an efficient alternative to Collections, enforcing type safety, providing better performance, and integrating tightly with the Microsoft IntelliSense technology.
Visual Basic 6.0 Collections
Visual Basic 6.0 really lets you store just about anything in a Collection. However, the Collection class has a number of limitations. For example, let's say that you want to store the following Employee class in a Collection.
' Visual Basic 6.0 Public SSN As String Public FirstName As String Public LastName As String Public Salary As Currency
Storing this in the collection is relatively straightforward.
Dim employees As New Collection Dim emp As Employee Set emp = New Employee emp.SSN = "111-11-1111" emp.FirstName = "Scott" emp.LastName = "Swigart" emp.Salary = 50000 employees.Add emp, emp.SSN
This code first creates an instance of a Collection named employees. An Employee object is then created and populated with data. Finally, this Employee object is added to the Collection. The following code shows how the Employee object could then be retrieved from the Collection.
Dim emp2 As Employee Set emp2 = employees("111-11-1111")
Now let's examine the limitations of the Visual Basic 6.0 Collection. First, it's likely that you want to store only Employee objects in the Employees collection. However, there is nothing to prevent anyone from storing any other type of object in the Employees collection. In addition, when you attempt to retrieve an item from the Employees collection, there's nothing to let you know what type you're retrieving. For example, the following code will compile just fine.
Dim s As String s = employees("111-11-1111")
Although it's obvious to the developer that this can't work, there's no way the compiler can catch this. This will show up as the worst kind of error, a run-time error. Collections also limit the ability of IntelliSense. Consider the following code.
employees("111-11-1111").LastName = "SomeoneElse"
This code shows that you can directly edit an item while it is in a Collection. However, you won't have any IntelliSense support when selecting the LastName property. Again, from the perspective of Visual Basic, that Collection could be storing anything. With Visual Basic 6.0, there's no way to say "Create a collection of employees."
Two final limitations of the collection class are performance and flexibility. Even though it's easy to use, a collection class performs poorly when used as a dynamic array. A collection class is also designed to work like a dictionary, so if you need something more like a Stack or Queue, it's not a good fit.
The .NET Framework versions 1.0 and 1.1 solved some of the problems with collections by simply offering more types of collections. By importing the System.Collections namespace, your code gained access to such collections as the ArrayList, BitArray, HashTable, Queue, SortedList, and Stack. The usage scenario for these types of collections is shown in Table 2-1.
Table 2-1. Collection Classes
|ArrayList||The ArrayList makes it easy to create arrays that grow dynamically.|
|BitArray||The BitArray class is optimized to store an array of Boolean (true/ false) values.|
|HashTable||The HashTable is most like the Visual Basic 6 Collection class. This class allows you to look up values by using a key. However, the key and the value can be any type.|
|SortedList||A SortedList is similar to a HashTable, except that the keys are always sorted. This means that if you use For…Each to iterate through the collection, you will always retrieve the items in a sorted order.|
|Queue||A Queue is a collection that supports a "first in, first out" model.|
|Stack||A Stack is just the opposite of a Queue, providing "first in, last out" functionality.|
The .NET Framework 1.0/1.1 did much to solve the flexibility limitations of Visual Basic 6.0; however, these collections are still loosely typed. This means that you can store anything in an ArrayList, even if, for a given application, it makes sense to store only a specific type.
What you really want is a collection where you can specify that every key must be a string and every value must be an Employee. With the .NET Framework 1.0/1.1, you would have been required to create your own class that wrapped the Hashtable and provided this functionality. With the .NET Framework 2.0, this problem is solved with far less code, using generics.
As the "generics" application shows, generics provide strict type checking, better IntelliSense functionality, and better performance. In other words, they address pretty much all the issues encountered with collection classes of the past.
First, it's worth mentioning that the .NET Framework 2.0 provides generic collections in addition to the existing .NET Framework 1.0/1.1 collection classes. The .NET Framework 2.0 in no way forces you to use generics.
If you do want to use the new generic types, you begin by importing the System.Collections.Generic namespace. This grants access to Dictionary, List, Queue, SortedDictionary, and Stack classes. An example of using the generic Dictionary is shown in the btnConsumeGenerics_Click event.
Private Sub btnConsumeGenerics_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnConsumeGenerics.Click Dim employees As New Dictionary(Of String, Employee) Dim emp As Employee emp = New Employee emp.SSN = "111-11-1111" emp.FirstName = "Scott" emp.LastName = "Swigart" emp.Salary = 50000 employees.Add(emp.SSN, emp) txtOutput.Text = "Consuming generics" & vbCrLf & vbCrLf Dim emp2 As Employee emp2 = employees.Item("111-11-1111") Dim s As String 's = employees.Item("111-11-1111") ' This is now a syntax error employees.Item("111-11-1111").LastName = "SomeoneElse" txtOutput.Text &= "Employee last name:" & vbCrLf & _ employees.Item("111-11-1111").LastName End Sub
If you walk through the code, you will notice a few interesting facts about generics. First, the generic type is instantiated as follows.
Dim employees As New Dictionary(Of String, Employee)
This code translates to "Create a Dictionary where the keys will be strings and the values will be Employees." Note the use of the new Of keyword to specify the desired data type for the keys. Any attempt to store a value other than an Employee object will result in a compile-time error. That's worth repeating. With generics, if you use the wrong type, it's a compile-time error, not a run-time error. In fact, the following line must be commented out or the application will not compile, as the compiler knows that the Dictionary holds Employee classes, not strings.
's = employees.Item("111-11-1111") ' This is now a syntax error
In addition, you now have full IntelliSense for the types in your collection. If you examine Figure 2-1, you can see that Microsoft Visual Studio knows that this Dictionary holds only Employee classes, and the properties of the Employee class are available through IntelliSense.
Figure 2-1. IntelliSense for items in a generic collection.
As you can see, generics are simple to use, they result in strongly typed code (which reduces the possibility for run-time errors), and they allow IntelliSense to be more functional. Those would be reasons enough to use generics, but generics have a number of additional advantages as well, including performance and code reuse.
One of the main reasons for including generics in the .NET Framework is performance. Simply put, they're faster than the previous collection classes because the compiler optimizes them specifically for the types they store. An example can be seen in the following code, which compares the performance of an array, ArrayList, and generic List:
Dim PerfTime As New Stopwatch txtOutput.Text = "Performance" & vbCrLf & vbCrLf Const iterations As Integer = 5000000 PerfTime.Start() Dim myArray(iterations) As Integer For i As Integer = 0 To iterations - 1 myArray(i) = i Next PerfTime.Stop() txtOutput.Text &= "Array time: " & PerfTime.Elapsed.TotalSeconds & vbCrLf myArray = Nothing GC.Collect() PerfTime.Reset() PerfTime.Start() Dim myArrayList As New ArrayList For i As Integer = 0 To iterations - 1 myArrayList.Add(i) Next PerfTime.Stop() txtOutput.Text &= "ArrayList time: " & PerfTime.Elapsed.TotalSeconds & vbCrLf myArrayList = Nothing GC.Collect() PerfTime.Reset() PerfTime.Start() Dim myList As New List(Of Integer) For i As Integer = 0 To iterations - 1 myList.Add(i) Next PerfTime.Stop() txtOutput.Text &= "List time: " & PerfTime.Elapsed.TotalSeconds & vbCrLf myList = Nothing GC.Collect()
This code stores 5 million values into a fixed-size array. The values are also stored in an Array- List, which grows automatically, and a generic List, which also grows as needed. The performance numbers (on the computer where this code was developed) tell an interesting story.
Array time: 0.0403548 ArrayList time: 1.1538257 List time: 0.3344849
Nothing is faster than an array that stores a specific type and never needs to resize. (Your numbers will vary from those shown.) However, for a collection class that grows dynamically, its performance compared to the static array is pretty good. And look at the ArrayList. Not good. It's less than one-tenth the speed of the static array. The problem is that the ArrayList is designed to store objects. Integers are not objects; they're value types. Before they can be stored into the ArrayList, they have to go through a process called "boxing," which converts the integer to an object. Boxing is expensive, and if you are storing value types (integer, DateTime, boolean, your own structures, and so on), you will notice a significant performance improvement using generic collections rather than the Object-based collections.
Note For more information about boxing and unboxing, see "Boxing Conversions" and "Unboxing Conversions" in the MSDN library.
You are not limited to simply consuming generic types with Visual Basic 2005. You have the ability to also create your own generic types and methods.
Generic Methods: You might want to create a generic method if there is some common algorithm you want to perform that is independent of any type. For example, a typical bubble sort walks through all the items in an array, comparing one item with the next and swapping the values as needed to sort the array.
If you know in advance that you're going to be sorting only integers, you could simply hard- code the Swap method for integer types. However, if you wanted to be able to sort any type, you could code a generic Swap as follows.
Private Sub Swap(Of ItemType) _ (ByRef v1 As ItemType, ByRef v2 As ItemType) Dim temp As ItemType temp = v1 v1 = v2 v2 = temp End Sub
Notice the Of ItemType. When the Swap method is called, in addition to the standard arguments, a data type must also be passed. This data type will be substituted for every instance of ItemType. The following is an example of a call to Swap.
Swap(Of Integer)(v1, v2)
This code tells the Swap method that it will be swapping integer types, causing every instance of ItemType in the original listing of Swap to be replaced with integer by the JIT compiler. The Swap method is essentially rewritten by the JIT to the following.
Private Sub Swap(ByRef v1 As Integer, ByRef v2 As Integer) Dim temp As Integer temp = v1 v1 = v2 v2 = temp End Sub
This is the code that's actually executed. The JIT generates a version of the method to handle integer types. However, if you later wanted to sort string types, you could have another call to Swap as follows.
Swap(Of String)(v1, v2)
When this method executes, the JIT will write another version of Swap, this one specialized for string types.
Private Sub Swap(ByRef v1 As String, ByRef v2 As String) Dim temp As String temp = v1 v1 = v2 v2 = temp End Sub
A full example of a bubble sort that uses Swap is as follows.
Private Sub btnSortIntegers_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSortIntegers.Click Dim ints(9) As Integer Dim r As New Random For i As Integer = 0 To 9 ints(i) = r.Next(1, 100) Next ' Bubble sort For j As Integer = 0 To 9 For k As Integer = 9 To 1 Step -1 If ints(k) < ints(k - 1) Then Swap(Of Integer)(ints(k), ints(k - 1)) End If Next Next txtOutput.Text = "Sort Integers" & vbCrLf & vbCrLf For i As Integer = 0 To 9 txtOutput.Text &= ints(i) & vbCrLf Next End Sub
Generic Types: As a final point, you can create entire generic classes. In this case, Of ItemType is used with the class declaration as follows.
Public Class SomeClass(Of ItemType) Private internalVar as ItemType Public Function SomeMethod(ByVal value As ItemType) As ItemType End Function End Class
The same rules apply to classes as methods. The JIT compiler will simply replace every instance of ItemType with a specific type when the class is instantiated.
Generics also support a feature known as constraints. This feature lets you ensure that when you specify types, they implement certain minimum functionality. For example, if you are implementing a generic sort algorithm, you might want to ensure that the type that you're sorting implements IComparible. You can accomplish this with a constraint.
Public Class SomeClass(Of ItemType As IComparible) Public Function SomeMethod(ByVal value As ItemType) As ItemType End Function End Class
Generics offer a number of advantages over Object-based collection classes. First, generic classes are strongly typed, which allows many errors to be caught at compile time rather than at run time. The strong typing also lets IntelliSense provide more information. Generics also allow you to reuse your code, creating generic algorithms that work with a variety of types. Finally, generic collections are just faster than Object-based collections, especially when dealing with value types.
Visual Basic 2005 introduces a speedy way to access many important classes relating to the computer on which your application is running, the user who is running it, the application itself, its forms, and any associated Web services. They are all accessible by using the new My Namespace.
When building applications with Visual Basic 6, you have access to the Visual Basic Run time, a wide variety of COM objects, and a Win32 API for whichever version of the Microsoft Windows operating system you're running on. The first versions of the .NET Framework unified much of this functionality into a single mammoth set of classes known as the Base Class Library. Within the Base Class Library, there are classes to support accessing information about the underlying operating system. There are classes that allow you to easily access information about the hardware on the machine. There are also classes that allow you to easily communicate across the network between different applications, encrypt data, provide access to the registry, and more.
Understanding the Base Class Library, and the wealth of functionality it offers, is important if you want to become an advanced .NET developer. A lack of knowledge about the Base Class Library can cause developers to reinvent the wheel and construct classes that already exist. In some cases, the functionality provided by the .NET Framework is so hidden or obscured by the size of the .NET Framework that things are overlooked. Numerous articles have been written that include a home-brewed algorithm to support actions that you could perform simply by knowing about such Framework classes as Path or PasswordDeriveBytes. In fact, many people will tell you that learning .NET isn't so much about learning Visual Basic .NET or C#, but that it is really about learning the Base Class Library.
However, learning the Base Class Library is also a mammoth challenge, simply because it is so large. The classes that are used every day are mixed in with other classes that you might never need. To make the common classes in the .NET Framework more discoverable, Visual Basic 2005 now provides a "speed dial" called the My Namespace. By using the My Namespace, you have the ability to easily access computer, application, and user information, as well as obtain access to Web services and forms. It is important to understand that the My Namespace is accessible to you only when writing Visual Basic .NET applications and is not directly available to you when using C#.
It is important to note that the My Namespace goes beyond being a simple speed dial. In some cases, the classes in the My Namespace provide more functionality than you easily find by simply searching through the various namespaces that make up the Base Class Library. For example, the Folder object provides additional properties, such as Drive, that are not available on the System.IO.DirectoryInfo class. the My Namespace also forced Microsoft to think of the computer, application, and user as complete entities and ask "What should you be able to do with a computer?" The result was methods such as My.Computer.Network.Ping. You will now find that functionality previously available only by calling into COM libraries, or the Win32 API, are now easily accessible and discoverable through the My Namespace.
The My Namespace is broken down into a few areas of focus, as detailed in Table 2-2.
Table 2-2. The My Namespace Model
|My.Application||Contains information about the running application, such as the title, working directory, version, and the common language runtime (CLR) version in use. It also gives access to environment variables, allows you to easily write to the local application log or to a custom log, and so on.|
|My.Computer||Contains information about the underlying platform and the hardware on the local machine that the application is running on.|
|My.Forms||A collection of all the instances of the forms in your current project.
Allows you to easily show and hide forms without having to explicitly create an instance of the form in code.
|My.Resources||Provides strongly typed access to resources included in your project. Provides IntelliSense for those resources.|
|My.Settings||Provides strongly typed access to settings in your project. Provides IntelliSense for those resources.|
|My.User||Contains information about the current user, including his display name, domain name, and so on.|
|My.WebServices||Allows you to easily access Web services that are added as Web References in your project.|
As you can see, the My Namespace gives you easy and direct access to functionality that you can use on a daily basis when building applications.
In this section, you'll gain a good understanding of the My Namespace by looking at a small sample application that explores it in some depth. The application has three tabs, as shown in Figure 2-2, each of which focuses on a particular subsection of the My Namespace.
Figure 2-2. My.Application tab.
The first tab with the text My.Application displays a data grid that exposes the values for many of the properties that are part of the My.Application object. The code that populates this grid with data is as follows.
With MyDataSet.MyInfo .Clear() Dim cmds As String = "" For Each cmd As String In My.Application.CommandLineArgs cmds &= ", " & cmd Next .AddMyInfoRow("Command Line Args", Mid(cmds, 2)) .AddMyInfoRow("Company Name", My.Application.Info.CompanyName) .AddMyInfoRow("Culture", My.Application.Culture.ToString()) .AddMyInfoRow("Description", My.Application.Info.Description) .AddMyInfoRow("Number of Procs", My.Application.GetEnvironmentVariable("NUMBER_OF_PROCESSORS")) .AddMyInfoRow("Directory Path", My.Application.Info.DirectoryPath) .AddMyInfoRow("Legal Copyright", My.Application.Info.Copyright) .AddMyInfoRow("Event Log Name", My.Application.Log.ToString) .AddMyInfoRow("Product Name", My.Application.Info.ProductName) .AddMyInfoRow("Title", My.Application.Info.Title) .AddMyInfoRow("Version", _ My.Application.Info.Version.Major & "." & _ My.Application.Info.Version.Minor & "." & _ My.Application.Info.Version.Revision & "." & _ My.Application.Info.Version.Build) End With
The preceding code is intentionally somewhat verbose. The code could have used reflection to iterate through the properties, but by listing them out in code, you can look at the application and get a better idea of what information is returned by a specific property. The individual value retrieved from a given property is added to the DataTable, dt, which is then bound to the Datagrid on the My.Application tab. You can gather a great deal of information about your application simply by accessing properties that make up the My.Application object. Table 2-3 lists the properties of the My.Application object.
Table 2-3. My.Application
|ApplicationContext||Gets the ApplicationContext object for the current thread of a Windows Forms application.
This property is available only for Windows Forms applications.
|CommandLineArgs||Gets a collection containing the command-line arguments as strings for the current application.|
|Culture||Gets the culture that the current thread uses for string manipulation and string formatting.|
|Deployment||Gets the current application's ClickOnce deployment object, which provides support for updating the current deployment programmatically and the on-demand download of files.|
|Info||Provides properties for getting the information about the application's assembly, such as the version number, description, and so on.|
|IsNetworkDeployed||Gets a Boolean that represents whether the application was network deployed.|
|Log||Provides a property and methods for writing event and exception information to the application's event log listeners.|
|MinimumSplashScreenDisplayTime||Gets or sets the minimum length of time, in milliseconds, for which the splash screen is displayed.|
|OpenForms||Gets a collection of all the application's open forms.|
|SaveMySettingsOnExit||Determines whether the application saves the user settings on exit.|
|SplashScreen||Gets or sets the splash screen for this application.|
|UICulture||Gets the culture that the current thread uses for retrieving culture-specific resources.|
The My.Application object provides you with a great deal of functionality with much less code than when building applications with the .NET Framework 1.0/1.1, Visual Basic 6, or both. In this section, we'll look at some ways you can use My.Application. For example, the code required to write to a Log has now been shortened to the following.
My.Application.Log.WriteEntry("Application Starting", _ EventLogEntryType.Information, Nothing)
By default, this code will write an entry to debug output, as well as to your application's log file. You can see the result either by running the application in Debug mode in Visual Studio and reading the output in the Output window, or by accessing the log file. This file, by default, is called assemblyname.log, and is located in the application's data directory, which is available as a property of My.Computer.
Dim AppLogLocation As String = _ My.Computer.FileSystem.SpecialDirectories.CurrentUserApplicationData _ & "\" & My.Application.Info.Title & ".log" MsgBox("Successfully wrote to log." & vbCr & _ "Log is located at: " & AppLogLocation)
Previously, if you wanted to write to the EventLog in the .NET Framework 1.0/1.1, you would need multiple lines of code to do so. With Visual Basic 6, you had some limited logging functionality through the App object, but you could not specify an event ID and you could not write to the System or Security log or create your own custom logs.
In addition, My.Application gives you direct access to a wealth of application-level information in a single line of code. Some examples of where you could use these aspects of My.Application follow:
- To quickly determine the folder in which your application is placed by using the Directory- Path property.
- To get quick access to assembly metadata such as the Product Name, Company Name, and so on.
The next piece of the My Namespace is the Computer object. The My.Computer object gives you access to information about the underlying platform and hardware that your application is running on. The My.Computer tab (shown in Figure 2-3), which is the second tab in the sample application, extracts some of the more interesting property values you can retrieve from the My.Computer object.
Figure 2-3. My.Computer tab.
The code that populates this grid with data is as follows.
With MyDataSet.MyInfo .Clear() .AddMyInfoRow("Name", My.Computer.Name) .AddMyInfoRow("Local Time", My.Computer.Clock.LocalTime) .AddMyInfoRow("GMT Time", My.Computer.Clock.GmtTime) .AddMyInfoRow("Resolution", My.Computer.Screen.Bounds.Width & _ "x" & My.Computer.Screen.Bounds.Height) .AddMyInfoRow("Online", My.Computer.Network.IsAvailable) .AddMyInfoRow("Physical Memory", My.Computer.Info.TotalPhysicalMemory) .AddMyInfoRow("OS", My.Computer.Info.OSFullName) .AddMyInfoRow("Mouse Wheel Exists", My.Computer.Mouse.WheelExists) .AddMyInfoRow("Mouse Buttons Swapped", My.Computer.Mouse.ButtonsSwapped) End With
The code is similar to the code used to populate the grid on the My.Application tab in the application. A DataTable, is populated with values from properties of the My.Computer object, and then bound to a grid.
The properties of the My.Computer object are listed in Table 2-4.
Table 2-4. My.Computer
|Audio||Returns the My.Computer.Audio Object, which provides access to the audio system of the computer and provides methods for playing .wav files.
This object is available only for non-server applications.
|Clipboard||Returns the My.Computer.Clipboard Object, which provides methods for manipulating the Clipboard.
This object is available only for non-server applications.
|Clock||Returns the My.Computer.Clock Object, which provides properties for accessing the current local time and Universal Coordinated Time (equivalent to Greenwich Mean Time) from the system clock.|
|FileSystem||Returns the My.Computer.FileSystem Object, which provides properties and methods for working with drives, files, and directories.|
|Info||Returns the My.Computer.Info Object, which provides properties for getting information about the computer's memory, loaded assemblies, name, and operating system.|
|Keyboard||Returns the My.Computer.Keyboard Object, which provides properties for accessing the current state of the keyboard, such as what keys are pressed, and provides a method to send keystrokes to the active window.
This object is available only for non-server applications.
|Mouse||Returns the My.Computer.Mouse Object, which provides properties for getting information about the format and configuration of the mouse installed on the local computer.
This object is available only for non-server applications.
|Name||Gets the computer name.|
|Network||Returns the My.Computer.Network Object, which can be used to access network types and events.|
|Ports||Returns the My.Computer.Ports Object, which provides a property and a method for accessing the computer's serial ports.
This object is available only for non-server applications.
|Registry||Returns the My.Computer.Registry Object, which can be used to read from and write to the registry.|
|Screen||Gets the Screen object that represents the computer's primary display screen.
This property is available only for non-server applications.
The My.Computer object gives you a great deal of access to the underlying platform that you can use in a variety of scenarios. For example, you can easily ping network addresses using the Network property and its associated Ping method.
Dim pingResult As Boolean = _ My.Computer.Network.Ping("localhost") If pingResult = True Then MessageBox.Show("Ping of localhost succeeded") Else MessageBox.Show("Ping of localhost was not successful") End If
The simple Ping method of the My.Computer.Network property returns True if the ping was successful, and False otherwise. Being able to ping an address such as this makes it easy to determine whether your application is able to communicate with a particular server. You can also easily determine the network connection status by simply using My.Computer.Network. IsAvailable. The IsAvailable property returns a value of true or false, depending on whether the computer has a current network connection.
Another good use of My.Computer in your applications is when you need access to the file system. My.Computer.FileSystem gives you better access to the file system with fewer lines of code than in previous versions of Visual Basic. With My.Computer.FileSystem, you can perform the following actions in one line of code:
- Read all the text in a file.
- Copy a folder while creating all the parent folders necessary to complete the path.
- Move a folder while creating all the parent folders necessary to complete the path.
If you wanted to read all the text from a file, you could accomplish this with the following code.
Copying and moving folders has also been made much easier.
My.Computer.FileSystem.CopyDirectory(sourcePath, targetPath, True, True)
The preceding code will take the folder that is the sourcePath and copy it to the targetPath. The last two Boolean arguments specify that the targetPath will be overwritten if it is found and that all the parent folders needed to create the targetPath will be created as necessary. With one line of code, you can determine the location of the My Documents folder.
It is also easy to work with individual files and folders using the FileIO object. For example, with one line of code you can easily rename the file you are working with.
The next piece of the My Namespace is the My.User object. The My.User object allows you to obtain information about the currently logged-in user, such as her username, display name, and the roles she belongs to. The My.User tab is shown in Figure 2-4.
Figure 2-4. My.User tab.
The code that populates this grid with data is as follows.
With MyDataSet.MyInfo .Clear() .AddMyInfoRow("Name", My.User.Name) .AddMyInfoRow("Authenticated", My.User.IsAuthenticated) .AddMyInfoRow("Is Admin", My.User.IsInRole("BUILTIN\Administrators")) End With
The code used to access current user information is quite simple. All you do is access the appropriate property of the User object. The user object is composed of the properties and methods shown in Table 2-5.
Table 2-5. My.User
|CurrentPrincipal||Gets or sets the current principal (for role-based security).
This is an advanced member; it does not show in IntelliSense unless you click the All tab.
|IsAuthenticated||Gets a value that indicates whether the user has been authenticated.|
|Name||Gets the name of the current user.|
|InitializeWithWindowsUser||Sets the thread's current principal to the Windows user that started the application.
This is an advanced member; it does not show in IntelliSense unless you click the All tab.
|IsInRole||Determines whether the current user belongs to the specified role.|
The My.User object gives you a good deal of information about the currently logged-on user. In many ways, My.User is an excellent example of the speed dial that the My Namespace provides for the developer learning or using the .NET Framework. In previous releases of the .NET Framework, if you wanted to get access to information similar to that provided by the My Namespace, you had to write code similar to the following.
Imports System.Security.Principal Imports System.Threading.Thread ... Dim winPrin as WindowsPrincipal = Thread.CurrentPrincipal MessageBox.Show(winPrin.Identity.Name)
You also had the option of writing the following code.
But with The My Namespace, you get the much more intuitive:
This code is a classic example of how the My Namespace performs the function of a speed dial for the .NET Framework. Most developers when first exposed to the .NET Framework would not natively assume that to get access to the currently logged-on user account name they would have to either access Principal objects or access the current process's identity from the executing thread for the application. Many developers would expect this information to be exposed at a higher level of abstraction. With the My Namespace, this type of information is easily and quickly accessible.
The My Namespace also provides simple, strongly typed, access to resources and configuration settings. Through the project properties, you can directly enter configuration settings for your application as shown in figure 2-5.
Figure 2-5. Entering application settings. (Click on the image for a larger picture)
Once the application settings have been entered, they can be accessed very easily through the My Namespace.
If My.Settings.ExpertMode Then ' Switch to expert mode End If
Resources are equally easy to work with. For example, if you want your application to contain custom images, you can simply drag and drop images into the resources tab of the project properties as shown in figure 2-6:
Figure 2-6. Adding resources to a project. (Click on the image for a larger picture)
You can insert all sorts of resources into your project, including strings, images, audio, icons, files, and more. These can also be accessed through the My Namespace as follows.
PictureBox1.Image = My.Resources.Sunset
As you'll learn in later chapters, two other significant areas of functionality make up the My Namespace. These are My.WebServices and My.Forms. Let's take a quick look at each of these before concluding this section of the chapter.
My.WebServices gives you the same easy access to Web services that are referenced in your project that you have when accessing data sources.
dgOrders.DataSource = _ My.WebServices.Northwind.GetOrders().Tables("Orders")
In this code, you can see that you do not have to create an instance of the proxy class for the Web service as you would have with the .NET Framework 1.0/1.1. In this way, My.WebServices provides an easy speed dial to all the Web References that are part of your project.
Last but definitely not least, My.Forms gives you access to a collection containing the application's forms. This restores a significant programming enhancement for Visual Basic desktop application development that was lost temporarily with the advent of .NET. If you programmed previously in Visual Basic 6, you know that the following code could be used to show one of the forms in your project.
With the advent of the .NET Frameworks 1.0/1.1, you were unable to do this as easily. The following code was required when you attempted to show a form in the .NET Framework 1.0/1.1.
Dim frm1 As New Form1() frm1.Show()
With My.Forms, all the forms in your project are easily accessible as part of this collection. In addition, you also have access to a default instance for all the forms in your project without even using My.Forms. Therefore, the following lines of code are equivalent.
You'll see many more uses of this method of displaying forms throughout the book. You'll also see My.WebServices and My.Forms covered in more detail later in the book.
The My Namespace has four main uses to you as a developer. First, it contains additional functionality that was not available to you in previous releases of the .NET Framework. Second, it provides a valuable speed dial to let you more quickly find and use .NET Framework Base Class Library functionality in your applications. Third, it fills in some gaps and lets you think of things, such as the computer, as logical entities. Fourth, it brings back some familiar coding syntax, such as Form1.Show.
This application introduces the new operator overloading features and the new IsNot operator that was added to the Visual Basic language.
Operator overloading allows you to define the behavior of your classes when used with intrinsic language operators such as +, -, <, >, =, and <>. The concept of using an operator such as + to operate on objects is not new in Visual Basic. For example, the following line of code operates on two string objects using the + operator.
str = "Hello, " + "world"
When applied to strings, the + operator concatenates two strings to produce a new string. Concatenation is an appropriate outcome for an addition operation on strings. Operator overloading in Visual Basic allows you to define the appropriate outcome for operations, such as addition, when applied to instances of your own classes.
Consider a scenario where you need to determine whether one employee has more seniority than another.
Dim emp1, emp2 As Employee emp1 = Employee.GetFromID(123) emp2 = Employee.GetFromID(155)
Seniority is the natural comparison between two Employee objects in this application, so the semantics of the next line of code should express "if emp1 is greater than emp2." With operator overloading, you can actually define how the > (greater than) operator works when used with two Employee objects. Once the > operator is defined, the following code would be valid.
If emp1 > emp2 Then ' Add code here End If
This application populates two list box controls with Employee objects. The user can then select two employees to see which employee has more seniority. For this application, seniority is determined solely on the basis of date hired.
Overloading operators is as simple as creating new methods. In fact, operator overloads are really just methods created with the Operator keyword. The definition of the > operator for the Employee class would look like this.
Public Shared Operator >(ByVal lhs As Employee, _ ByVal rhs As Employee) As Boolean ' Add code here End Operator
The operator must be marked Shared and at least one of its parameters must be of the enclosing type (Employee). The > operator is a binary operator, which means that it requires two operands: the left-hand side of the operator and the right-hand side of the operator. Only one of these operands has to be of the enclosing type. The other operand can be another type if you want to define how an Employee object is compared against something else (for example, an integer). A typical result of this code is shown in Figure 2-5.
Figure 2-7. Employee objects can be compared based on the hire date of the employees.
The arguments passed to an operator method can be Nothing (null references), so you need to check them before attempting to access properties or methods. The full implementation of the > operator for the Employee class includes logic for handling null references.
Public Shared Operator >(ByVal lhs As Employee, _ ByVal rhs As Employee) As Boolean If rhs Is Nothing Then Return lhs IsNot Nothing End If If lhs Is Nothing Then Return False End If Return rhs.HireDate > lhs.HireDate End Operator
When you define certain operators, Visual Basic requires that you also define the operator for the inverse operation. If you define the = (equal to) operator, you must define the <> (not equal to) operator. If you define the >= (greater than or equal to) operator, you must define the <= (less than or equal to) operator. If you define the > (greater than) operator, you must define the < (less than) operator. Fortunately, you can usually leverage the first operator to define the inverse operator. So instead of writing the inverse logic for the < operator, you simply reverse the operands and use the > operator.
Public Shared Operator <(ByVal lhs As Employee, _ ByVal rhs As Employee) As Boolean Return not rhs > lhs and not rhs = lhs End Operator
Similarly, the <> operator returns the logical inverse of the = operator.
Public Shared Operator <>(ByVal lhs As Employee, _ ByVal rhs As Employee) As Boolean Return Not lhs = rhs End Operator
The > operator overload method in this application makes use of the new IsNot operator. In previous versions of Visual Basic, determining that an object reference was not Nothing required applying the Not keyword to an Is keyword.
If Not obj Is Nothing Then
Although this syntax is perfectly logical, it can be awkward to use, especially when trying to read or verbalize the logic. With the new IsNot operator, the following If statement is possible.
If obj IsNot Nothing Then
This statement is logically identical to the previous If statement but reads more naturally: if the object is not nothing, then....
Operator overloading in Visual Basic includes the ability to overload two special operators named IsTrue and IsFalse. These operators cannot be called explicitly from your code, but they can be used by the compiler to compute a true or false value for an object in expressions from which a Boolean value is expected (such as an If statement). Consider the following use of an Employee object.
Dim emp As New Employee() If emp Then ' Add code here End If
This use of emp is valid only if the class definition of Employee contains a way for a Boolean value to be computed. This can be done by defining an IsTrue operator or a CType operator (described in the next section). The IsTrue operator for Employee returns the value of the Employee.IsActive field or false for a null reference.
Public Shared Operator IsTrue(ByVal emp As Employee) As Boolean If emp Is Nothing Then Return False Else Return emp.IsActive End If End Operator
When you define IsTrue, Visual Basic requires you to also define its inverse operator, IsFalse. In a simple case such as the Employee class, defining this inverse operator is very simple.
Public Shared Operator IsFalse(ByVal emp As Employee) As Boolean Return Not emp End Operator
However, the luxury of using the Not operator on the Employee class does not come for free. You must define that operator as well.
Public Shared Operator Not(ByVal emp As Employee) As Boolean If emp Then Return False Else Return True End Operator
Another special operator that you can overload in Visual Basic is the CType operator. CType is used to convert an expression from its original data type to a new data type. For example, CType("100", Integer) converts a string to an integer. By overloading the CType operator for your classes, you are able to define exactly how an instance of your class is converted to another data type. Three possible conversions for the Employee class in this application are Employee to string, Employee to date, and Employee to boolean. For each conversion, you must provide a separate overload of the CType operator.
Public Shared Widening Operator CType(ByVal emp As Employee) As String Return emp.Name End Operator Public Shared Widening Operator CType(ByVal emp As Employee) As Date If emp Is Nothing Then Return Nothing Else Return emp.HireDate End If End Operator Public Shared Widening Operator CType(ByVal emp As Employee) As Boolean If emp Is Nothing Then Return False Else Return emp.IsActive End Operator
CType operator overloads can be marked as either Widening or Narrowing. A conversion is narrowing if the target data type cannot express all possible values of the original data type. That is not the case with any of the conversions defined for Employee, so they have all been marked as Widening.
Note The Visual Basic compiler does not allow implicit narrowing conversions when using Option Strict On.
In the Employee class, the ToString method is overridden so that the employee's first and last names are returned rather than the class name. The Employee class has a CType operator that defines a widening conversion from an Employee to a string, so the code required in the ToString method is minimal. A conversion from Employee to string is implicit because the function has an explicit return type (string) and a widening conversion is defined from the Employee object to that return type.
Public Overrides Function ToString() As String Return Me End Function
Note Unless ToString is redefined (overridden), an object uses the ToString method inherited from System.Object, which returns the fully qualified class name of the object.
You can use operator overloading to create an effective and natural interface for working with your classes. However, poor choices for parameter and return types can make an application programming interface (API) confusing. For example, the greater than (>), less than (<), equal to (=), and not equal to (<>) operators should normally return Boolean values. A > operator that returns DateTime would be confusing and unnatural in most situations. The binary + operator should normally return the enclosing type, as should the unary + and ? operators.
Dim var1 As New MyCustomClass Dim var2 As New MyCustomClass Dim var3 As MyCustomClass = var1 + var2 var3 = -var3
For operators such as the binary operator, you might want to return a different data type. For example, the difference between two Employee objects in this application could be an integer value that represents the difference in months between the hire dates of the two employees.
Operator overloading lets you determine how operators such as = and > work when used with two instances of an object you've defined. To overload an operator, you simply have to create a method in your class that includes the Operator keyword. When overloading operators, you often must define their inverses. For example, for an overload of the = (equal to) operator, define a corresponding <> (not equal to) operator overload.
A well-planned set of operator overloads with appropriate parameter and return types can make your classes easier to use and promote more concise source code.
This application introduces the new Visual Basic keywords Using, Global, and Continue.
Language designers are generally conservative about adding new keywords to a programming language. A language has to be expressive enough to be useful, but adding too many keywords makes the language too complex to master. The new keywords being added to Visual Basic with the release of Visual Studio 2005 are designed to clarify or simplify your code in some common programming scenarios.
Visual Basic allows you to create your own namespaces to organize the classes in your applications and libraries. Meaningful namespaces can greatly increase the intuitiveness of your class hierarchy and the readability of your source code. But if you name one of your namespaces the same as a top-level namespace from the .NET Framework Base Class Library, the top-level namespace will be inaccessible. For example, you would be unable to declare a variable as System.String within the custom namespace Util.System because String is not defined in Util.System. The Util.System namespace eclipses the top-level System namespace.
The new Global keyword in Visual Basic allows you to access the root of the namespace hierarchy when your namespace hierarchy eclipses part of it. So a string variable inside the Util.System namespace can be declared as Global.System.String. See the "Walkthrough" section for an example.
The .NET Framework common language runtime contains a sophisticated memory management subsystem that deals with allocating and releasing memory for managed applications. Memory is reclaimed from unused objects by the garbage collector at appropriate times. The garbage collector will automatically call a special cleanup method known as a finalizer when it is freeing memory. However, there is no guaranteeing when objects will be finalized. This nondeterministic approach to cleaning up objects can be problematic when your objects are using unmanaged resources such as a file handle.
To work around the nondeterministic nature of garbage collection, your classes that use unmanaged resources should include some way for a programmer to explicitly clean up unmanaged resources before abandoning an object. The conventional way to do this is to implement the IDisposable interface, which requires a Dispose method. The programmer simply calls your Dispose method when an object is no longer needed. This approach requires the programmer to ensure that a call to Dispose is made at the end of every possible path of execution.
The new Using keyword in Visual Basic simplifies the use of Dispose methods by automatically calling Dispose for an object. In the following code segment, the Dispose method of obj1 is called automatically at the end of the Using block.
Dim obj1 As New MyCustomClass Using (obj1) ' Add code here End Using
The Using keyword can be applied to any object that implements the IDisposable interface. Using keywords can be nested so that Dispose is called automatically for any number of objects.
Most Visual Basic programmers have encountered a situation when certain iterations of a loop can be skipped. Previous versions of Visual Basic did not have an easy way to simply skip to the next iteration of a loop. This meant that you had to wrap the entire body of the loop inside conditional blocks. The Continue keyword in Visual Basic now allows you to skip to the next iteration of a loop without processing the rest of the loop body. The following While loop uses the Continue keyword to skip to the next iteration if the first column of the current record (in a SqlDataReader) is Nothing.
While dr.Read() If dr(0) Is Nothing Then Continue While ' Process current record in SqlDataReader End While
The Continue keyword can be used in a nested loop to skip to the next iteration of an outer loop.
While I < 1000 For J As Integer = 1 To 5 If I Mod J = 0 Then Continue While End If Next I += 1 End While
When an inner loop is the same type of loop as an outer loop, such as a For loop nested inside a For loop, the Continue keyword applies to the innermost loop.
The Keywords application is built around the FileWriter class defined in the Util.System.IO namespace.
Namespace Util.System.IO Public Class FileWriter Implements IDisposable Private _outWriter As Global.System.IO.StreamWriter Public Sub New(ByVal filename As String) _outWriter = Global.System.IO.File.CreateText(filename) _outWriter.WriteLine("Output started at " + Now.ToString()) End Sub Public Sub WriteLine(ByVal message As String) _outWriter.WriteLine(Now.ToString().PadRight(30) + message) End Sub Public Sub Dispose() Implements IDisposable.Dispose _outWriter.Close() MsgBox("StreamWriter closed by Dispose()") GC.SuppressFinalize(Me) End Sub Protected Overrides Sub Finalize() MsgBox("StreamWriter closed by Finalize()") End Sub End Class End Namespace
The FileWriter class is a simple wrapper around the StreamWriter class. A FileWriter object adds the current date and time to each line it writes to the file. The StreamWriter is closed either when the FileWriter.Dispose method is called by the consumer or when the Finalize method is called by the .NET Framework garbage collector.
The Dispose and Finalize methods contain MsgBox calls to tell you when each method is called. These calls are for illustrative purposes only in this application. You should not normally display message boxes from non-UI classes such as FileWriter.
As you can see in Figure 2-8, the user interface for the Keywords application is a single Windows form with two buttons. The click event handler for one button uses the Using statement with a FileWriter object. The click event handler for the second button does not use Using nor does it call Dispose—it relies on the garbage collector to call Finalize.
Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles Button1.Click Dim fw As New _ Util.System.IO.FileWriter( _ System.Environment.CurrentDirectory + "\button1.txt") Using (fw) For I As Integer = 1 To 100 If I Mod 3 = 0 Then Continue For End If fw.WriteLine(CStr(I)) Next End Using End Sub Private Sub Button2_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles Button2.Click Dim fw As New _ Util.System.IO.FileWriter( _ System.Environment.CurrentDirectory + "\button2.txt") For I As Integer = 1 To 100 If I Mod 3 = 0 Then Continue For End If fw.WriteLine(CStr(I)) Next End Sub
Figure 2-8. Keywords application user interface.
In both cases, the click event handlers use the FileWriter class to write to a file every integer from 1 to 100 except values that are evenly divisible by 3.
Using the Global Keyword
The FileWriter class uses a StreamWriter from the Base Class Library System.IO namespace. But System.IO cannot be accessed directly from the class definition because FileWriter is defined inside a System.IO namespace (Util.System.IO) that eclipses the System.IO namespace at the root of the namespace hierarchy. To work around this naming collision, the Global keyword is used to access System.IO from the Base Class Library.
Private _outWriter As Global.System.IO.StreamWriter
The Util.System.IO namespace does not contain a StreamWriter class, so declaring the variable as System.IO.StreamWriter or IO.StreamWriter would not compile. But using the Global keyword allows the compiler to resolve the class to the proper type from the .NET Framework class library.
A FileWriter object opens its internal StreamWriter when the object is created (that is, in its constructor) and keeps the StreamWriter open until Dispose or Finalize is called. In the Button1_Click event handler, the Using keyword is applied to the FileWriter object fw. At the end of the Using block, fw.Dispose is called automatically, which closes the underlying Stream-Writer object. The result is shown in Figure 2-7.
Figure 2-9. Message box displayed after Dispose is called automatically.
Dispose is called immediately after the Using block, so in this example the message box from Dispose is displayed immediately. By contrast, the second button creates a FileWriter but does not call Dispose. The message box from the Finalize method will appear at some seemingly arbitrary time when the garbage collector is cleaning up the object. Also note that the underlying stream cannot be closed in the finalizer because it may have already been garbage collected. The result is, if dispose is not called, data may not end up being written to the file. For this simple application, the finalizer message box is most likely to appear when you close the program.
Using the Continue Keyword
The click event handlers for the buttons in this application both write to file the integers from 1 to 100 that are not divisible by 3.
For I As Integer = 1 To 100 If I Mod 3 = 0 Then Continue For End If fw.WriteLine(CStr(I)) Next
The For loop that generates the numbers illustrates a simple use of the new Continue keyword. If I Mod 3 is 0, the current value of I is evenly divisible by 3. When this condition is true, Continue For moves execution to the next iteration of the loop without processing the remaining body of the For loop. If, for example, I is 6, Continue For would execute, which would move iteration back to the top of the loop with I equal to 7. A partial listing of the output would look like the following (note the absence of 3, 6, 9, and 12).
Output started at 12/12/2003 8:06:42 AM 12/12/2003 8:06:42 AM 1 12/12/2003 8:06:42 AM 2 12/12/2003 8:06:42 AM 4 12/12/2003 8:06:42 AM 5 12/12/2003 8:06:42 AM 7 12/12/2003 8:06:42 AM 8 12/12/2003 8:06:42 AM 10 12/12/2003 8:06:42 AM 11 12/12/2003 8:06:42 AM 13
The new keywords demonstrated in this application—Continue, Global, and Using—add extra convenience and flexibility to the Visual Basic language. The Continue keyword can help alleviate awkward logic in loops to deal with special cases that you do not want to process. The Global keyword gives you the flexibility to use any namespace names for your class hierarchy without making namespaces from the .NET Framework class library inaccessible. And the Using keyword relieves you of the responsibility of calling Dispose explicitly at the end of every path of execution in your program. These new keywords are incremental changes to the Visual Basic language. They will not dramatically change the way you write code, but in some situations they will serve to simplify or clarify your source code.