Visual Basic .NET is a significant enhancement over Visual Basic 6. For one thing, it is a truly object-oriented language. For another, it has the benefit of full access to the .NET Framework and its huge class library, reducing the amount of code you have to write.
The following applications illustrate many of the features of Visual Basic .NET that give you more power and convenience than ever before, including string manipulation, structured exception handling, walking the stack, working with text files, and a variety of other features.
This sample shows you how to create, search, and sort arrays. Arrays are commonly used for grouping similar items, such as employee names in a list. Arrays provide a convenient container in which to store items, and they make it easy to manipulate those items using a single set of code.
To create an array, you declare a variable with a number in parentheses after it—for example, Dim Employees(4) As String. This example creates an array that will hold not four but five String elements because .NET arrays are always zero-based. This means that the first element has an index of zero and the fifth element has an index of 4. You use the index to refer to a particular element, such as Employees.GetValue(2).
You can find out how many elements are in an array with the Length property (Employees.Length), and the GetUpperBound method reveals the upper bound (the highest index) of the array Employees.GetUpperBound(0).
Caution |
The upper bound of an array is always one less than the number of elements because the array is zero-based. Don’t confuse the Length with the upper bound. The Length is the actual number of elements. The upper bound is the highest index. |
You can also create and populate an array all at once, like this:
DimEmployees()AsString={"AndrewFuller", "NancyDavolio"}
Sometimes you don’t know ahead of time how many elements you’ll need in an array. For instance, you might be reading data from a database into an array and not know how many records there are. This situation is perfect for a dynamic array. You declare a dynamic array without an upper bound—for example, Dim Employees(). Later, when you know how big you need the array to be, you ReDim the array with the desired upper bound—for example, ReDim Employees(4).
You can ReDim as many times as you care to—enlarging or shrinking the array even after it has data in it—but each time you do, the array gets released and re- created. If you want to keep the existing values when you ReDim, use the Preserve keyword—for example, ReDim Preserve Employees(6).
A matrix array (also known as a rectangular array) has multiple dimensions, or ranks. You can think of a matrix array as a combination of rows and columns, like a spreadsheet. For example, an array of employee names might have a row for each employee and two columns, one for first names and one for last names.
To create a matrix array, you declare it with two upper bounds, one for each rank—for example, Dim Employees(3,1) As String. This matrix array has four rows and two columns. You access each element in the array by using a separate index for each rank—for example, Employees.GetValue(2,0) would access the element in row 3, column 1.
As with single-rank arrays, you can short-circuit the creation process by declaring and populating the array all at once, as in the following example. Notice the comma between the parentheses to indicate two dimensions and the curly braces around each pair of array items and around the entire set.
DimEmployees(,)AsString={{"Fuller", "Andrew"},{"Davolio", "Nancy"}}
You’ll often want to sort your arrays, and the good news is that it’s a piece of cake. Just use the following structure: Array.Sort(Employees). Sort is a Shared method, which means you invoke it on the class itself (Array), not on an instance of the class (Employees). Figure 2-1 shows a before-and-after view of a sorted array.
Figure 2-1: With Visual Basic .NET, it’s easy to create, sort, and search arrays.
You’ll need to sort your array if you want to search it because the items must be in sequence for the very efficient BinarySearch method to work. Be aware, however, that both Sort and BinarySearch work only with one-dimensional arrays.
The sample application demonstrates the use of static arrays of value types and of object types as well as dynamic arrays. The sample also includes code for sorting and searching an array and for implementing matrix arrays.
Static Arrays of Value Types
To examine the concepts mentioned in the previous section in action, run the sample, select Strings from the Array Of group box, and click the Create Static Array button. The application creates an array of type String with five elements and displays the elements in the first list box on the form.
In this snippet from btnCreateStatic_Click, as the peopleList array is populated, its upper bound is automatically set to 4 because five names are passed to it.
DimpeopleList()AsString={"JoeHasterman" , "TedMattison",_ "JoeRummel", "BrianGurnure", "DougLandal"}
The contents of the array are displayed using the DisplayArrayData procedure, which adds each array element to a ListBox control. The heart of the display procedure is in the following three lines, in which the GetValue method retrieves the array element whose index is i, and returns it as a String, using the ToString method. The variable i is simply a counter, and u contains the array’s upper bound, which was set earlier with the statement Dim u As Integer = (arr.Length - 1).
Fori=0Tou lst.Items.Add(String.Format("{0}={1}", i,_ arr.GetValue(i).ToString())) Next
Static Arrays of Object Types
Now click Objects in the Array Of group box and, once again, click the Create Static Array button. This time, the application creates an array of five Customer objects. The Customer class has a parameterized constructor, so we can pass data (in this case an Id and Name) to each new Customer instance as it is instantiated. Note that because this is an array of objects, you must instantiate each item with the New keyword.
DimcustData()AsCustomer={NewCustomer(34 23, "JoeHasterman"),_ NewCustomer(9348, "TedMattison"),NewCustomer(3581,_ "JoeRummel"),NewCustomer(7642, "BrianGurnure"),_ NewCustomer(2985, "DougLandal")}
Once again, the application displays the contents of the array as strings. However, the Customer class has its own version of ToString, which formats and returns the Id and Name in a single string.
Dynamic Arrays
The btnCreateDynamic_Click event procedure demonstrates how to create a dynamic array: declare it, ReDim it, and populate it.
DimdynamicData()AsString ReDimdynamicData(System.Convert.ToInt32(Me.t xtLength.Text)-1) DimiAsInteger Fori=0TodynamicData.Length-1 dynamicData(i)=InputBox("Enterastring ",i.ToString(),_ "None " &i) Next
Creating a dynamic array of objects is equally easy. In a loop, you instantiate new objects one by one, setting properties of each one as needed. In this example, the Id and Name properties are set for each new customer:
DimdynamicData()AsCustomer ReDimdynamicData(System.Convert.ToInt32(Me.t xtLength.Text)-1) DimiAsInteger Fori=0TodynamicData.Length-1 dynamicData(i)=NewCustomer() dynamicData(i).Id=((i+1)*10) dynamicData(i).Name=InputBox("Enteras tring",("Item " &_ (i+1)),("None " &i+1)) Next
Sorting an Array
As the btnSort_Click event procedure shows, you need just two lines of code to create and sort either a value-type array or an object-type array:
DimpeopleList()AsString={"JoeHasterman" , "TedMattison", "JoeRummel",_ "BrianGurnure", "DougLandal"} Array.Sort(peopleList) ⋮ DimcustData()AsCustomer={NewCustomer(34 23, "JoeHasterman"),_ NewCustomer(9348, "TedMattison"),NewCustomer(3581, "JoeRummel"),_ NewCustomer(7642, "BrianGurnure"),NewCustomer(2985, "DougLandal")} Array.Sort(custData)
If you take a look at the Customer class, you’ll notice that right after the class declaration is the statement Implements IComparable. Because the class implements that interface and also has a CompareTo procedure, an array of Customer objects is both sortable and searchable. The class also has a mechanism for selecting which field to sort on, either Name or ID. If you’re creating a class and want it to permit sorting, searching, or both, you need to follow this pattern.
Searching an Array
Finding an item in your array is slightly more challenging than sorting, but the very efficient BinarySearch method makes it almost a breeze. First, for the search to work correctly, the array must be sorted. Then you call the BinarySearch method as shown in the btnBinarySearch_Click event procedure. The method returns an Integer value that represents the position in the array where the item was found.
position=Array.BinarySearch(strData,strDat aToFind) Ifposition>=0Then formattedMsg=String.Format("Thevalue{ 0}wasfoundinthearrayat " &_ "position{1}.",dataToFind,position.ToStrin g())
If the BinarySearch method doesn’t find the search item, it returns a negative number that’s the bitwise complement for the location where the item would have been if it existed. So a return value of -4 means that the item would have belonged at position 3 (the fourth element) if it actually existed. A return value of -6 would mean position 5 (the sixth element), and so on. To flip the negative number to its corresponding positive number, you use the Not operator.
Else DimbWCAsInteger=(Notposition)
If the result of Not is zero, the item you didn’t find would have been before the first item in the array if it existed. If the result is one greater than the upper bound of the array, the missing item would have been last in the array. If the result of Not is anything else, you can figure out which items it would have fitted between.
IfbWC=0Then formattedMessage=String.Format("The value{0}wasNOTfoundin " &_ "thearray.Ifitdidexistitwouldbeatpo sition{1} " &_ "before{2}",dataToFind,bWC.ToString(),peo pleList(0)) ElseIfbWC=UBound(peopleList)+1Then formattedMessage=String.Format("The value{0}wasNOTfoundin " &_ "thearray.Ifitdidexistitwouldbeatpo sition{1} " &_ "after{2}",dataToFind,bWC.ToString(),_ peopleList(UBound(peopleList))) Else formattedMessage=String.Format("The value{0}wasNOTfoundin " &_ "thearray.Ifitdidexistitwouldbeatpo sition{1} " &_ "between{2}and{3}.",dataToFind,bWC.ToStr ing(),_ peopleList(bWC- 1),peopleList(bWC)) EndIf EndIf
Matrix Arrays
Creating a matrix array (rectangular array) is simple, as you can see in the following btnCreateMatrix_Click event procedure:
DimstrMatrix(,)AsString={{"Bob", "Carol"},{"Ted", "Alice"},_ {"Joe", "Lisa"}}
If you want to access all the values in a matrix array, you’ll need two loops, one inside the other. The outer loop cycles through the rows, and the inner loop cycles through the columns. Note the use of GetLength(n) to retrieve the length of each dimension. Also note GetValue(n, n) to access each array element in turn.
Fori=0To(arr.GetLength(0)-1) Forj=0To(arr.GetLength(1)-1) lst.Items.Add(String.Format("({0},{1 })={2}",i,j,_ arr.GetValue(i,j).ToString())) Nextj Nexti
If you really have to, you can consider creating arrays with more than two dimensions, but be aware that managing such arrays is challenging.
Arrays make it simple to work with multiple objects as if they were a single object while allowing you easy access to the individual objects. .NET arrays make operations such as sorting and searching easy.
This sample introduces you to most of the properties and methods of the DateTime and TimeSpan classes, and it allows you to work interactively with them.
The DateTime structure in .NET is different from the Date type in Visual Basic 6. Unlike the Visual Basic 6 Date, which was really a Double type in disguise, DateTime is a value type defined in the Microsoft Windows .NET Framework library and is available to all .NET languages. You can instantiate and initialize a DateTime class like this
DimmyDateAsNewDateTime(2004,9,15)
and it becomes September 15, 2004.
A TimeSpan object represents a period of time, measured in ticks, 100-nanosecond intervals. TimeSpan values can be positive or negative and can represent up to one day. The TimeSpan structure is useful for setting and manipulating periods such as time-out intervals or expiration times of cached items. TimeSpan structures can also be initialized like DateTime structures.
This sample application illustrates the use of the DateTime and TimeSpan classes.
DateTime Shared Members
The shared DateTime properties and methods are demonstrated in the LoadCalculationMethods procedure. Shared properties include:
lblNow.Text=DateTime.Now.ToString lblToday.Text=DateTime.Today.ToString lblUtcNow.Text=DateTime.UtcNow.ToString lblMinValue.Text=DateTime.MinValue.ToString lblMaxValue.Text=DateTime.MaxValue.ToString
Shared methods include:
lblDaysInMonth.Text=DateTime.DaysInMonth(_ CInt(txtYear.Text),CInt(txtMonth.Text)). ToString lblFromOADate.Text=_ DateTime.FromOADate(CDbl(txtFromOADate.Te xt)).ToString lblIsLeapYear.Text=_ DateTime.IsLeapYear(CInt(txtIsLeapYear.Te xt)).ToString
Figure 2-2 shows the wide variety of DateTime properties you can access.
Figure 2-2: DateTime properties include just about any date-related or time-related information you might need.
DateTime Calculation Methods
DateTime instances permit a variety of calculations whose methods begin with Add. To subtract, simply pass a negative number as the parameter (or use the Subtract method). Most of the calculation methods are self-explanatory and most require a Double data type as a parameter, which means you can add or subtract fractional numbers—such as adding 1.5 hours to the current time. The exceptions are AddMonths and AddYears, which require Integers, and AddTicks, which accepts a Long value. (See the LoadCalculationMethods procedure for these examples.)
DimdtAsDateTime=DateTime.Now lblNow3.Text=dt.ToString lblAddDays.Text=dt.AddDays(CDbl(txtDays.Tex t)).ToString lblAddHours.Text=dt.AddHours(CDbl(txtHours. Text)).ToString lblAddMilliseconds.Text=_ dt.AddMilliseconds(CDbl(txtMilliseconds.T ext)).ToString lblAddMinutes.Text=dt.AddMinutes(CDbl(txtMi nutes.Text)).ToString lblAddMonths.Text=dt.AddMonths(CInt(txtMont hs.Text)).ToString lblAddSeconds.Text=dt.AddSeconds(CDbl(txtSe conds.Text)).ToString lblAddTicks.Text=dt.AddTicks(CLng(txtTicks. Text)).ToString lblAddYears.Text=dt.AddYears(CInt(txtYears. Text)).ToString
DateTime Properties
In addition to the DateTime shared properties, you can access a number of useful properties specific to a particular DateTime instance: instance properties. First, you declare and assign a value to a DateTime variable, as shown in the following code, taken from the LoadProperties procedure in frmMain.
DimdtAsDateTime=DateTime.Now
Once you have the object variable, you can access a variety of properties, including the following ones. (See the LoadProperties procedure for implementation of these properties.)
DateTime Conversion Methods
Various instance methods let you convert DateTime instances. For example, you might have data gathered from a Web service using Coordinated Universal Time (UTC) and find that you need to convert to local time. These Date/Time conversion methods, demonstrated in the LoadConversionMethods procedure, provide just the capability you need:
Here is how these properties are accessed in the LoadConversionMethods procedure:
DimdtAsDateTime=DateTime.Now lblNow2.Text=dt.ToString lblToFileTime.Text=dt.ToFileTime.ToString lblToLocalTime.Text=dt.ToLocalTime.ToString lblToLongDateString.Text=dt.ToLongDateStrin g lblToLongTimeString.Text=dt.ToLongTimeStrin g lblToOADate.Text=dt.ToOADate.ToString lblToShortDateString.Text=dt.ToShortDateStr ing lblToShortTimeString.Text=dt.ToShortTimeStr ing lblToString.Text=dt.ToString lblToUniversalTime.Text=dt.ToUniversalTime. ToString
TimeSpan Properties
In the btnRefreshTSProperties_Click procedure, you establish a TimeSpan by subtracting a beginning DateTime from an end DateTime. The Duration method returns an absolute value for the TimeSpan, even if its value had been negative.
DimtsAsTimeSpan DimdtStartAsDateTime DimdtEndAsDateTime 'Parsethetextfromthetextboxes. dtStart=DateTime.Parse(txtStart.Text) dtEnd=DateTime.Parse(txtEnd.Text) ts=dtEnd.Subtract(dtStart).Duration
You can also create a TimeSpan from raw text, like “5.10:27:34.17”. The TimeSpan Parse method interprets this as “5 days, 10 hours, 27 minutes, 34 seconds and 17 fractions of a second.” See the btnCalcParse_Click procedure for a demonstration of the Parse method.
You can access the properties of TimeSpan as demonstrated in the DisplayTSProperties procedure, which dissects and displays the individual parts of the TimeSpan. The property names are self-explanatory.
lblDays.Text=ts.Days.ToString lblHours.Text=ts.Hours.ToString lblMilliseconds.Text=ts.Milliseconds.ToStri ng lblMinutes.Text=ts.Minutes.ToString lblSeconds.Text=ts.Seconds.ToString lblTimeSpanTicks.Text=ts.Ticks.ToString lblTotalDays.Text=ts.TotalDays.ToString lblTotalHours.Text=ts.TotalHours.ToString lblTotalMilliseconds.Text=ts.TotalMilliseco nds.ToString lblTotalMinutes.Text=ts.TotalMinutes.ToStri ng lblTotalSeconds.Text=ts.TotalSeconds.ToStri ng
TimeSpan Methods
Most TimeSpan methods are shared methods. Those illustrated in the LoadTSMethods procedure are among them. They each produce a TimeSpan from a Double (except FromTicks, which accepts a Long). This allows you to accept a value from a user or use the output of a previous operation, and it allows you to turn the value in that string into a TimeSpan object representing that value. For example, FromDays will produce a TimeSpan based on the number of days passed to it. FromHours turns a number into a TimeSpan with that many hours, and so on.
lblFromDays.Text=TimeSpan.FromDays(CDbl(txt FromDays.Text)).ToString lblFromHours.Text=TimeSpan.FromHours(CDbl(t xtFromHours.Text)).ToString lblFromMilliseconds.Text=_ TimeSpan.FromMilliseconds(CDbl(txtFromMil liseconds.Text)).ToString lblFromMinutes.Text=_ TimeSpan.FromMinutes(CDbl(txtFromMinutes. Text)).ToString lblFromSeconds.Text=_ TimeSpan.FromSeconds(CDbl(txtFromSeconds. Text)).ToString lblFromTicks.Text=TimeSpan.FromTicks(CLng(t xtFromTicks.Text)).ToString
TimeSpan Fields
The fields of a TimeSpan are all either read-only or constants. MinValue and MaxValue represent the smallest and largest values, respectively, that a TimeSpan can hold, and they’re read-only. All the fields beginning that start with TicksPer are constants representing the number of ticks in a given period of time. The Zero field is a constant intended to give you a convenient source for 0 in time calculations. Examples are:
lblMaxValueTS.Text=TimeSpan.MaxValue.ToStri ng lblMinValueTS.Text=TimeSpan.MinValue.ToStri ng lblTicksPerDay.Text=TimeSpan.TicksPerDay.To String lblTicksPerHour.Text=TimeSpan.TicksPerHour. ToString lblTicksPerMillisecond.Text=TimeSpan.TicksP erMillisecond.ToString lblTicksPerMinute.Text=TimeSpan.TicksPerMin ute.ToString lblTicksPerSecond.Text=TimeSpan.TicksPerSec ond.ToString lblZero.Text=TimeSpan.Zero.ToString
In this sample application you’ve seen that working with dates and times is greatly simplified in the .NET environment. Adding and subtracting days, hours, minutes, and so on, is intuitive and easy to do. DateTime has a number of other methods, such as Compare, which accepts two DateTime instances and returns a value indicating whether they are equal, whether one is greater than the other, and so on. You’ve also seen that working with time intervals for timeouts and expiration times is convenient with the new TimeSpan structure.
This sample demonstrates many methods of the Visual Basic .NET String class. The sample form divides the methods into three groups: methods that return strings (such as Insert and Remove), methods that return information (such as IndexOf), and shared methods (such as String.Format). In addition, the demonstration introduces two other useful string handling classes: StringBuilder and StringWriter.
The String class, part of the System namespace, provides the data type for all strings. A String object is truly an object: it’s allocated on the heap like all other objects, and it’s subject to garbage collection. The String class offers a variety of methods for string manipulation, comparison, formatting, and so forth. Characters in a String object are always Unicode.
String manipulation is one of the most expensive operations you can perform, in terms of system resources. This is even more true in .NET, where all strings are immutable—that is, once you create a string, you can’t add to it, subtract from it, or change its value in any way. When you append a string to another, for example, the .NET runtime actually creates a new string with the old and new strings combined, and then it makes the original string available for garbage collection.
To the rescue comes the StringBuilder class, which is not a string but an object in its own right. It has a special internal buffer for manipulating a string far more quickly and efficiently than you could do otherwise. StringBuilder is most useful when you need to do repeated or large-scale manipulating of strings. It has methods for inserting, appending, removing, and replacing strings—and when you’re done, you extract the result with the ToString method. StringBuilder is part of the System.Text namespace.
The StringWriter class is an implementation of the abstract TextWriter class, and its purpose is to write sequential character information to a string. It writes (under the hood) to an underlying StringBuilder object, which can already exist or be created automatically when the StringWriter is initialized. With a StringWriter, you have, in effect, an in-memory file to which you can write at will. StringWriter belongs to the System.IO namespace. Figure 2-3 shows the String Manipulation sample application in action.
Figure 2-3: Manipulating strings is easier than ever with Visual Basic .NET.
For clarity in this walkthrough, we’ll present the code slightly modified to show the actual strings and numbers being passed as parameters, rather than the code’s CInt(strParam1), strParam2, and so forth. We’ll examine each method in the sequence presented on the form’s tabs. Unless we say otherwise, the original sample string is “the quick brown fox jumps over the lazy dog”. Keep in mind that the indexes used are zero-based.
Methods that Return Strings
Some methods of the String class return a string. These methods are:
txtResults.Text=sampleString.Insert(19, " happily")
txtResults.Text=sampleString.Remove(10,6)
txtResults.Text=sampleString.Replace("jumps ", "leaps")
txtResults.Text=sampleString.PadLeft(10) txtResults.Text=sampleString.PadLeft(10, "$")
txtResults.Text=sampleString.Substring(12) txtResults.Text=sampleString.Substring(12, 5)
txtResults.Text=sampleString.ToLower txtResults.Text=sampleString.ToUpper
txtResults.Text=sampleString.Trim() txtResults.Text=sampleString.Trim("thedog" .ToCharArray())
Methods that Return Information
The String class has a number of methods that help you get information about a string, including:
txtResults.Text=sampleString.IndexOf("brown ",4,20).ToString txtResults.Text=sampleString.IndexOf("brown ",4).ToString txtResults.Text=sampleString.IndexOf("brown ").ToString
txtResults.Text=_ sampleString.IndexOfAny("abc".ToCharArray ,4,12).ToString txtResults.Text=_ sampleString.IndexOfAny("abc".ToCharArray ,4).ToString txtResults.Text=_ sampleString.IndexOfAny("abc".ToCharArray ).ToString
txtResults.Text=sampleString.StartsWith("Th equi").ToString
Results=sampleString.Split("abc".ToCharArra y) Results=sampleString.Split("abc".ToCharArra y,3)
Shared Methods
In all the previous examples, a method was invoked on an instance of a string, sampleString. In these examples, we’re demonstrating shared methods, which are methods that don’t need an instance of a string but are invoked on the String class itself.
txtResults.Text=String.Compare("Thisisat est",_ "thisisatest",True).ToString txtResults.Text=String.Compare("Thisisat est",_ "thisisatest").ToString
txtResults.Text=String.CompareOrdinal("This isatest",_ "thisisatest").ToString
txtResults.Text=String.Concat("Thisisate st", " ofhowthisworks", " whenyouconcatenate").ToString
txtResults.Text=_ String.Format("Your{0:N0}itemstotal{1 :C}.",12,17.35)
Dimvalues()AsString values= "item1,item2,item3,item4".Split(", ".ToCharArray) txtResults.Text=String.Join("/ ",astrValues)
The StringBuilder Class
As we mentioned earlier, the StringBuilder class streamlines your string handling. In the following example, we insert a new word at index 19, take out six characters beginning at index 10, replace one word with another, and add a string to the end of the original one. StringBuilder has an Append method, but because we want to format the number of minutes, we use the AppendFormat method, which mimics the Format method of the String class.
DimsbAsNewStringBuilder("Thequickbrown foxjumpsoverthelazydog") sb.Insert(19, " happily") sb.Remove(10,6) sb.Replace("jumps", "leaps") sb.AppendFormat(" {0}timesin{1:N1}minutes",17,2)
Now if you wanted to add a comma after the word “dog,” you would first need to locate the word and then insert a comma after it. IndexOf would be perfect for locating “dog,” but unfortunately StringBuilder doesn’t have an IndexOf method. Your solution: use ToString to get a copy of the string from StringBuilder, use IndexOf to locate the position of “dog,” and then use the StringBuilder Insert method to put the comma where you want it in the original string. Once you’re done, you extract the final string from StringBuilder with the ToString method.
DimpositionAsInteger position=sb.ToString.IndexOf("dog") Ifposition>0Then 'Insertthecommaattheposition 'youfound+thelengthofthetext "dog". sb.Insert(position+ "dog".Length, ", ") EndIf txtResultsOther.AppendText("StringBuilderout put: " &sb.ToString)
The same actions using the String class would look like the following code. Note that this code causes the .NET runtime to create a new string five separate times.
DimsampleStringAsString= "Thequickbrownfoxjumpsoverthelazydog" sampleString=sampleString.Insert(19, " happily") sampleString=sampleString.Remove(10,6) sampleString=sampleString.Replace("jumps", "leaps") sampleString&=String.Format("{0}timesin{ 1:N1}minutes",17,2) position=sampleString.IndexOf("dog") Ifposition>0Then sampleString=sampleString.Insert(positi on+ "dog".Length, ", ") EndIf txtResultsOther.AppendText("Stringoutput: " &sampleString)
The StringWriter Class
With its ability to write and to store sequential information in a StringBuilder using its Write and WriteLine methods, the StringWriter class makes assembling an output string easy.
Let’s imagine that you have an array of strings containing address information and you want to create an address formatted for mailing. You could do it with a String object, concatenating with “&” and “&=”. But the StringWriter class offers another way, utilizing its under-the-hood StringBuilder object. Here’s how:
DimswAsNewStringWriter() DimaddressInfo()AsString={"JohnSmith", "123MainStreet",_ "Centerville", "WA", "98111"} 'WritethenameandaddresslinestotheStr ingWriter: sw.WriteLine(addressInfo(0)) sw.WriteLine(addressInfo(1))
You could use String.Format to create the final line of the address, but here’s how you’d do it with the StringWriter Write and WriteLine methods. Write simply appends data. WriteLine appends the data along with a line-termination character.
sw.Write(addressInfo(2)) sw.Write(", ") sw.Write(addressInfo(3)) sw.Write(" ") sw.WriteLine(addressInfo(4)) 'Or,perhapsmoreefficiently: 'sw.WriteLine(String.Format("{0},{1}{2}",a ddressInfo(2),_ 'addressInfo(3),addressInfo(4)))
If you used the String class, the code would be considerably less elegant, as the following example shows:
DimstrAsString str=addressInfo(0)&Environment.NewLine str&=addressInfo(1)&Environment.NewLine 'Addthecity/region/postalcodevalues: str&=addressInfo(2)& ", " str&=addressInfo(3)& " " &addressInfo(4) str&=Environment.NewLine 'Or: 'str&=String.Format("{0},{1}{2}{3}",add ressInfo(2),addressInfo(3),_ 'addressInfo(4),Environment.NewLine)
In this application you’ve seen that you can use the wide variety of methods of the String class to insert, remove, modify, locate, pad, trim, and otherwise manipulate strings. You’ve also seen that you can use shared methods of String—such as Compare, Concat, Format, and Join—directly on the String class without first instantiating a String object.
It’s recommended that you use the StringBuilder class for efficiently manipulating large strings or to manage repeated manipulation of smaller strings. When you need to output a string, the StringWriter class offers the convenience of an in-memory file to which you can write repeatedly, while it uses a StringBuilder for efficient string concatenation.
No matter how carefully you write your code, errors are bound to happen. One sign of a well-written application is graceful handling of such errors. This sample demonstrates the new Try...Catch...Finally exception handling in Visual Basic .NET.
You’re probably accustomed to writing error handlers in your Visual Basic 6 applications, using On Error GoTo. You can still do that in Visual Basic .NET, but this sample application demonstrates a better way.
Visual Basic .NET introduces a new concept to Visual Basic developers: structured exception handling (SEH). An exception is simply an anomaly—an error—in the execution of your application, and in keeping with the concept that everything in the .NET world is object-based, structured exception handling allows you to access a specific object associated with each kind of error (exception) that can occur. You can respond to an exception based on its specific type, or you can handle all exceptions generically.
The way you write your exception-handling code is different, too. In Visual Basic 6, you probably constructed your error handlers something like this:
SubFoo() OnErrorGoToErrHandler 'Codethatcouldfail ExitHere: 'Closedatabaseconnections,delete tempfiles,etc. ExitSub ErrHandler: 'Handletheerror ResumeExitHere EndSub
This is a classic example of spaghetti code. First you jump to the ErrHandler label if there’s an error, and then after handling the error, you jump back to ExitHere to make sure you clean up any resources in use. Finally, you exit the procedure with the Exit Sub statement.
Another limitation of the On Error GoTo construct was that you could have only one active error handler per procedure. That meant that a second On Error GoTo statement would inactivate the first one.
Typically, your error handler block would include an If/Then or Select Case construct for handling different errors, based on Err.Number. At times, the error-handling code was greater in size than the code it was protecting.
The following sample application (shown in Figure 2-4) demonstrates how structured exception handling takes a different approach. This new approach is based on a specific Exception object for each kind of error and the use of the Try/Catch exception-handling code. The sample code that follows Figure 2-4 shows a Try/Catch block in its simplest form.
Figure 2-4: Structured Exception Handling gives you more control over errors than On Error GoTo.
Try 'Codethatcouldfail(theprotectedblo ck). CatchexpAsException 'Handletheexception. Finally 'Codethatgetsexecutedregardless. 'Finallyisoptional. EndTry
The Try/Catch concept is superior to On Error GoTo in a number of ways, and key advantages of using SEH include:
As we just mentioned, you use a Try/Catch construct for working with exceptions. When an error occurs within the protected block, control passes to the Catch block, where you handle the exception—perhaps by simply notifying the user of the problem via a message box, perhaps by trying an alternative such as connecting to a different database server, or by whatever other action you consider appropriate.
The Exception object provides a number of informative properties, including:
Other features of SEH include:
Let’s examine structured exception handling in action. We’ll cover each option as reflected in the buttons on the sample form.
No Exception Handling
Without exception handling, an exception such as a file not being found results in a message box generated by the .NET runtime that invites the user to either ignore the error and continue or quit. The following example produces such a result because there’s no exception handling in place:
PrivateSubbtnNoTryCatch_Click(... ⋮ DimfsAsFileStream 'Thiscommandwillfailifthefiledoes notexist. fs=File.Open(Me.txtFileName.Text,FileM ode.Open) MessageBox.Show("Thesizeofthefileis: " &fs.Length,Me.Text,_ MessageBoxButtons.OK,MessageBoxIcon. Information) fs.Close() ⋮ EndSub
Basic Exception Handling
To implement basic exception handling, you add Try/Catch/End Try to the procedure, placing the code that could fail in the protected area between the Try and the Catch. If the file isn’t found, control passes to the Catch block, where you can inform the user of the problem. In this case, we’ve chosen to refer to the Exception object as exp, but you can choose any name you care to. Note also that the message box displays the exception’s Message property, which is a description of the error—much like Err.Description in Visual Basic 6.
PrivateSubbtnBasicTryCatch_Click(... DimfsAsFileStream Try 'Thiscommandwillfailifthefile doesnotexist. fs=File.Open(Me.txtFileName.Text,F ileMode.Open) MessageBox.Show("Thesizeofthefile is: " &fs.Length,Me.Text,_ MessageBoxButtons.OK,MessageBoxI con.Information) fs.Close() CatchexpAsException MessageBox.Show(exp.Message,Me.Text, MessageBoxButtons.OK,_ MessageBoxIcon.Stop) EndTry EndSub
Handling Specific Exceptions
The .NET Framework provides a specific exception object type for every kind of exception. Whenever you can, you should code for specific exceptions, taking the appropriate action depending on which exception occurred. In the following example, we’re able to notify the user with a precise message when either the file doesn’t exist or the directory doesn’t exist. We can also handle generic exceptions such as IOException, which could include a FileLoadException, a PathTooLongException, or the ultimate generic exception, Exception.
When you’re handling specific exceptions, as we’re doing here, be sure to sequence your Catch blocks from most specific to most general. For example, the last Catch block shown in the following code segment handles any exceptions not specifically handled earlier. Because it’s the most generic of the Catch blocks, it needs to be last; otherwise, it will catch all exceptions, and the specific Catch blocks will never see them.
Private Sub btnSpecificTryCatch_Click(... DimfsAsFileStream Try 'Thiscommandwillfailifthefile doesnotexist. fs=File.Open(Me.txtFileName.Text,F ileMode.Open) MessageBox.Show("Thesizeofthefile is: " &fs.Length,Me.Text,_ MessageBoxButtons.OK,MessageBoxI con.Information) fs.Close() CatchexpAsFileNotFoundException 'Willcatchanerrorwhenthefiler equesteddoesnotexist. MessageBox.Show("Thefileyourequest eddoesnotexist.",_ Me.Text,MessageBoxButtons.OK,Me ssageBoxIcon.Stop) CatchexpAsDirectoryNotFoundException 'Willcatchanerrorwhenthedirect oryrequesteddoesnotexist. MessageBox.Show("Thedirectoryyoure questeddoesnotexist.",_ Me.Text,MessageBoxButtons.OK,Me ssageBoxIcon.Stop) CatchexpAsIOException 'WillcatchanygenericIOexception . MessageBox.Show(exp.Message,Me.Text, MessageBoxButtons.OK,_ MessageBoxIcon.Stop) CatchexpAsException 'Willcatchanyerrorthatwe'renot explicitlytrapping. MessageBox.Show(exp.Message,Me.Text, MessageBoxButtons.OK,_ MessageBoxIcon.Stop) EndTry EndSub
Displaying a Customized Message
You can take advantage of the wealth of information that the Exception object offers in its properties and methods, by creating custom messages that are formatted as you choose, including just the information you want to present. For example, the following procedure responds to an IOException by notifying the user that the file could not be opened and adding the message, source, and stack trace information provided by the Exception object:
PrivateSubbtnCustomMessage_Click(... DimfsAsFileStream Try 'Thiscommandwillfailifthefile doesnotexist fs=File.Open(Me.txtFileName.Text,F ileMode.Open) MessageBox.Show("Thesizeofthefile is: " &fs.Length,Me.Text,_ MessageBoxButtons.OK,MessageBoxI con.Information) fs.Close() CatchexpAsIOException 'WillcatchanygenericIOexception DimsbAsNewSystem.Text.StringBuild er() Withsb .Append("Unabletoopenthefile yourequested, ") .Append(Me.txtFileName.Text&vbC rLf&vbCrLf) .Append("DetailedErrorInformati onbelow:" &vbCrLf) .Append(" Message: " &exp.Message&vbCrLf) .Append(" Source: " &exp.Source&vbCrLf&vbCrLf) .Append(" StackTrace:" &vbCrLf) EndWith
This example also shows a nested Try/Catch block, something that you simply couldn’t do with the Visual Basic 6 unstructured On Error/GoTo error handling.
DimstrStackTraceAsString 'Accessinganexceptionobject'sSta ckTracecouldcausean 'exceptionsoweneedtowraptheca llinitsownTry..Catch 'block. Try strStackTrace=exp.StackTrace() CatchstExpAsSecurity.SecurityExcep tion 'Catchasecurityexception strStackTrace= "Unabletoaccessstacktracedueto " &_ "securityrestrictions." CatchstExpAsException 'Catchanyotherexception strStackTrace= "Unabletoaccessstacktrace." EndTry sb.Append(strStackTrace) MessageBox.Show(sb.ToString,Me.Text, MessageBoxButtons.OK,_ MessageBoxIcon.Stop) CatchexpAsSystem.Exception 'Catchanyotherexception MessageBox.Show(exp.Message,Me.Text, MessageBoxButtons.OK,_ MessageBoxIcon.Stop) EndTry EndSub
Using the Finally Block
If you want certain code—such as code for closing database connections—to run whether there was an exception or not, you include a Finally block, which is always executed. Finally is optional, but if it’s present, it runs immediately after the Try block if there was no error or immediately after the Catch block if there was. This is roughly analogous to using Resume ExitHere in Visual Basic 6, which was used to ensure that cleanup code would always run. But Finally is superior because it doesn’t require you to write spaghetti code to jump to it. Instead, the .NET runtime ensures that the Finally block gets executed every time.
PrivateSubcmdTryCatchFinally_Click(... DimfsAsFileStream Try 'Thiscommandwillfailifthefile doesnotexist. fs=File.Open(Me.txtFileName.Text,F ileMode.Open) MessageBox.Show("Thesizeofthefile is: " &fs.Length,Me.Text,_ MessageBoxButtons.OK,MessageBoxI con.Information) CatchexpAsException 'Willcatchanyerrorthatwe'renot explicitlytrapping. MessageBox.Show(exp.Message,Me.Text, MessageBoxButtons.OK,_ MessageBoxIcon.Stop) Finally 'Cleanup,ifwedidopenthefiles uccessfully. IfNotfsIsNothingThen fs.Close() MessageBox.Show("Fileclosedsucc essfullyinFinallyblock",_ Me.Text,MessageBoxButtons.OK ,MessageBoxIcon.Information) EndIf EndTry EndSub
This application has shown you that structured exception handling provides a more robust environment for handling errors than was available in Visual Basic 6, and that you can protect any code that could generate an exception with a Try/Catch block. You can, and should, write your code to respond to specific exceptions with a separate Catch block for each kind of error you want to handle.
Because exceptions are generated by the run time, you write less code and your application’s exceptions are available to other client applications written in other .NET languages. You can use the StackTrace property to pinpoint where the error occurred in your code.
One final note: On Error GoTo is supported in Visual Basic .NET for backward compatibility, primarily for applications migrated from Visual Basic 6. It has been updated so that it actually generates exceptions and is fully integrated into the .NET Framework. You might be tempted to use it, but don’t do it. Structured exception handling is infinitely superior and is compatible with cross-language development. Stick with SEH.
Even though the .NET Framework provides scores of exception types, seemingly one for every possible kind of exception, they might not be enough for you. This sample demonstrates how to create and use custom exceptions in Visual Basic .NET, as well as how to set up a global exception handler.
Building Upon… |
Application #4: Try...Catch...Finally Application #7: Object-Oriented Features |
Creating custom exceptions to enhance those already provided by the .NET Framework is easy. We’ll show you uses for such exceptions and how to create them.
Uses for Custom Exceptions
The .NET Framework class library provides more than 100 built-in exception types, covering almost every imaginable error. Why would you consider creating custom exceptions? Primarily for handling situations that break business rules or for situations in which you want to provide the client with more information than one of the built-in exceptions might provide.
In addition to the properties common to all exceptions—such as Source, Message, and the like—you will likely want to provide additional properties unique to your custom exception. These properties can offer detailed information to a client using your application or component. You should document your exception so that a developer will know under what circumstances the custom exception will be thrown, and what information he can gather from it.
Some guidelines on using custom exceptions include:
Creating Custom Exceptions
To create a custom exception, you must create a class that defines the exception. All exceptions inherit directly or indirectly from the System.Exception class. Two subclasses of System.Exception serve as the base classes for most other exceptions: System.SystemException and System.ApplicationException. You should inherit from System.ApplicationException, which represents exceptions thrown by applications, as opposed to System.SystemException, which represents exceptions thrown by the common language runtime itself.
In the sample application (shown in Figure 2-5), the class library project contains a variety of exception classes, arranged in the inheritance hierarchy depicted in Figure 2-6.
Figure 2-5: You can easily create your own custom exceptions with Visual Basic .NET. You can also set a global exception trap by attaching your own handler to the Application.ThreadException exception.
Figure 2-6: The sample application, Custom Exceptions Client, includes several custom exceptions arranged in an inheritance hierarchy.
Let’s examine the Customer class and the custom exceptions designed to work with it, all of which are defined in Customer.vb.
Creating the Customer Class
This class exposes two shared methods, EditCustomer and DeleteCustomer, as well as three public fields, Id, FirstName, and LastName. EditCustomer is designed to return a Customer object based on a supplied ID. The code in the sample simulates a failed database search for a customer, and then creates and throws an exception of type CustomerNotFoundException.
PublicSharedFunctionEditCustomer(ByValId AsInteger)AsCustomer ⋮ DimmsgAsString msg=String.Format("Thecustomeryoureq uestedbyId{0} " &_ "couldnotbefound.",Id) DimexAsNewCustomerNotFoundException(m sg) Throwex EndFunction
The DeleteCustomer method simulates finding customers, but it doesn’t delete them. It demonstrates one of the benefits of creating your own exception—the ability to add methods like LogError (described later).
PublicSharedSubDeleteCustomer(ByValIdAs Integer) DimcAsNewCustomer() ⋮ DimmsgAsString msg=String.Format("Thecustomer'{0}{1 }'couldnot " &_ "bedeleted.Youraccount'{2}'doesnothave " &_ "permission.",c.FirstName,c.LastName,user) DimexAsNewCustomerNotDeletedException (msg,c,user) exp.LogError() Throwex EndSub
Creating Custom Exceptions
The first custom exception is CRMSystemException, which is the base class for the other custom exceptions in the class library. Like all exceptions, it requires a Message parameter, which is a description of the exception. Its constructor then invokes the constructor of its base class, System.ApplicationException, passing the Message object to it. CRMSystemException exposes a LogError method, which makes an entry in the Application log. Finally, it exposes an AppSource property, which defaults to “SomeCompany CRM System” (set in the constructor), but this can be overridden by derived classes. Both AppSource and Message are used to identify the log entry.
PublicClassCRMSystemException InheritsSystem.ApplicationException Privatem_AppSourceAsString PublicSubNew(ByValMessageAsString) MyBase.New(Message) Me.m_AppSource= "SomeCompanyCRMSystem" EndSub FriendSubLogError() DimeAsSystem.Diagnostics.EventLog e=NewSystem.Diagnostics.EventLog(" Application") e.Source=Me.AppSource e.WriteEntry(Me.Message,_ System.Diagnostics.EventLogEntryT ype.Error) e.Dispose() EndSub PublicOverridableReadOnlyPropertyAppS ource()AsString Get Returnm_AppSource EndGet EndProperty EndClass
From this base exception class, the first derived class is CustomerException, which requires not only a Message but an object representing the customer whose account is being accessed when the exception occurs (reqCustomer). When this exception is thrown, the client application has access to the customer information through the CustomerInfo property. In some cases, such as when the customer is not found (as seen in CustomerNotFoundException later in this section), reqCustomer might be Nothing. Finally, the AppSource property of the class overrides its parent’s AppSource, returning “SomeCompany CRM Customer Module.” This means that when LogError gets called, it will use this AppSource property, not the AppSource property of the parent class.
PublicClassCustomerException InheritsCRMSystemException Privatem_AppSourceAsString Privatem_CustomerAsCustomer PublicSubNew(ByValMessageAsString,B yValReqCustomerAsCustomer) MyBase.New(Message) Me.m_Customer=ReqCustomer Me.m_AppSource= "SomeCompanyCRMCustomerModule" EndSub PublicReadOnlyPropertyCustomerInfo()A sCustomer Get ReturnMyClass.m_Customer EndGet EndProperty PublicOverridesReadOnlyPropertyAppSou rce()AsString Get ReturnMe.m_AppSource EndGet EndProperty EndClass
From the CustomerException class, we derive CustomerNotFoundException, which simply invokes its parent’s constructor, passing Nothing for the customer because the customer could not be found.
PublicClassCustomerNotFoundException InheritsCustomerException PublicSubNew(ByValMessageAsString) MyBase.New(Message,Nothing) EndSub EndClass
The second class derived from CustomerException is CustomerNotDeletedException, which takes an additional parameter, UserId, and returns it in a property of the same name. A client handling this exception can use this UserId property to take other actions related to the customer.
PublicClassCustomerNotDeletedException InheritsCustomerException Privatem_UserIdAsString PublicSubNew(ByValMessageAsString,_ ByValReqCustomerAsCustomer,ByVal UserIdAsString) MyBase.New(Message,ReqCustomer) Me.m_UserId=UserId EndSub PublicReadOnlyPropertyUserId()AsStri ng Get ReturnMe.m_UserId EndGet EndProperty EndClass
One other custom exception class, EmployeeException, inherits from CRMSystemException. This exception is not used in the sample, but it could serve as the base for derived classes such as EmployeeNotFoundException and EmployeeNotDeletedException.
PublicClassEmployeeException InheritsCRMSystemException PublicSubNew(ByValmessageAsString) MyBase.New(message) EndSub EndClass
Using the Custom Exceptions
The sample application’s frmMain form has buttons for editing and deleting customers. In the following example, you’re invoking the shared EditCustomer method, to which you pass the customer ID. In the sample, the customer is not found and the code catches the exception of type CustomerNotFoundException thrown by EditCustomer. Note that the code is also prepared to catch CustomerException, the parent of CustomerNotFoundException, as well as any other kind of exception that might be thrown.
PrivateSubbtnEdit_Click(... DimcAsCustomer Try DimiAsInteger=14213 c=Customer.EditCustomer(i) 'dosomeworkhereifwegetavalid customerback CatchexpAsCustomerNotFoundException MessageBox.Show(exp.Message,exp.AppS ource,MessageBoxButtons.OK,_ MessageBoxIcon.Error) CatchexpAsCustomerException MessageBox.Show(exp.Message,exp.AppS ource,MessageBoxButtons.OK,_ MessageBoxIcon.Error) CatchexpAsException MessageBox.Show(exp.Message,exp.Sour ce,MessageBoxButtons.OK,_ MessageBoxIcon.Error) EndTry EndSub
When you try to delete a customer, the DeleteCustomer method throws a CustomerNotDeletedException. When you catch it, you can choose to work further with the Customer object that it returns. Keep in mind that because DeleteCustomer is a shared method, you don’t need to instantiate a customer object to call it.
PrivateSubcmdDelete_Click(... Try DimiAsInteger=14213 Customer.DeleteCustomer(i) MessageBox.Show(String.Format("Custom erId{0}wasdeleted.",_ i),Me.Text,MessageBoxButtons.OK ,MessageBoxIcon.Information) CatchexAsCustomerNotDeletedException DimcAsCustomer c=ex.CustomerInfo 'Wecannowdosomethingmoreintere stingwith 'thecustomerifwewantedto. MessageBox.Show(ex.Message,ex.AppSou rce,MessageBoxButtons.OK,_ MessageBoxIcon.Error) CatchexAsCustomerException MessageBox.Show(ex.Message,ex.AppSou rce,MessageBoxButtons.OK,_ MessageBoxIcon.Error) CatchexAsException MessageBox.Show(ex.Message,ex.Source ,MessageBoxButtons.OK,_ MessageBoxIcon.Error) EndTry EndSub
Global Exception Handler
Normally an untrapped error in Visual Basic 6 and earlier would produce a quick MessageBox dialog box, and then your process would shut down. Windows Forms, however, has injected a top-level error catch between the common language runtime and your code, presenting a dialog box that gives the user a chance to continue or quit. To see the global exception handler at work, run the sample application outside the debugger. (You can do this by pressing Ctrl+F5. If you run the code inside the debugger, you’ll simply go into Break mode when there’s an untrapped exception.) The special dialog box appears if the following three conditions are true:
The global exception handler is a welcome new capability, but you can take it further by attaching your own handler to take care of untrapped exceptions. You do that by adding a handler for the exception Application.ThreadException and associating it with an OnThreadException procedure.
PrivateSubcmdTrapped_Click(... 'Turnonourownglobalexceptionhandle r. AddHandlerApplication.ThreadException,_ AddressOfMe.OnThreadException GenerateError() EndSub
When an untrapped exception occurs, OnThreadException gets executed, and in it you can do any type of handling you care to. In fact, you can use this technique to centralize the handling of all your exceptions, thereby creating a global exception handler. By the way, there’s no magic to the name OnThreadException; you can call the procedure anything you like as long as it has the correct signature.
FriendSubOnThreadException(ByValsenderAs Object,_ ByValtAsSystem.Threading.ThreadExcepti onEventArgs) DimexAsException=t.Exception DimexTypeAsString IfTypeOfexIsApplicationExceptionThen exType= "ApplicationException" ElseIfTypeOfexIsArgumentExceptionThe n exType= "ArgumentException" ElseIfTypeOfexIsCustomerNotFoundExcep tionThen exType= "CustomerNotFoundException" Else exType= "Exception" EndIf DimmsgAsString msg=String.Format("We'resorry,anuntr apped{2}has " &_ "occurred.{0}Theerrormessagewas:{0}{0}{1}" ,vbCrLf,_ ex.Message,exType) MessageBox.Show(msg, "GlobalExceptionTrap",_ MessageBoxButtons.OK,MessageBoxIcon. Exclamation) EndSub
When you no longer want the handler, just unhook it, as the following procedure does:
PrivateSubcmdUntrapped_Click(ByValsenderA sSystem.Object,_ ByValeAsSystem.EventArgs)HandlescmdU ntrapped.Click 'Turnourhandleroffandreverttothe WindowsFormsdefault. RemoveHandlerApplication.ThreadException ,_ AddressOfMe.OnThreadException GenerateError() EndSub
Custom exceptions enhance the power of structured exception handling, making it an even more powerful weapon in the Visual Basic .NET developer’s arsenal. Keep the following principles in mind:
This sample shows you how to programmatically access the call stack, which contains the sequence of procedures that led to the application’s current execution point.
Building Upon… |
Application #3: String Manipulation Application #4: Try…Catch…Finally Application #7: Object-Oriented Features |
When an error occurs in your application, how do you determine the root cause? True, you can use Try/Catch/Finally blocks to provide information on the error, but you probably want more. This application introduces new ways for you to find out more.
Walking the Stack—Conceptual
When a procedure is called, it changes the flow of control in your application just like a GoTo statement does. But when the procedure is done, control returns to the statement following the original procedure call. The stack is a portion of memory that keeps track of the procedures that have been called in your application, noting those that are waiting to finish because calls they’ve made haven’t been completed yet.
While you’re debugging your application, or when you want to do some application profiling, you might want some way to determine which procedures your code passed through on its way to a particular problem. You’d like to know the procedure that called the current procedure, as well as the caller of that procedure, and so on, up the stack. This is commonly referred to as walking the stack, and it’s simply a way to trace the sequence of procedures that haven’t completed because they’re waiting for the current one to finish its work. The sample application, shown in Figure 2-7, demonstrates how to walk the stack.
Figure 2-7: When you pass an Exception object to the constructor of the StackTrace class, you can show the code in your application that led up to the exception.
The StackFrame Class
To do a stack walk in Visual Basic 6, you’re forced to use extraordinary measures— such as calling custom logging procedures—just to see which procedures got you to where you are. But with Visual Basic .NET it’s far easier to get the information you need, thanks to the StackTrace class, found in the System.Diagnostics namespace.
A StackTrace object is a container for individual StackFrame objects, one for each method call on your application’s stack. By using these objects, along with the MethodInfo and ParameterInfo classes (in the System.Reflection namespace), you can have access not only to the names of the procedures on the stack, but much more detailed information including the file and line number of the procedure, parameters of the various methods, and much more.
We’ll show you how to use MethodInfo to identify the attributes of a method and gain access to its metadata. ParameterInfo lets you do the same for the method’s parameters.
The sample form has two buttons and a check box. The first button lets you traverse the complete stack, while the second button focuses on the portion of the stack that relates directly to an exception. Check the Include Source Info check box to see more information on each procedure listed. You’ll see DeclaringType, which is the class or module in which the procedure is declared, Namespace, which is handy if you’re tracing through more than one module or namespace. You’ll also see the file name and line number of the procedure.
Test the Procedure Stack
The Test Procedure Stack button shows how to trace the entire stack all the way back to its beginning. It starts with the btnStackTrace_Click event procedure, which calls ProcA, which in turn calls ProcB.
PrivateSubbtnStackTrace_Click(... ProcA(1,2, "Hello") EndSub PrivateSubProcA(ByValItem1AsInteger,ByR efItem2AsInteger,_ ByValItem3AsString) DimstrResultsAsString=ProcB(String.C oncat(Item1,Item2,Item3)) EndSub
When ProcB calls GetFullStackFrameInfo, there are three method calls on the stack, and the GetStackFrameInfo procedure shows how you can access their names and other information. ProcB passes a StackTrace object (with stack information at this point in the application) to GetFullStackFrameInfo. You can include an optional True or False parameter in the constructor of the StackTrace object. If the parameter is True, information—including line and column number—is gathered on the file containing the code being executed. If it’s False or omitted, no file information is provided.
PrivateFunctionProcB(ByValNameAsString) AsString GetFullStackFrameInfo(NewStackTrace(chkI ncludeSource.Checked)) EndFunction
GetFullStackFrameInfo loops through the stack frames, starting at the current procedure (which has an index of zero), and it retrieves the frames by number, using the GetFrame method. Note that there are more frames than the three methods pushed onto the stack so far by our application because StackTrace includes every frame on the stack, all the way to the initial Sub Main that started the application.
PrivateFunctionGetFullStackFrameInfo(ByVal stAsStackTrace)AsString DimfcAsInteger=st.FrameCount DimiAsInteger txtStackItems.Clear() Fori=0Tofc–1 'Getinfoonasingleframe. txtStackItems.Text&=GetStackFrameIn fo(st.GetFrame(i))&vbCrLf Next EndFunction
GetStackFrameInfo gathers information about the method associated with a single stack frame. We’ve shown several method attributes, but there are still more available, including IsConstructor, IsOverloaded, and others.
PrivateFunctionGetStackFrameInfo(ByValsfA sStackFrame)AsString DimmiAsMethodInfo=CType(sf.GetMethod (),MethodInfo) DimoutputAsString 'Showthemethod'saccessmodifier Ifmi.IsPrivateThen output&= "Private " ElseIfmi.IsPublicThen output&= "Public " ElseIfmi.IsFamilyThen output&= "Protected " ElseIfmi.IsAssemblyThen output&= "Friend " EndIf 'Isitshared? Ifmi.IsStaticThen output&= "Shared " EndIf output&=mi.Name& "("
We’re gathering information about the method’s parameters, including the name, type, and whether the parameter is ByVal or ByRef. We could also have included whether the parameter was optional, as well as other information.
DimpiList()AsParameterInfo=sf.GetMethod. GetParameters() DimparamsAsString=String.Empty DimpiAsParameterInfo ForEachpiInpiList params&=String.Format(",{0}{1}As {2}",_ IIf(pi.ParameterType.IsByRef, "ByRef", "ByVal"),pi.Name,_ pi.ParameterType.Name) Nextpi 'Getridofthefirst ", " ifitexists. Ifparams.Length>2Then output&=params.Substring(2) EndIf 'Gettheprocedure'sreturntypeandapp endittotheoutputstring. DimtypAsType=mi.ReturnType output&= ")As " &typ.ToString
Now we’re optionally showing more detailed information about the method, such as its namespace, the module in which it’s declared, the name of the file it lives in, and its line and column number in the file.
IfchkIncludeSource.CheckedThen 'Getthesourcefileforthecurrent methodonthestack. DimsourceFileAsString=sf.GetFile Name()& "" 'Givedetailedinfoonthemethod IfsourceFile.Length<>0Then 'Givedetailedinfoonthemetho d output&=String.Format("{0}D eclaringType:{1}{0}" &_ " Namespace:{2}{0}File:{3}{0}Line: {4}," &_ " Column:{5}",vbCrLf,mi.DeclaringType,_ mi.DeclaringType.Namespace,s ourceFile,_ sf.GetFileLineNumber,sf.GetF ileColumnNumber) EndIf EndIf ReturnstrOut EndFunction
Test Exception Handling
Sometimes you might want to retrieve stack information only on the code in your application that led up to the exception. The Test Exception Handling button on the sample form demonstrates how you can optionally pass an Exception object to the constructor of the StackTrace class to accomplish this goal.
Note that, in this example, there is an exception handler in btnException_Click but none in the procedures it calls. When an exception is thrown in ProcException4, the .NET runtime walks back up the stack until it finds an active handler and uses it. Note also that you get access to the portion of the stack that relates to your exception by calling GetFullStackFrameInfo with a StackTrace object that includes the current Exception in its constructor. You’ll notice that the stack information does include ProcException1 through ProcException4, even though the exception handler is actually several frames higher on the stack.
ProcException3 and ProcException4 are located in a separate module, and if you check the Include Source Info check box, the stack trace will reveal the module name as the Declaring Type.
PrivateSubbtnException_Click(... Try ProcException1(1,2) CatchexpAsException GetFullStackFrameInfo(NewStackTrace( exp)) EndTry EndSub PrivateSubProcException1(ByValxAsInteger ,ByValyAsInteger) ProcException2("Mike",12) EndSub PrivateSubProcException2(ByValNameAsStri ng,ByValSizeAsLong) ProcException3() EndSub FriendFunctionProcException3()AsString ReturnProcException4("mike@microsoft.com ") EndFunction PrivateFunctionProcException4(ByValEmailAd dressAsString)AsString ThrowNewArgumentException("Thisisafa keexception!") EndFunction
In this application you’ve seen that walking the stack lets you trace the route your code took to its current location. This process is valuable for debugging or profiling your application. You can walk the stack all the way to its beginning if you want to, or you can deal only with the portion that relates to the exception that just occurred.
When you don’t need the kind of detail GetStackFrameInfo provides in this example, try using StackFrame.ToString(), which gives you a quick description of the current frame.
Keep in mind that you can get the stack frame information we’ve described here only when you’ve compiled in Debug mode.
One criticism of Visual Basic 6 was that it was not a truly object-oriented language, which denied it membership among the premier development languages. That criticism no longer applies because Visual Basic .NET is fully object oriented. This sample application demonstrates some of the new object-oriented (OO) features in Visual Basic .NET.
Building Upon… |
Application #3: String Manipulation Application #8: Scoping, Overloading, Overriding |
As you’ve no doubt heard over and over, Visual Basic .NET is an object-oriented language. What exactly does that mean? It means that the code you write manipulates a series of objects, each of which has certain characteristics and capabilities, and you make them do your will to get your work done.
Object Orientation Overview
You can think of an object as something like a robot that carries out actions on your behalf, like writing to a file or calculating a result from values you pass to it. To understand how objects work, we need to define a few basic terms:
A blueprint or design from which an object is created. A class spells out the abilities and characteristics the object will have when it’s created.
An instance of a class. Just as a telephone is created from a design, so an object is created (instantiated) from a class.
You’ll often hear the words object and class used interchangeably. Just remember that a class is an abstract definition of what the object will be, while the object is a concrete instance created from the class. Other important terms are:
Propeties, methods, and events are the class’s Members, and by manipulating them you can make the object do your bidding. Three other terms we should note are:
In the sample code, we’ll demonstrate how to instantiate objects, use constructors, implement inheritance, and handle overloading, properties, and shared members.
Instantiating Objects
You instantiate an object using the New keyword. In the following example, you’re instantiating a new Customer object and assigning it to the variable cust.
PrivateSubcmdInstantiating_Click(... DimcustAsNewCustomer()
Once you have the object, you can assign values to its properties.
cust.AccountNumber= "1101" cust.FirstName= "Carmen" cust.LastName= "Smith"
You can use a method of the object (GetCustomerInfo) to do some work, such as gathering customer information.
DimcustInfoAsString=cust.GetCustomerInfo ()
Constructors
A Constructor is a procedure within a class that executes whenever an object is instantiated from that class. In Visual Basic .NET, the constructor is a Sub procedure named New. As with other procedures, the constructor might or might not require parameters to be passed to it.
Here’s an example of a class with a constructor. It accepts three parameters and uses them to assign values to the object’s properties. Instantiating the object and setting its properties all at one time is more efficient than setting the properties later.
PublicClassCustomerWithConstructor ⋮ SubNew(ByValAccountNumberAsString,By ValFirstNameAsString,_ ByValLastNameAsString) Me.AccountNumber=AccountNumber Me.FirstName=FirstName Me.LastName=LastName EndSub
Here’s the recommended syntax for using a single line of code to create an instance of a class that has a constructor:
PrivateSubcmdConstructors_Click(... DimcustAsNewCustomerWithConstructor(" 1101", "Carmen", "Smith")
Alternatively, you can declare the variable first and then instantiate the object:
Dimcust2AsCustomerWithConstructor cust2=NewCustomerWithConstructor("1101 ", "Carmen", "Smith")
Figure 2-8 shows the sample application after the user has clicked the Constructors button, which causes the application to instantiate the CustomerWithConstructor class (and set its properties) using the class’s constructor, as shown in the preceding code.
Figure 2-8: Passing initialization arguments to a constructor is more
efficient than setting properties later.
Inheritance
Sometimes you have a class that’s a perfect candidate for subclassing (deriving a new class from an existing one). For example, imagine that you’ve already defined a Customer class, but you have some customers with special characteristics: Government customers have a special government-issued number, and corporate customers need a special procedure for placing their orders.
To handle these situations, you can create derived classes that inherit from the base class, Customer. They get all the Customer members, but they can add members of their own, modify the way their inherited members behave, or both.
So you create the GovtCustomer and CorpCustomer classes. The Inherits keyword gives these classes all the characteristics of Customer, while allowing them to add their own members. GovtCustomer is given an additional property, GovtNumber, and CorpCustomer has an additional PlaceCorpOrder method.
PublicClassGovtCustomer InheritsCustomer Privatem_GovtNumberAsString PublicPropertyGovtNumber()AsString Get Returnm_GovtNumber EndGet Set(ByValValueAsString) _mGovtNumber=Value EndSet EndProperty EndClass PublicClassCorpCustomer InheritsCustomer PublicFunctionPlaceCorpOrder(ByValorde rAmtAsSingle,_ ByValorderDateAsDate)AsString 'Processorderandwritedatatodat abase... Return "Orderfor$" &orderAmt& " placedon " &orderDate EndFunction EndClass
You use these classes just like you would use Customer, but in addition, you have access to their unique members.
PrivateSubbtnInheritance_Click(... Dimcust1AsNewGovtCustomer() Withcust1 .GovtNumber= "9876543" .AccountNumber= "1103" .FirstName= "John" .LastName= "Public" EndWith Dimcust2AsNewCorpCustomer() Withcust2 .AccountNumber= "1104" .FirstName= "Mary" .LastName= "Private" DimstrOrderInfoAsString=.PlaceCo rpOrder(123.45,Today) EndWith
Overloads, Property Syntax, and Shared Members
See the code in the sample application for demonstrations and extensive in-code comments on these topics, which are touched upon in even greater detail in Application #8: Scoping, Overloading, Overriding.
This application has shown that object-oriented programming resembles the way you used to play as a child, using bricks of different shapes and colors to construct the house of your dreams. Each brick is a separate object that contributes to the structure. When you need to change a brick, you either replace it with another or modify it, but you don’t have to reconstruct the whole house.
Not every member of every class needs to be publicly available to applications using that class. Some procedures, properties, and other members should be for the private use of the class itself, while others might be made available to derived classes. This sample shows how to set various levels of access to the members of a class, including Public, Private, Protected, and others. It also demonstrates how to extend derived classes with features such as overloading and overriding.
The application simulates a simple hiring system that allows you to hire full- time, part-time, and temporary employees. It uses a series of classes to do its work. Employee is a base class containing the features common to all employees, and FullTimeEmployee, PartTimeEmployee, and TempEmployee are all derived from Employee.
All employees have many things in common: they all get hired, all have salaries, each has a name, and so forth. But each employee type has specific features that set it apart from other kinds—for example:
To satisfy these needs, each derived class extends the Employee class in some way: by overriding methods of the base class, by implementing new methods or properties of its own, or by replacing (shadowing) members of the base class. There is also a Friend class named EmployeeDataManager, which simulates reading employee data to and writing employee data from a database.
Building Upon… |
Application #3: String Manipulation Application #4: Try…Catch…Finally Application #7: Object-Oriented Features |
Visual Basic .NET has some new rules regarding the visibility and access of classes and their members. This application illustrates a number of them.
Scoping
The application demonstrates scoping with the use of these keywords, which control the level of access allowed to a class and its members:
The sample application, shown in Figure 2-9, illustrates these concepts.
Figure 2-9: This application has a base class named Employee, from which FullTimeEmployee, PartTimeEmployee, and TempEmployee all inherit common characteristics. Each derived class has its own unique implementation of key features.
Overloading
In Visual Basic 6, you could declare a procedure with optional parameters. Code calling the procedure could then choose to include or omit those parameters. In Visual Basic .NET, you still can do that, but you can also overload a method. Overloading means having several versions of the same method, each with a different set of parameters.
Overriding
When a derived class implements its own version of a method in the base class, it is said to be overriding that method. Overriding is just one of the buzz words you’ll need to be familiar with in the Visual Basic .NET world. The application demonstrates the use of the following statements and modifiers in classes and their members:
Let’s examine our classes and the relationships between them, beginning with the Employee class, which is the foundation for our application and from which three other classes are inherited. Note that all the classes are declared with the Public keyword.
The Employee Class
We want the Employee class to serve as a blueprint for other classes that will inherit from it, but we don’t want users to create instances of Employee. By declaring the class with the MustInherit keyword, we ensure that no instances of this class can be created—it can only be inherited.
PublicMustInheritClassEmployee Protectedc_HireDateAsDateTime Protectedc_NameAsString Protectedc_SalaryAsDecimal Protectedc_SocialServicesIDAsString
The four variables we just declared will hold the internal values for the HireDate, Name, Salary, and SocialServicesID properties. Because they’re declared with the Protected keyword, they’re accessible only from within the Employee class and classes derived from it, such as FullTimeEmployee and PartTimeEmployee. (If we had used the Private keyword, those variables would have been accessible only within Employee.) You can use Protected only at the class level, outside of any procedures. You cannot declare protected variables at the module, namespace, or file level.
Whenever an Employee object gets created, we want to set the default HireDate to today. We also want to allow the user to optionally include the name of the new employee. So Employee has two versions of its constructor, the procedure that runs whenever an instance of the class is created. You can use it to set up default values for certain properties, to establish database connections, or to perform any other initialization activities.
PublicSubNew() Me.HireDate=Today EndSub
The preceding procedure is the class’s default constructor, which runs when an Employee object is instantiated with no parameters. The following version is an overload of the constructor—another version of the same procedure, with a different set of parameters. Because it accepts parameters, it’s referred to as a parameterized constructor. This version runs when an Employee object is instantiated with a String parameter. Parameterized constructors allow data to be passed to the object at the same time it’s instantiated. This requires less frequent access to the object and less code, and it results in better performance than individually setting properties later.
The following constructor lets you create an Employee object and set its Name property at the same time. (Caution: you might be tempted to set the c_Name variable directly in your constructor or other procedures, like this: c_Name = strName. Don’t do it because it will bypass your Name property procedure. Using Me.Name = strName forces the property procedure to run and to execute any validation code it might contain.)
PublicSubNew(ByValstrNameAsString) Me.Name=strName Me.HireDate=Today EndSub
Employee has several properties: Bonus is declared ReadOnly, so clients using the class can retrieve, but not set, its value. This lets you keep tight control over how much of a bonus employees receive. The Get procedure runs whenever a client retrieves the value of Bonus, which we provide by executing the ComputeBonus function. Each property has a data type; this one is a Decimal.
PublicReadOnlyPropertyBonus()AsDecim al Get ReturnComputeBonus() EndGet EndProperty
The HireDate property is a read/write Date property. When a value is retrieved from it, the Get procedure runs, and the Set runs when someone sets its value. In either of these procedures, we can enforce business rules. In this example, we won’t accept a HireDate value later than the current date.
PublicPropertyHireDate()AsDate Get Returnc_HireDate EndGet Set(ByValValueAsDate) IfValue<=TodayThen c_HireDate=Value Else ThrowNewArgumentException( _ "HireDatecannotbelaterthantoday", "HireDate") EndIf EndSet EndProperty
The Name property is also Read/Write and has no validation code.
PublicPropertyName()AsString Get Returnc_Name EndGet Set(ByValValueAsString) c_Name=Value EndSet EndProperty
The Salary property is a special case, one that must be overridden by derived classes. We want each of the derived classes to implement its own means of assigning wages or salary, depending on the kind of employee it represents. To accomplish this, we declare the Salary property with the MustOverride keyword, which requires the derived class to override it and provide its own implementation code. Note that there is no End Property statement, nor any implementation statements.
PublicMustOverridePropertySalary()As Decimal
The SocialServicesID property is another special case, declared with the Overridable keyword. Because our company might have branches in other countries, we’re using the generic term SocialServicesID to represent Social Security numbers in the U.S.A., as well as other social service–type IDs in other countries. The following example assumes that most of our employees are U.S. based. Consequently, we’ve decided that, unlike what we did with Salary, we’ll include implementation statements to ensure that the SocialServicesID is numeric and exactly 11 characters long. Derived classes used in divisions of our company in other countries are free to override the property, implementing it as they choose to, but they are not required to do so, as they are with Salary.
PublicOverridablePropertySocialService sID()AsString Get Returnc_SocialServicesID EndGet Set(ByValValueAsString) IfIsNumeric(Value)AndAlsoLen(V alue)=11Then c_SocialServicesID=Value Else ThrowNewArgumentException( _ "SocialSecurityNumbermustbe11numeric " &_ "characters", "SocialServicesID") EndIf EndSet EndProperty
ComputeBonus is also declared with the MustOverride keyword, requiring derived classes to implement their own bonus calculation code.
PublicMustOverrideFunctionComputeBonus ()AsDecimal
The Hire method that follows is an overloaded method, with three versions. When someone calls the Hire method, she must at least provide the name of the new employee. But two other versions of this method allow the user to optionally provide the employee’s hire date and salary as well.
The argument list in each version of an overloaded method must be different from all the others, either in the number of arguments, their data types, or both. This allows the compiler to figure out which version of the method to use when the method is called. Derived classes might also have their own overloaded versions of the method, which must have their own unique list of arguments.
The first version here runs if Hire is called with just a String parameter—for example, emp.Hire(“Nancy Davolio”). Version two runs if Hire is called with String and Date parameters—for example, emp.Hire(“Nancy Davolio”, #12/5/2005#). The third version runs if Hire is called with String, Date, and Decimal parameters—for example, emp.Hire(“Nancy Davolio”, #12/5/2005#, CDec(50000)).
PublicSubHire(ByValNameAsString) Me.Name=Name EndSub PublicSubHire(ByValNameAsString,ByV alHireDateAsDateTime) Me.Name=Name Me.HireDate=HireDate EndSub PublicSubHire(ByValNameAsString,ByV alHireDateAsDateTime,_ ByValStartingSalaryAsDecimal) Me.Name=Name Me.HireDate=HireDate Me.Salary=StartingSalary EndSub EndClass
The FullTimeEmployee Class
FullTimeEmployee is derived from the Employee class, as indicated by the Inherits keyword. It therefore has all the properties, methods, and events of Employee, but it extends Employee by adding an AnnualLeave property and a ComputeAnnualLeave method and by implementing the Salary property and overriding the ComputeBonus method. Each of its constructors, shown in the following code, calls its counterpart in the base class by using the MyBase keyword.
PublicClassFullTimeEmployee InheritsEmployee PublicSubNew() MyBase.New() EndSub PublicSubNew(ByValNameAsString) MyBase.New(Name) EndSub
The AnnualLeave property is measured in days, and only the FullTimeEmployee class has it, because neither part-time nor temporary employees are eligible for annual leave. It is ReadOnly, and we return a value from it by executing the ComputeAnnualLeave method.
PublicReadOnlyPropertyAnnualLeave()As Integer Get ReturnComputeAnnualLeave() EndGet EndProperty
This Salary property procedure provides the implementation for the Salary property that was declared but not implemented in the base class. It includes validation code that restricts the salary to a range between 30,000 and 500,000.
PublicOverridesPropertySalary()AsDecimal Get Returnc_Salary EndGet Set(ByValValueAsDecimal) IfValue<30000.0OrValue>500 000.0Then ThrowNewArgumentOutOfRangeE xception("Salary",_ "Full- timeemployeesalarymustbebetween " &_ "30,000and500,000") Else c_Salary=Value EndIf EndSet EndProperty
By implementing the ComputeAnnualLeave method, this class is extending Employee. The method does not appear in the base class, nor in the other classes derived from Employee. The method computes how long the employee has been with the company and determines his leave accordingly.
PublicFunctionComputeAnnualLeave()AsI nteger 'Codetocomputeannualleavewould gohere. End Function
The following code implements the ComputeBonus method (which had no implementation in the base class) for full-time employees, who get an annual bonus of 1% of their salary.
PublicOverridesFunctionComputeBonus() AsDecimal ReturnMe.Salary*CDec(0.01) EndFunction EndClass
The PartTimeEmployee Class
This class also inherits from Employee, and it extends Employee in similar ways to FullTimeEmployee. It does have one item of note, the Hire method.
The PartTimeEmployee version of Hire overloads the already overloaded Hire method in the Employee base class. (There are now four versions of the Hire method available in the PartTimeEmployee class.) This version of Hire makes the StartingSalary parameter optional and adds an optional MinHoursPerWeek parameter.
Note that, because these parameters are optional, they must be last in the parameter list and must each be given a default value, which will be used if the parameter is omitted.
PublicOverloadsSubHire(ByValNameAsS tring,ByValHireDateAs_ DateTime,OptionalByValStartingSala ryAsDecimal=10000,_ OptionalByValMinHoursPerWeekAsDou ble=10) Me.Name=Name Me.HireDate=HireDate Me.Salary=StartingSalary Me.MinHoursPerWeek=MinHoursPerWeek EndSub
The TempEmployee Class
This class has a couple of notable items. Temporary employees have an expected termination date, which is entered as an argument to the Hire method (shown later). TempEmployee uses a public variable rather than a property to hold that date. A public variable is called a Field, which acts like a property but can be written and read without a property procedure.
PublicExpectedTermDateAsDateTime
Of course, when you use a Field you give up the validation and control that property procedures offer. Try setting ExpectedTermDate to a date in the past, for example, and it will be accepted because there’s no validation being done to it.
The second notable item is the TempEmployee implementation of Hire, which Shadows the Hire method in Employee. In other words, this version of Hire completely replaces the Hire method in the base class, which is therefore not accessible at all to TempEmployee. It’s a way of implementing a method in a completely different way than the base class, including having a different set of parameters, having a different return type, and in every way being independent of the original.
PublicShadowsSubHire(ByValNameAsStr ing,ByValHireDate_ AsDateTime,ByValStartingSalaryAs Decimal,ByVal_ EmploymentEndDateAsDateTime) Me.Name=Name Me.HireDate=HireDate Me.Salary=StartingSalary ExpectedTermDate=EmploymentEndDate EndSub
Using the Classes
Fire up the sample form, and you’ll be presented with data for a potential full-time employee. Click the Hire button, and the employee’s data will be set and shown in the text box on the right. Click the Save button, and the application simulates writing the data to the database. To understand what’s happening, put breakpoints on the first line of each of the following procedures: HireFullTimeEmployee, HireFullTimeEmployeeWithProperties, HirePartTimeEmployee, HireTempEmployee, and btnSave_Click. Each procedure is liberally commented.
Look at the EmployeeDataManager class, which simulates writing data to the database. Note that it is declared with the Friend keyword, which means it’s accessible to any code running within the same assembly. Also note its Shared methods, which can be used without creating an EmployeeDataManager object.
In this application, you’ve seen that inheritance lets you set a standard and then customize it in classes you derive from the base. An abstract class (declared with the MustInherit keyword) is a great way to set that standard. Derived classes get all the characteristics of the base class, but they can extend it with their own members.
This application demonstrates how to perform callbacks using both interfaces and delegates.
Building Upon… |
Application #79: Use Thread Pooling Application #84: Asynchronous Calls |
Visual Basic .NET makes it easy for you to call a function by its address. This is an important feature because that’s how you set up a callback.
Callbacks—Conceptual
What is a callback function, and why would you need one? A callback function is a reference to a method that you can pass to a second method. At some point, the second method invokes the reference, thereby calling back to the first method. You can use a callback to notify you when a job is done by calling a procedure in your code. First you initiate the action you want to happen, and then when it’s done, it causes the callback procedure in your main code to be executed.
Let’s say you want to delete a set of backup files, sort a group of objects, or perhaps enumerate all the files with a certain extension on your hard drive. You want to call a procedure to take care of these tasks, and you want that procedure to run another procedure once it’s finished. That’s when a callback fits the bill.
Creating and Using Callbacks
First you create the method you want the callback function to execute. This is usually a method that will take an action after some other action is complete. For example, suppose you want to create a call to a method that enumerates the *.vb files in your Microsoft Visual Studio Projects folder while your main code goes on to do something else. In addition, you want to be notified when the enumeration is done and create a list of the files that were found. You could write a procedure named PrepareListAfterEnumeration and make it your callback function.
The sample application, shown in Figure 2-10, contains code that illustrates how to create and use callbacks.
Figure 2-10: A callback function is a reference to a method you can pass to a second method. This process lets you determine, at run time, which method should be called.
In this sample application, the callback function is simple—it just pops up a message box telling you that it’s been executed. The Implements statement is there only because we want to be able to call this procedure via an interface, as well as directly.
PublicSubCallbackMethod()ImplementsICallb ack.CallbackMethod MessageBox.Show("Processingcomplete.We' reintheCallbackmethod",_ Me.Text,MessageBoxButtons.OK,Messag eBoxIcon.Information) EndSub
Using an Interface
If you care to, you can use an interface to call your callback function. You declare the interface like this:
InterfaceICallback SubCallbackMethod() EndInterface
Then you implement that interface on the callback procedure (shown in the preceding CallBackMethod), and set the whole thing up by registering your client class with the class that’s going to do the work. In the following click event, we create an instance of the CallbackViaInterface class, register this class (frmMain) with the instance, and then call its DoSomeProcessing method. When that method is done, it calls back into the client via the ICallback interface. Finally, we unhook our class from the CallbackViaInterface class.
PrivateSubcmdInterfaceCallback_Click(... DimcviAsNewCallbackViaInterface(lblRe sults) cvi.RegisterInterFace(Me) cvi.DoSomeProcessing() cvi.UnRegisterInterface() EndSub
Here’s what the CallbackViaInterface class looks like. A private field named icb will hold a reference to the ICallBack interface that we’ll later use to call our callback procedure. Another private field, ResultsLabel, is initialized in the constructor and refers to a label on the demo form that will be updated while DoSomeProcessing is busy working.
FriendClassCallbackViaInterface PrivateicbAsICallback PrivateResultsLabelAsLabel PublicSubNew(ByVallblAsLabel) ResultsLabel=lbl EndSub
The following DoSomeProcessing method does the actual work the class is intended to accomplish. In this example, we’re simply running a counter, but we could perform any number of other operations, such as sorting objects, deleting backup files, or counting the number of *.vb files in a folder. Once we’re done processing, we invoke the interface’s CallBackMethod method, which in turn calls the procedure in the client code that the interface is pointing to—we call back into the client code.
PublicSubDoSomeProcessing() IfNoticbIsNothingThen DimiAsInteger Fori=1To3000 ResultsLabel.Text=_ "Processingin'CallbackViaInterface'" &vbCrLf_ &i.ToString() ResultsLabel.Refresh() Next icb.CallbackMethod() EndIf EndSub
For all the above to work, we had to register the calling class with the worker class. So the worker class has a procedure for that as well as an unregister procedure for unhooking the caller from the worker.
PublicSubRegisterInterFace(ByValcbAs ICallback) icb=cb EndSub ⋮ EndClass
Using Delegates
Another way to call the worker class and implement callbacks is through delegates. A delegate is a reference to a method that has the same signature as that method, but has no implementation. The advantage of a delegate is that you can use it to call any method whose signature it matches, allowing you to select different methods at run time based on input from the user or based on the results of previous processing.
In our example (which resembles our earlier interface-based technique), we create an instance of the CallbackViaDelegate class and then instantiate a DelegateForCallback object, passing it the address of the procedure we want it to execute later.
PrivateSubbtnDelegateCallback_Click(... DimcvdAsNewCallbackViaDelegate(lblRes ults) DimdAsNewDelegateForCallback(AddressO fMe.CallbackMethod) cvd.RegisterDelegate(d) cvd.DoSomeProcessing() cvd.UnRegisterDelegate() EndSub
The CallbackViaDelegate class resembles the CallbackViaInterface class, but it uses a delegate rather than an interface. The delegate is declared outside the class and must be unique within the project. The following line of code shows how the delegate is declared. (Note that it takes no parameters, which means it can be a stand-in for any method that also takes no parameters, including our CallbackMethod procedure.)
DelegateSubDelegateForCallback()
We use a private field to hold an instance of the delegate, and then assign it to the delegate that gets passed in to the RegisterDelegate procedure. Keep in mind that the delegate is now pointing to the address of our CallbackMethod procedure.
Privatedel1AsDelegateForCallback ⋮ PublicSubRegisterDelegate(ByValdAsDelega teForCallback) del1=d EndSub
Our DoSomeProcessing method does some work and then uses the registered client reference (the delegate) to call back to the client. Note the optional argument indicating whether or not we want an asynchronous callback. If we don’t, we call Invoke, which means, “Call the procedure that the delegate is pointing to.” If we do want an asynchronous callback, we call BeginInvoke, which is similar to Invoke, except that the CLR uses a separate worker thread from its thread pool to call the delegate’s target procedure. The procedure then runs parallel to this code.
PublicSubDoSomeProcessing(OptionalByValas yncAsBoolean=False) ⋮ Ifasync=FalseThen del1.Invoke() Else del1.BeginInvoke(Nothing,Nothing ) EndIf ⋮ EndSub
Using Built-In Callbacks
There’s still another technique you can use: built-in callbacks. Delegates have a built- in mechanism to call back to the client, as long as the signature of the method to be called back to matches the signature of the AsyncCallback delegate class (that is, it accepts a single parameter, of the IAsyncResult type). The AsyncCallback class is defined in the CLR specifically for calling back after an asynchronous invocation of a delegate.
This method will make an asynchronous call on a delegate and use its built-in callback to invoke the BuiltInCallback method. No user registration is needed because we’re passing the callback delegate (ac) as an argument to the BeginInvoke method, which calls the delegate asynchronously.
PrivateSubbtnBuiltInCallback_Click(... DimcvbAsNewCallbackViaBuiltIn(lblResu lts) DimdAsNewDelegateForCallback(AddressO fcvb.DoSomeProcessing) DimacAsNewAsyncCallback(AddressOfMe. BuiltInCallback) d.BeginInvoke(ac,Nothing) EndSub
Callbacks are perfect when you want to execute a method or series of methods and then have your calling code taking some action when the called code has finished its work.
Whereas C# developers revel in their built-in XML documentation capabilities, Visual Basic .NET developers have no such intrinsic documentation tool. This sample corrects that imbalance by providing a tool you can use to create XML documentation files for your library projects.
Building Upon… |
Application #37: Use Menus Application #66: Build a Custom Collection Class Application #70: Reflection |
Have you noticed how IntelliSense offers help on each class, method, and parameter as you type? Not only does it offer the available choices, it also provides a description of each item that helps to guide you. It seems to happen automatically for all the native .NET classes, but for the classes you create, it presents only the names of the items with no descriptions. Have you ever wished you could arrange to have such descriptions displayed for your classes? Well, you can, by creating an XML documentation file containing those descriptions.
When you add a reference to a component (let’s say foo.dll), Visual Studio .NET searches the referenced component’s folder for an XML file with the same base name (foo.xml). It reads the documentation from the file, and when you refer to classes, methods, and parameters from foo.dll in the code editor, the information about each one is displayed in IntelliSense. You can also use the Object Browser to view summary information, parameter information, and any remarks.
The .NET documentation offers details on how an XML documentation file should be structured, but it provides little information on how to automate creating and managing such a file. This application lets you create and manage an XML documentation file for your component. Figure 2-11 shows an example of the XML Documentation Tool in action.
Figure 2-11: With this tool, you can add documentation to your library projects that will show up in the Object Browser and IntelliSense.
What’s in the Solution
The solution contains both the XMLDocumentationTool project and a SampleComponent class library project to be used for demonstration. The SampleComponent library contains two classes, SampleClass and SampleClass2. Both classes are identical, but we’ve documented the SampleClass by using the XML Documentation Tool. Here’s what SampleClass looks like:
PublicClassSampleClass 'Storetheaccountnumberinternally. 'TheXMLdocumentationtoolignorespriv atevariables. Privatem_AccountNumberAsString PublicSubNew() MyBase.new() EndSub 'Createacustomerandassigntheiracco untnumber. PublicSubNew(ByValacctNumAsString) MyBase.new() m_AccountNumber=acctNum EndSub PublicPropertyAccountNumber()AsString Get Returnm_AccountNumber EndGet Set(ByValValueAsString) m_AccountNumber=Value EndSet EndProperty PublicFunctionLookUpCustomer(ByValcust omerNameAsString)AsString 'Codetofindcustomerbynameinda tabase. EndFunction PrivateSubSomeOtherProcedure() 'Aprivateprocedure. 'TheXMLdocumentationtoolignores privateprocedures. EndSub EndClass
Testing Existing Documentation
Now, let’s see how some existing documentation shows up as you work. You’ll need to be sure the XML documentation file is in the same folder as the DLL it documents. We’ve provided a starter XML file in the root folder of the XML Documentation Tool. To use it, follow these steps:
Now you want to test the documentation, so do the following:
You should see Summary And Remarks information for the namespace in the bottom pane. Now expand SampleNamespace, and select SampleClass. You’ll see Summary And Remarks information for the class in the bottom pane and the members of the class in the right pane. Select each member in turn, and view the documentation below it. (For some items, the documentation will show only in IntelliSense, not in the Object Browser.)
Now let’s see how IntelliSense uses the information you just saw. Double- click Form1 to get into its Form_Load event procedure, and declare a new instance of SampleClass by typing the following (being careful not to type the whole word SampleNamespace):
Dim samp As New SampleNamesp
Notice how IntelliSense selects SampleNamespace in the drop-down list and presents a ToolTip-type popup describing the namespace. Type a period and IntelliSense will complete the name and present a list of the classes in the namespace: SampleClass and SampleClass2. Click once on SampleClass, and you’ll see a description of the class. Type an opening parenthesis and IntelliSense will finish typing SampleClass and offer a list of its constructor overloads. Notice that the popup includes a description of the AcctNum parameter.
Using the Documentation Tool
Now let’s see how this information came to be available to IntelliSense and the Object Browser. Re-open the XML Documentation Tool solution, and press F5 to execute it. Select File | Open Assembly, and browse to SampleComponentin. Choose SampleComponent.dll, and click Open and OK. SampleNameSpace appears.
Expand the SampleNamespace node, and you’ll see the two classes it contains. Notice that SampleClass is shown in bold, which means it has documentation, while SampleClass2 does not. Expand SampleClass, and you’ll see its public members. Right-click each one and then choose Open, and you’ll see the documentation we provided. Also examine the class and the namespace, which have their own documentation.
The data is saved in SampleComponent.xml, in the component’s bin folder. The file looks like this:
SampleComponent 1.0.0.0 SampleComponent,Version=1. 0.0.0,Culture=neutral, PublicKeyToken=null Samplecomponentfordem onstratingXML documentation. Thiscomponentisnotfu nctional.It'sdesigned fordemonstrationpurposes. Sampleclassfordemonst ration. Thisclasshasmethodsa ndpropertiesthatare documentedfordemonstrationpurp oses. Thecustomer'saccountn umber. Valuemustbebetween1an d999. Althoughtheaccountnum berisnumeric,itis storedasastring. LookUpCustomer(System.String)"> Locateacustomerbynam e. Thecu stomer'sname. ReturnsthecustomerID asastring. Helpsinfindingacusto merwhoseaccount numberisnotknown. ⋮
Now add some documentation of your own. Right-click on SampleClass2, and choose Open. Enter Summary and Remarks information, add any other documentation you want for the members of the class, and then press Save.
Test it in the Windows Application you created earlier. (If it’s still open, you’ll need to close and reopen it for Visual Studio to read the changed XML file.) You should see the additional documentation you entered.
You can open any existing component and document it. The tool will create a corresponding XML file and put it in the same folder as the DLL. As you work with the tool, note that:
The XML Documentation Tool makes it simple for you to document your component so that important information about it is available to IntelliSense and the Object Browser. Whereas C# programmers have a built-in capability for documenting their code directly in their classes, this tool gives equivalent functionality to the Visual Basic .NET developer. The tool can be used for assemblies built in languages other than Visual Basic .NET, as long as the assemblies are common language specification (CLS)–compliant managed-code assemblies, such as those created in C#.
Visual Basic .NET offers a variety of innovations and enhancements over Visual Basic 6. This sample demonstrates several new features. They’re described in much greater detail in other applications in this series, but we’re providing a quick overview here just to whet your appetite. Figure 2-12 shows the sample application in action.
Figure 2-12: The sample application creates a thread and sends it off to run a particular procedure. Meanwhile, the original thread continues its work. It’s easy to do with Visual Basic .NET.
Building Upon… |
Application #3: String Manipulation Application #4: Try...Catch...Finally Application #5: Custom Exceptions Application #7: Object-Oriented Features Application #8: Scoping, Overloading, Overriding Application #54: Work with Environment Settings Application #55: Use the File System Application #57: Use the Event Log Application #73: Read From and Write To a Text File Application #76: Create Trace Listeners Application #79: Use Thread Pooling |
Features shown in this sample include:
We’ll briefly discuss each feature.
Debugging/Tracing
You can easily write debugging information to the console, which is the default listener, with a simple Write or WriteLine statement:
Debug.WriteLine(strDebug)
When you want to write the debug information to a file, you can do so by creating a file for output and then creating a text writer and adding it to the debug listeners, like this:
DimstrFileAsString= "C:DebugOutput.txt" DimstmFileAsStream=File.Create(strFile) DimtwTextListenerAsNewTextWriterTraceList ener(stmFile) WithDebug.Listeners .Clear() .Add(twTextListener) EndWith Debug.Write(strDebug) Debug.Flush()
Writing to the Event Log is easy. Just create a listener for the event log, add it to the Listeners collection, and write to it.
DimlogdebugListenerAsNewEventLogTraceList ener(_ "101VB.NETSampleApplications:WhyVB.NETis Cool") WithDebug.Listeners .Clear() .Add(logdebugListener) EndWith Debug.Write(strDebug) Debug.Flush()
Exception Handling
Structured Exception Handling is much more robust and extensible than On Error GoTo. Here we’re trying to open a file in a nonexistent directory. We can catch the specific exception associated with this type of situation, and we can accommodate other exceptions as well. The Message property is like the Visual Basic 6 Err.Description, and StackTrace shows the sequence of calls that got us here.
PrivateSubExceptionReadingFromFile() Try DimswAsNewStreamWriter("c:123456 78asdfaddirectory.txt") CatchexpDirNotFoundAsDirectoryNotFound Exception txtExceptionHandlingResult.Text= "Message: " &_ expDirNotFound.Message&vbCrLf&vbC rLf txtExceptionHandlingResult.Text&= "StackTrace: " &_ expDirNotFound.StackTrace CatchexpAsException MsgBox(exp.ToString(),MsgBoxStyle.OK OnlyOr_ MsgBoxStyle.Critical,Me.Text) EndTry EndSub
File Handling
The .NET Framework has a variety of classes that make file and directory handling convenient, including File, FileInfo, Directory, DirectoryInfo, StreamWriter, StreamReader, FileStream, Path, and others. Reading from a file is as simple as:
DimsrAsNewStreamReader(strFile) txtFileResult.Text=sr.ReadToEnd() sr.Close()
The following code shows how you can write to a file. (We’re creating the file with CreateText, one of the shared methods of the File class.)
DimswAsStreamWriter=File.CreateText(strF ileWrite) sw.WriteLine("Thequickbrownfoxjumpedover the " &_ "lazydogs.") sw.Flush() sw.Close()
The FileInfo class lets you copy, delete, move, and collect information about a file. In this case, we’re simply checking the file’s size:
DimfiAsNewFileInfo(strFileWrite) txtFileResult.Text= "Sizeof " &_ strFileWrite.Substring(InStr("/ ",strFileWrite))& ": " &_ fi.Length.ToString+ " bytes."
When you need a temporary file name, a simple method call provides it:
txtFileResult.Text= "Tempfilename: " &Path.GetTempFileName
Forms and Graphics
On the sample form, frmControls, the Name text box is anchored to the Top, Left, and Right. This arrangement means that when the form is resized, the text box will automatically resize with the form, maintaining its relative position to those three points. The Address text box is anchored to the Top, Bottom, Left, and Right, so it will automatically resize all its dimensions with the form. The text box at the bottom is docked to the bottom of the form. Docking glues a control to one or more edges of the form so that the text box will maintain its original height, stay docked to the bottom, and expand or contract horizontally when the form is resized.
Working with graphics is easier than ever, but significantly different than in Visual Basic 6. Drawing a circle (in frmGraphics) involves creating a Graphics object, clearing the PictureBox control, and then creating a Pen object and drawing with it. Note how the graphics object is created by calling a method on the object it will later interact with, the PictureBox.
DimgAsGraphics=picDrawing.CreateGraphics () g.Clear(Me.BackColor) DimpAsNewPen(Color.Red,3) g.DrawEllipse(p,120,120,100,100) g.Dispose()
Drawing a line or rectangle is equally simple. But you might be surprised to find that writing text graphically is much like drawing a shape. For example, the sample application creates some text in the PictureBox with the DrawString method of the Graphics object.
g.DrawString("VB.NET",NewFont("Arial",20), Brushes.Blue,135,135)
Inheritance
Inheritance lets you take an existing class and make it the prototype for derived classes. The sample application has an Employee class, from which are derived FullTimeEmployee, PartTimeEmployee, and TempEmployee. Each of the derived classes has all the characteristics of the original (base) class but can implement functionality of its own. See the code for extensive comments, and refer to Application #8: Scoping, Overloading, Overriding for much more information.
StringBuilder
The StringBuilder exists to speed up manipulation of strings. The sample repeatedly appends a string to the StringBuilder, an action that would create a separate String object for each concatenation if it were done the traditional way. Here are both methods. When you run the application, note how much faster the StringBuilder is.
tmr.Begin() strConcatenated=strSBOrig Fori=1TointStrIterations strConcatenated=strConcatenated&strSB Append Next tmr.End() tmr.Begin() Fori=1TointSBIterations sb.Append(strSBAppend) Next tmr.End()
Threading
The sample application creates a thread and sends it off to run a particular procedure. Meanwhile, the original thread continues its work. The AddressOf operator creates a delegate that references the CalledByThread procedure. When the Start method is invoked, the thread executes the procedure associated with the delegate.
newThread=NewThread(AddressOfCalledByThre ad) newThread.Name= "NewDemoThread" newThread.Start()
In this example, both the current thread and the new thread are doing some work—in this case, simply running a numeric loop. Note the use of Application.DoEvents method in each thread’s loop, which ensures that the thread yields to other threads and does not monopolize the CPU’s time while it’s looping.
DimiAsInteger Fori=0TomaxCount lblCurrCounter.Text= "Orig-- " &i.ToString() lblCurrCounter.Refresh() Application.DoEvents() Next
Visual Basic .NET leverages a variety of features of the .NET Framework as well as language-specific features of its own to provide a language that is more robust and capable than Visual Basic 6 and that is truly object oriented.
Be sure to refer to other applications in this series for more detailed explanations of the features presented in this sample.
About the Applications