Working with the Microsoft Visual Basic .NET Language
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.
Application #1 Use Arrays
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.
New Concepts
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.
Code Walkthrough
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.
Conclusion
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.
Application #2 Use DateTimes
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.
New Concepts
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.
Code Walkthrough
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:
- NowThe current date and time as set on your system. Similar to the Visual Basic 6 function of the same name.
- TodayThe current date as set on your system.
- UtcNowThe current date and time as set on your system, expressed as coordinated universal time (UTC), also known as Greenwich Mean Time.
- MinValueThe smallest possible value a DateTime can have.
- MaxValueThe largest possible value a DateTime can have.
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:
- DaysInMonthGiven a year and month, returns the number of days in that month.
- FromOADateReturns a DateTime value that corresponds to the provided Ole Automation Date, which is a floating-point number representing the number of days from midnight, December 30, 1899. Use this method to convert dates stored in Microsoft Excel—for example, to .NET DateTime format.
- IsLeapYearGiven a four-digit year, returns True if it is a leap year and False otherwise.
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.)
- DateThe date that dt contains.
- DayAn integer between 1 and 31, representing the dt day of the month.
- DayOfWeekAn enumerated constant that indicates the day of the week in the dt date, ranging from Sunday to Saturday.
- DayOfYearAn integer between 1 and 366, representing the dt day of the year.
- HourAn integer between 0 and 23, representing the dt hour of day.
- MillisecondAn integer between 0 and 999, representing the millisecond of the dt time.
- MinuteAn integer between 0 and 59, representing the minute of the dt time.
- MonthAn integer between 1 and 12, representing the dt month of the year.
- SecondAn integer between 0 and 59, representing the second of the dt time.
- TicksA Long containing the number of Ticks in the dt date and time, counting from 12:00 A.M., January 1, 0001. A Tick is a 100-nanosecond period of time.
- TimeOfDayA TimeSpan that represents the fraction of the day between midnight and the dt time.
- YearAn integer that represents dt year, between 1 and 9999.
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:
- ToFileTimeA system file time is a Long representing a date and time as 100-nanosecond intervals since January 1, 1601, 12:00 A.M.
- ToLocalTimeAssumes that the parameter passed to it represents UTC time, and returns a DateTime, converted to local time, allowing for daylight savings time. The opposite of ToUniversalTime.
- ToLongDateStringIf the current culture is us-EN, returns a String with the date in the form: Monday, November 15, 2004. The format of the string varies depending on the current culture.
- ToLongTimeStringIf the current culture is us-EN, returns a String with the time in the form: 5:03:29 PM. The format of the string varies depending on the current culture.
- ToOADateConverts a DateTime to its OLE Automation date equivalent.
- ToShortDateStringIf the current culture is us-EN, returns a String with the date in the form: 11/15/2004. The format of the string varies depending on the current culture.
- ToShortTimeStringIf the current culture is us-EN, returns a String with the time in the form: 5:03:29 PM. The format of the string varies depending on the current culture.
- ToStringPresents the DateTime value as a string, with many formatting choices.
- ToUniversalTimeAssumes that the parameter passed to it represents local time, and returns a DateTime, converted to UTC time. The opposite of ToLocalTime.
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
Conclusion
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.
Application #3 String Manipulation
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.
New Concepts
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.
Code Walkthrough
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:
- InsertTo insert one string into another, use the String class Insert method, specifying where to insert and what to insert. (In this case, insert the phrase “ happily”—note the space—at index 19.) Result below: “the quick brown fox happily jumps over the lazy dog”.
txtResults.Text=sampleString.Insert(19, " happily")
- RemoveTo remove a string from within another, use the Remove method, specifying the start point for removal and how many characters to remove. (In this case, remove 6 characters beginning at index 10.) Result below: “the quick fox jumps over the lazy dog”.
txtResults.Text=sampleString.Remove(10,6)
- ReplaceTo replace a part of a string with some other string use the Replace method, providing the old value and the new. Result below: “the quick brown fox leaps over the lazy dog”.
txtResults.Text=sampleString.Replace("jumps ", "leaps")
- PadLeft/PadRightSometimes you want to pad a string, ensuring that it’s at least n characters long. The PadLeft method lets you pick a character and use it to pad your string. If you don’t provide a padding character, it defaults to the space character. PadRight is just like PadLeft, except that the padding characters are added to the end of the string. Original: “123.45”, Result 1: “ 123.45”, Result 2: “$$$$123.45”.
txtResults.Text=sampleString.PadLeft(10) txtResults.Text=sampleString.PadLeft(10, "$")
- SubstringThis method is similar to the old Visual Basic 6 favorite Mid. It lets you extract a portion of a string. You provide a start index and specify how many characters you want. If you don’t specify a length, Substring returns all remaining characters. But be careful: whereas the Mid index starts at one, the Substring index, like everything else in .NET, is zero-based. Result 1 below: “own fox jumps over the lazy dog”. Result 2: “own f”.
txtResults.Text=sampleString.Substring(12) txtResults.Text=sampleString.Substring(12, 5)
- ToLower/ToUpperTo transform a string to lowercase or uppercase, invoke the ToLower or ToUpper method, neither of which takes any parameters. Original: “This Sample has SOME mixed-CASE Text!”, Result 1 below: “this sample has some mixed-case text”, Result 2: “THIS SAMPLE HAS SOME MIXED-CASE TEXT!”.
txtResults.Text=sampleString.ToLower txtResults.Text=sampleString.ToUpper
- TrimOne of the best things you can do when you accept input from a user is to trim it, removing unintended white space at the beginning and end of the input. The Trim method lets you do that and more: it lets you provide an array of characters to be removed. Original: “ the quick brown fox jumped over the lazy dog ”. Result 1: “the quick brown fox jumps over the lazy dog”. Result 2: “ quick brown fox jumps over the lazy ”. Note that in the second example the characters to be removed include both letters and a space. Also note that the characters must be submitted as a character array.
txtResults.Text=sampleString.Trim() txtResults.Text=sampleString.Trim("thedog" .ToCharArray())
- TrimEnd/TrimStartThese are just like Trim, except they work on the end and start of the string, respectively.
Methods that Return Information
The String class has a number of methods that help you get information about a string, including:
- IndexOf
- If you need to find the location of a character or string within another string, IndexOf does the job. It also lets you choose where to start looking and how many positions to examine. It returns the index of the found item if it succeeds, or -1 if it fails. Example 1 below: Find “brown” beginning at index 4, and examine 20 characters. Result 1: 10. Example 2: Find “brown” beginning at index 4, examining all remaining characters. Result 2: 10. Example 3: Find “brown” anywhere in the string. Result 3: 10.
txtResults.Text=sampleString.IndexOf("brown ",4,20).ToString txtResults.Text=sampleString.IndexOf("brown ",4).ToString txtResults.Text=sampleString.IndexOf("brown ").ToString
- IndexOfAnyLet’s imagine that you want to locate any one of a series of characters in a string, and you just need to know the first spot where any of them occurs. You’d use the IndexOfAny method in much the same way as you use IndexOf except that you pass a character array as the first parameter. Each of the following code snippets will return 7 as the index where one of the letters “a,” “b,” or “c” was found.
txtResults.Text=_ sampleString.IndexOfAny("abc".ToCharArray ,4,12).ToString txtResults.Text=_ sampleString.IndexOfAny("abc".ToCharArray ,4).ToString txtResults.Text=_ sampleString.IndexOfAny("abc".ToCharArray ).ToString
- LastIndexOf/LastIndexOfAnyThese methods are like IndexOf and IndexOfAny, but they report on the last occurrence of a string or character instead of the first.
- StartsWith/EndsWithThese methods return True or False if the target string starts or ends with a specified string. The comparison is case-sensitive. The following example returns False because “The qui” doesn’t match “the qui”.
txtResults.Text=sampleString.StartsWith("Th equi").ToString
- SplitThis method is handy for taking a string and slicing it into substrings based on one or more separators you provide. It returns an array containing the separated substrings. The first argument for Split is an array of separator characters, and the optional second argument is the maximum number of substrings you want. If you pass an empty string for the first parameter, Split will use white-space characters as separators. Result 1 below: An array containing “the qui”, “k”, “rown fox jumps over the l”, “zy dog”. Result 2: An array containing “the qui”, “k”, “rown fox jumps over the lazy dog”.
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.
- CompareWhen you want to compare two strings, the Compare method offers a variety of ways to do it. In addition to specifying the two strings to be compared, you can optionally specify whether to ignore case, where to start comparing, how many characters to compare, and which culture information to use. Compare returns a negative number if string A is less than string B, a positive number if it’s the other way around, and a zero if they’re equal. The comparison is lexical and takes into account the current culture. An uppercase “A” is considered greater than a lowercase “a”. Result 1 below: (ignores case): 0. Result 2: (uses default case sensitivity): 1.
txtResults.Text=String.Compare("Thisisat est",_ "thisisatest",True).ToString txtResults.Text=String.Compare("Thisisat est",_ "thisisatest").ToString
- CompareOrdinalThis method does its comparison checking without considering the local culture. It’s actually considerably faster than Compare because it doesn’t have to translate each character to a number representing the character’s position in the local alphabet. Like Compare, it allows comparison of substrings but has no other overloads. Result below: -32, because an uppercase “T” is lower in the ASCII collating sequence than a lowercase “t”.
txtResults.Text=String.CompareOrdinal("This isatest",_ "thisisatest").ToString
- ConcatYou can concatenate up to four strings with the Concat method, which also lets you pass it objects and ParamArrays of objects or strings. Result below: “This is a test of how this works when you concatenate”.
txtResults.Text=String.Concat("Thisisate st", " ofhowthisworks", " whenyouconcatenate").ToString
- FormatThe Format method lets you apply a variety of standard and custom formats to strings, numbers, dates, times, and enumerations. The items in curly braces in the following code are replaceable parameters, which will be filled by 12 and 17.35, respectively. The first item, “{0:N0}”, means “display item zero as a number with zero decimal places.” The second item, “{1:C}”, means “display item one as currency.” The Format method has several other numeric format specifiers, including: d (decimal), f (fixed), as well as date/time specifiers, like d (short date). You can also specify your own custom formats. Result below: “Your 12 items total $17.35.”.
txtResults.Text=_ String.Format("Your{0:N0}itemstotal{1 :C}.",12,17.35)
- JoinWhen you have an array that you want to convert into a single string, Join fills the bill. It lets you specify a separator character between the joined elements. Result below: “item1/item2/item3/item4”.
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)
Conclusion
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.
Application #4 Try Catch Finally
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.
New Concepts
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:
- Exceptions are generated and raised by the .NET runtime, which means not only that you don’t have to write a lot of code to use SEH, but also that client applications written in another .NET language can catch and respond to exceptions generated in your Visual Basic .NET application.
- Because each exception comes with its own object, you no longer have to write lengthy If or Select Case statements to check for specific error numbers. Instead, you can respond to each exception type in its own Catch block. (See the “Handling Specific Exceptions” section later in the chapter.)
- You can have multiple Try/Catch blocks within the same procedure, one for each piece of code you want to protect.
- You can nest Try/Catch blocks. For example, if a database connection fails in a Try block, your Catch block can have a nested Try/Catch that attempts to connect to an alternate server.
- The Try/Catch construct has an optional Finally block, in which you can place all your clean-up code. You don’t have to jump to it, it runs automatically.
Structured Exception Handling Specifics
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:
- MessageA description of the error.
- SourceThe name of the object or application that generated the error.
- StackTraceA list of the frames on the call stack at the time the exception was thrown. The StackTrace property includes information Visual Basic 6 developers have craved for years: the name of the original procedure where the exception was triggered, and the line number of the offending statement.
Other features of SEH include:
- The ToString method combines the name of the exception, the error message, the stack trace, and more.
- There’s no equivalent for On Error Resume Next.
- You can jump out of a Try/Catch with an Exit Try or GoTo statement, but code in your Finally block will still be executed.
- You can omit the Catch block if you have a Finally block.
- You can create your own exceptions. (See the Custom Exceptions sample later in the chapter.)
- SEH can even handle exceptions generated by unmanaged code.
- Like Visual Basic 6, when an exception occurs in a procedure without a Try/Catch, the .NET runtime walks back up the call stack looking for an active exception handler. If it finds one, it passes control to it; otherwise, it generates a default exception response.
Code Walkthrough
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
Conclusion
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.
Application #5 Custom Exceptions
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 |
New Concepts
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:
- Think carefully before you create a custom exception, and do so only if you’re sure there isn’t already a built-in one that meets your needs.
- Trigger your custom exception only when there’s an exceptional event, not for common errors, such as a failed File.Open statement.
- Don’t use a custom exception for controlling program flow.
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.
Code Walkthrough
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:
- There’s no active debugger.
- You don’t have your own exception handler in place.
- You haven’t turned on just-in-time (JIT) debugging in your application’s config file.
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
Conclusion
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:
- You create a custom exception by defining it in its own class.
- Base your custom exceptions on System.ApplicationException.
- Custom exceptions can be thrown and caught just like built-in exceptions.
- You will usually create custom exceptions to handle violations of business rules in your application, as opposed to run-of-the-mill errors, for which there is probably an exception class already defined.
- By exposing custom properties, your custom exceptions can provide additional information that might be useful to the client.
- Document your custom exceptions thoroughly so that a developer using your application knows what circumstances will trigger each exception, and what information it provides in addition to regular exception properties.
- To conform to convention, name your custom exceptions with a suffix of Exception.
- Consider attaching your own handler to Application.ThreadException to trap unhandled exceptions.
Application #6 Walking the Stack
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 |
New Concepts
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.
Code Walkthrough
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
Conclusion
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.
Application #7 Object Oriented Features
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 |
New Concepts
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:
- Class
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.
- Object
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:
- PropertyA characteristic of a class. For a telephone, it could be Color or Material, each with a value like Beige or Plastic. A Field is similar to a property, but it can be changed more easily. Properties and fields are used to hold information related to the object.
- MethodSomething the object can do or can be used to do. A telephone can Dial, for example.
- EventAn object’s way of notifying you that something has happened. A telephone Rings when it gets an incoming signal.
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:
- EncapsulationAn object’s means of keeping its members self-contained, controlling how they are changed, and hiding from the user how it does its internal work.
- InheritanceThe ability to create a new class that’s derived from an existing one (known as the base class). The derived class gets all the members of the base class, but it can add its own, modify the way its inherited members work, or both.
- PolymorphismThe ability to have multiple classes that can be used interchangeably, even though each class implements the same properties or methods in different ways.
Code Walkthrough
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.
Conclusion
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.
- You instantiate (create) an object by using the New keyword. An object is a concrete instance of a class, which is a blueprint or pattern that defines the object’s characteristics.
- A constructor in a Visual Basic .NET class is a Sub procedure named New, which might or might not accept parameters. It runs each time an object gets instantiated from the class, so it’s a good place to initialize values of the new object.
- Classes with parameterized constructors let you create the object and assign values to properties all in one action, a very efficient way to go.
- When you find that you need a class that’s very much like one you already have but needs some special properties or methods, create a new class that inherits from the original, and then add the members you need.
- When you inherit, the original class is called the base class and the inherited one is called the derived class. This process is known as subclassing.
Application #8 Scoping, Overloading, Overriding
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:
- Only full-time employees get annual leave.
- Only temporary employees have an expected termination date when they are hired.
- Part-time employees are required to work at least 20 hours per week.
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 |
New Concepts
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:
- PublicWhen a class or procedure is marked Public, any code that can get to it can use it with no restrictions.
- ProtectedClass members declared with the Protected keyword are accessible only from within their own class or from within a derived class.
- FriendClasses and procedures declared with the Friend keyword are accessible from within the program where they are declared and from anywhere else in the same assembly.
- PrivateClass members declared with the Private keyword are accessible only from within the class where they are declared.
- SharedProcedures declared with the Shared keyword can be used without necessarily having to create an instance of the class they belong to. You can call a shared procedure either by qualifying it with the class name (EmployeeDataManager.WriteEmployeeData) or with the variable name of a specific instance of the class (edmManager.WriteEmployeeData). You can also declare fields and properties as Shared.
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:
- InheritsTells you which base class the current class is inheriting from.
- NotInheritablePrevents the class from being used as a base class.
- MustInheritYou can’t create an instance of this class. The only way to use the class is to inherit from it.
- OverridableUsed to mark a property or method in a base class. It means derived classes can have their own implementation of the property or method. Public methods are NotOverridable by default.
- OverridesUsed in a derived class, it allows you to override a property or method that’s defined in the base class.
- NotOverridable (default)Used in a base class, it prevents a property or method from being overridden in a derived class.
- MustOverrideUsed on a property or method in the base class, it requires the derived class to override the property or method.
- ShadowsUsed in a derived class, it lets you use the name of an inherited class member but replace it completely with your own implementation. The inherited type member is then unavailable in the derived class.
Code Walkthrough
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.
Conclusion
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.
- Don’t confuse overloading and overriding. They sound alike, but they’re very different. Overloading lets you have multiple versions of a method. Overriding lets a derived class implement a base class method in its own way.
- Only methods or properties that you declare as Overridable can be overridden in derived classes.
- When a method is overloaded, each version must have a unique combination of parameters. Overloading is one way of allowing a method to be called with different combinations of parameters. However, you can also make parameters optional with the Optional keyword.
- Always use the least public method that will meet your needs when you declare variables, methods, properties, and classes. The greater the scope of a variable, the more resources are required to carry it around, and the more likely it can be modified when you don’t intend for it to be.
- Use property procedures rather than fields for holding object data. It’s easier to implement a field (which you can do just by declaring a public variable), but property procedures let you control and validate the data values that are set and retrieved.
Application #9 Use Callbacks
This application demonstrates how to perform callbacks using both interfaces and delegates.
Building Upon… |
Application #79: Use Thread Pooling Application #84: Asynchronous Calls |
New Concepts
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.
Code Walkthrough
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
Conclusion
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.
- Callbacks are useful for applications with long-running procedures, where you’d like your code to do something else in the meantime, but be notified when the lengthy operation is complete.
- You might want to use a callback to keep you posted on the status of a long-running event, such as a File Transfer Protocol (FTP) download, by periodically updating a progress bar.
- You can also use callbacks for notifying you when an event occurs, such as a form being closed, a numeric threshold being reached, or a long download being completed.
Application #10 Use XML Comments
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 |
New Concepts
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.
Code Walkthrough
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:
- Build the XMLDocumentationTool solution.
- Copy SampleComponent.xml from the root folder of the application to SampleComponentin. (You can do this in Visual Studio .NET if you want to.)
Now you want to test the documentation, so do the following:
- Create a new Windows Application project. It must be in a separate solution from the component.
- Set a reference to SampleComponentinSampleComponent.dll.
- Open the Object Browser, and select SampleComponent.
- Expand SampleComponent, and select SampleNamespace.
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:
- You can copy information from one node to another. Just select the first node and drag it onto the node you want to copy the information to.
- You can use the tool to edit documentation only for public and protected types and members that are exposed from the assembly file. Private and Friend methods or classes are not exposed and, therefore, are not viewable in the tree view.
- You can search for a specific member by selecting Find from the Edit menu.
- When there are conflicts between the information in the XML file and the assembly, the XML Documentation tool lists these conflicts as errors, along with the path to the node creating the error and a brief description of the error. The status bar displays the number of errors. Errors occur when the assembly and its XML documentation file are out of sync. See the tool’s Help file for more details on possible errors.
Conclusion
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#.
Application #11 Key Visual Basic NET Benefits
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 |
New Concepts
Features shown in this sample include:
- Debugging/TracingYou can use the Debug and Trace classes during development to help debug your application by writing variable values, status messages, and anything else you consider useful to a Listener. The Listener can be the console, a file, or the Event Log. Debug and Trace are almost mirror images of each other, except that the compiler removes Debug code when it produces a release build of your application but keeps Trace code.
- Exception handlingVisual Basic .NET structured exception handling outclasses On Error GoTo because you can manage errors more methodically. You can respond to generic exceptions, or you can easily target specific exceptions found in the .NET Framework. You can even create your own exceptions derived from the ApplicationException class.
- File handlingReading from and writing to files is easier with the StreamReader and StreamWriter classes. You can manipulate files and get file information with the FileInfo and FileVersionInfo classes. And if you need a temporary file, it’s readily available by using the GetTempFileName method of the Path class.
- Forms and graphicsIf you ever wrote code to make controls resize when a user resized your form, you’ll appreciate the simplicity of Anchoring and Docking. By setting a control’s Anchor property, you can make it elastic, meaning it keeps its relationship to any of the four sides of the form. Docking lets you glue one or more edges of the control to one or more sides of the form. The new Graphics objects in Visual Basic .NET let you draw shapes and text wherever you care to, in any color and font.
- InheritanceWith inheritance, you can use any class as a base or template for new classes. See the classes named Employee, EmployeeDataManager, FullTimeEmployee, PartTimeEmployee, and TempEmployee for extensive comments on inheritance, overloading, overriding, and scoping.
- StringBuilderOne of the most expensive operations your application can perform is manipulating strings. The StringBuilder provides a more efficient way to do it. The StringBuilder manipulates strings without repeatedly creating new strings, as happens when you use traditional string methods to do simple things such as concatenation. Whenever you expect to make multiple adjustments to a string, use the StringBuilder. It is orders of magnitude faster than traditional string manipulation.
- ThreadingThe .NET Framework provides great control over individual threads. You can run any procedure on its own thread, set thread priorities, suspend and resume threads, and perform any number of other thread-related operations.
Code Walkthrough
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
Conclusion
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.