- •Using Your Sybex Electronic Book
- •Acknowledgments
- •Contents at a Glance
- •Introduction
- •Who Should Read This Book?
- •How About the Advanced Topics?
- •The Structure of the Book
- •How to Reach the Author
- •The Integrated Development Environment
- •The Start Page
- •Project Types
- •Your First VB Application
- •Making the Application More Robust
- •Making the Application More User-Friendly
- •The IDE Components
- •The IDE Menu
- •The Toolbox Window
- •The Solution Explorer
- •The Properties Window
- •The Output Window
- •The Command Window
- •The Task List Window
- •Environment Options
- •A Few Common Properties
- •A Few Common Events
- •A Few Common Methods
- •Building a Console Application
- •Summary
- •Building a Loan Calculator
- •How the Loan Application Works
- •Designing the User Interface
- •Programming the Loan Application
- •Validating the Data
- •Building a Math Calculator
- •Designing the User Interface
- •Programming the MathCalculator App
- •Adding More Features
- •Exception Handling
- •Taking the LoanCalculator to the Web
- •Working with Multiple Forms
- •Working with Multiple Projects
- •Executable Files
- •Distributing an Application
- •VB.NET at Work: Creating a Windows Installer
- •Finishing the Windows Installer
- •Running the Windows Installer
- •Verifying the Installation
- •Summary
- •Variables
- •Declaring Variables
- •Types of Variables
- •Converting Variable Types
- •User-Defined Data Types
- •Examining Variable Types
- •Why Declare Variables?
- •A Variable’s Scope
- •The Lifetime of a Variable
- •Constants
- •Arrays
- •Declaring Arrays
- •Initializing Arrays
- •Array Limits
- •Multidimensional Arrays
- •Dynamic Arrays
- •Arrays of Arrays
- •Variables as Objects
- •So, What’s an Object?
- •Formatting Numbers
- •Formatting Dates
- •Flow-Control Statements
- •Test Structures
- •Loop Structures
- •Nested Control Structures
- •The Exit Statement
- •Summary
- •Modular Coding
- •Subroutines
- •Functions
- •Arguments
- •Argument-Passing Mechanisms
- •Event-Handler Arguments
- •Passing an Unknown Number of Arguments
- •Named Arguments
- •More Types of Function Return Values
- •Overloading Functions
- •Summary
- •The Appearance of Forms
- •Properties of the Form Control
- •Placing Controls on Forms
- •Setting the TabOrder
- •VB.NET at Work: The Contacts Project
- •Anchoring and Docking
- •Loading and Showing Forms
- •The Startup Form
- •Controlling One Form from within Another
- •Forms vs. Dialog Boxes
- •VB.NET at Work: The MultipleForms Project
- •Designing Menus
- •The Menu Editor
- •Manipulating Menus at Runtime
- •Building Dynamic Forms at Runtime
- •The Form.Controls Collection
- •VB.NET at Work: The DynamicForm Project
- •Creating Event Handlers at Runtime
- •Summary
- •The TextBox Control
- •Basic Properties
- •Text-Manipulation Properties
- •Text-Selection Properties
- •Text-Selection Methods
- •Undoing Edits
- •VB.NET at Work: The TextPad Project
- •Capturing Keystrokes
- •The ListBox, CheckedListBox, and ComboBox Controls
- •Basic Properties
- •The Items Collection
- •VB.NET at Work: The ListDemo Project
- •Searching
- •The ComboBox Control
- •The ScrollBar and TrackBar Controls
- •The ScrollBar Control
- •The TrackBar Control
- •Summary
- •The Common Dialog Controls
- •Using the Common Dialog Controls
- •The Color Dialog Box
- •The Font Dialog Box
- •The Open and Save As Dialog Boxes
- •The Print Dialog Box
- •The RichTextBox Control
- •The RTF Language
- •Methods
- •Advanced Editing Features
- •Cutting and Pasting
- •Searching in a RichTextBox Control
- •Formatting URLs
- •VB.NET at Work: The RTFPad Project
- •Summary
- •What Is a Class?
- •Building the Minimal Class
- •Adding Code to the Minimal Class
- •Property Procedures
- •Customizing Default Members
- •Custom Enumerations
- •Using the SimpleClass in Other Projects
- •Firing Events
- •Shared Properties
- •Parsing a Filename String
- •Reusing the StringTools Class
- •Encapsulation and Abstraction
- •Inheritance
- •Inheriting Existing Classes
- •Polymorphism
- •The Shape Class
- •Object Constructors and Destructors
- •Instance and Shared Methods
- •Who Can Inherit What?
- •Parent Class Keywords
- •Derived Class Keyword
- •Parent Class Member Keywords
- •Derived Class Member Keyword
- •MyBase and MyClass
- •Summary
- •On Designing Windows Controls
- •Enhancing Existing Controls
- •Building the FocusedTextBox Control
- •Building Compound Controls
- •VB.NET at Work: The ColorEdit Control
- •VB.NET at Work: The Label3D Control
- •Raising Events
- •Using the Custom Control in Other Projects
- •VB.NET at Work: The Alarm Control
- •Designing Irregularly Shaped Controls
- •Designing Owner-Drawn Menus
- •Designing Owner-Drawn ListBox Controls
- •Using ActiveX Controls
- •Summary
- •Programming Word
- •Objects That Represent Text
- •The Documents Collection and the Document Object
- •Spell-Checking Documents
- •Programming Excel
- •The Worksheets Collection and the Worksheet Object
- •The Range Object
- •Using Excel as a Math Parser
- •Programming Outlook
- •Retrieving Information
- •Recursive Scanning of the Contacts Folder
- •Summary
- •Advanced Array Topics
- •Sorting Arrays
- •Searching Arrays
- •Other Array Operations
- •Array Limitations
- •The ArrayList Collection
- •Creating an ArrayList
- •Adding and Removing Items
- •The HashTable Collection
- •VB.NET at Work: The WordFrequencies Project
- •The SortedList Class
- •The IEnumerator and IComparer Interfaces
- •Enumerating Collections
- •Custom Sorting
- •Custom Sorting of a SortedList
- •The Serialization Class
- •Serializing Individual Objects
- •Serializing a Collection
- •Deserializing Objects
- •Summary
- •Handling Strings and Characters
- •The Char Class
- •The String Class
- •The StringBuilder Class
- •VB.NET at Work: The StringReversal Project
- •VB.NET at Work: The CountWords Project
- •Handling Dates
- •The DateTime Class
- •The TimeSpan Class
- •VB.NET at Work: Timing Operations
- •Summary
- •Accessing Folders and Files
- •The Directory Class
- •The File Class
- •The DirectoryInfo Class
- •The FileInfo Class
- •The Path Class
- •VB.NET at Work: The CustomExplorer Project
- •Accessing Files
- •The FileStream Object
- •The StreamWriter Object
- •The StreamReader Object
- •Sending Data to a File
- •The BinaryWriter Object
- •The BinaryReader Object
- •VB.NET at Work: The RecordSave Project
- •The FileSystemWatcher Component
- •Properties
- •Events
- •VB.NET at Work: The FileSystemWatcher Project
- •Summary
- •Displaying Images
- •The Image Object
- •Exchanging Images through the Clipboard
- •Drawing with GDI+
- •The Basic Drawing Objects
- •Drawing Shapes
- •Drawing Methods
- •Gradients
- •Coordinate Transformations
- •Specifying Transformations
- •VB.NET at Work: Plotting Functions
- •Bitmaps
- •Specifying Colors
- •Defining Colors
- •Processing Bitmaps
- •Summary
- •The Printing Objects
- •PrintDocument
- •PrintDialog
- •PageSetupDialog
- •PrintPreviewDialog
- •PrintPreviewControl
- •Printer and Page Properties
- •Page Geometry
- •Printing Examples
- •Printing Tabular Data
- •Printing Plain Text
- •Printing Bitmaps
- •Using the PrintPreviewControl
- •Summary
- •Examining the Advanced Controls
- •How Tree Structures Work
- •The ImageList Control
- •The TreeView Control
- •Adding New Items at Design Time
- •Adding New Items at Runtime
- •Assigning Images to Nodes
- •Scanning the TreeView Control
- •The ListView Control
- •The Columns Collection
- •The ListItem Object
- •The Items Collection
- •The SubItems Collection
- •Summary
- •Types of Errors
- •Design-Time Errors
- •Runtime Errors
- •Logic Errors
- •Exceptions and Structured Exception Handling
- •Studying an Exception
- •Getting a Handle on this Exception
- •Finally (!)
- •Customizing Exception Handling
- •Throwing Your Own Exceptions
- •Debugging
- •Breakpoints
- •Stepping Through
- •The Local and Watch Windows
- •Summary
- •Basic Concepts
- •Recursion in Real Life
- •A Simple Example
- •Recursion by Mistake
- •Scanning Folders Recursively
- •Describing a Recursive Procedure
- •Translating the Description to Code
- •The Stack Mechanism
- •Stack Defined
- •Recursive Programming and the Stack
- •Passing Arguments through the Stack
- •Special Issues in Recursive Programming
- •Knowing When to Use Recursive Programming
- •Summary
- •MDI Applications: The Basics
- •Building an MDI Application
- •Built-In Capabilities of MDI Applications
- •Accessing Child Forms
- •Ending an MDI Application
- •A Scrollable PictureBox
- •Summary
- •What Is a Database?
- •Relational Databases
- •Exploring the Northwind Database
- •Exploring the Pubs Database
- •Understanding Relations
- •The Server Explorer
- •Working with Tables
- •Relationships, Indices, and Constraints
- •Structured Query Language
- •Executing SQL Statements
- •Selection Queries
- •Calculated Fields
- •SQL Joins
- •Action Queries
- •The Query Builder
- •The Query Builder Interface
- •SQL at Work: Calculating Sums
- •SQL at Work: Counting Rows
- •Limiting the Selection
- •Parameterized Queries
- •Calculated Columns
- •Specifying Left, Right, and Inner Joins
- •Stored Procedures
- •Summary
- •How About XML?
- •Creating a DataSet
- •The DataGrid Control
- •Data Binding
- •VB.NET at Work: The ViewEditCustomers Project
- •Binding Complex Controls
- •Programming the DataAdapter Object
- •The Command Objects
- •The Command and DataReader Objects
- •VB.NET at Work: The DataReader Project
- •VB.NET at Work: The StoredProcedure Project
- •Summary
- •The Structure of a DataSet
- •Navigating the Tables of a DataSet
- •Updating DataSets
- •The DataForm Wizard
- •Handling Identity Fields
- •Transactions
- •Performing Update Operations
- •Updating Tables Manually
- •Building and Using Custom DataSets
- •Summary
- •An HTML Primer
- •HTML Code Elements
- •Server-Client Interaction
- •The Structure of HTML Documents
- •URLs and Hyperlinks
- •The Basic HTML Tags
- •Inserting Graphics
- •Tables
- •Forms and Controls
- •Processing Requests on the Server
- •Building a Web Application
- •Interacting with a Web Application
- •Maintaining State
- •The Web Controls
- •The ASP.NET Objects
- •The Page Object
- •The Response Object
- •The Request Object
- •The Server Object
- •Using Cookies
- •Handling Multiple Forms in Web Applications
- •Summary
- •The Data-Bound Web Controls
- •Simple Data Binding
- •Binding to DataSets
- •Is It a Grid, or a Table?
- •Getting Orders on the Web
- •The Forms of the ProductSearch Application
- •Paging Large DataSets
- •Customizing the Appearance of the DataGrid Control
- •Programming the Select Button
- •Summary
- •How to Serve the Web
- •Building a Web Service
- •Consuming the Web Service
- •Maintaining State in Web Services
- •A Data-Driven Web Service
- •Consuming the Products Web Service in VB
- •Summary
344 Chapter 8 BUILDING CUSTOM CLASSES
Tip The members of an enumeration are not variables. They’re constants, and you can only access them through a variable of the AgeGroup enumeration.
Because the AgeGroup enumeration was declared as Public, it’s exposed to any application that uses the Minimal class. Let’s see how we can use the same enumeration in our application. Switch to the form’s code window, add a new button, and enter the statements from Listing 8.14 in its event handler.
Listing 8.14: Using the Enumeration Exposed by the Class
Protected Sub Button3_Click(ByVal sender As Object, _ ByVal e As System.EventArgs)
Dim obj As Minimal obj = New Minimal() obj.BDate = #2/9/1932#
Console.WriteLine(obj.Age) Dim discount As Single
If obj.GetAgeGroup = Minimal.AgeGroup.Baby Or _ obj.GetAgeGroup = Minimal.AgeGroup.Child Then discount = 0.4
If obj.GetAgeGroup = Minimal.AgeGroup.Senior Then discount = 0.5 If obj.GetAgeGroup = Minimal.AgeGroup.Teenager Then discount = 0.25 Console.WriteLine(discount)
End Sub
This routine calculates discounts based on the person’s age. Notice that we don’t use numeric constants in our code, just descriptive names. Moreover, the possible values of the enumeration are displayed in a drop-down list by the IntelliSense feature of the IDE as needed (Figure 8.4), and you don’t have to memorize them, or look them up, as you would with constants.
Figure 8.4
The members of an enumeration are displayed automatically in the IDE as you type.
Using the SimpleClass in Other Projects
The project you’ve built in this section is a Windows application that contains a Class module. The class is contained within the project, and it’s used by the project’s main form. What if you wanted to use this class in another project?
Copyright ©2002 SYBEX, Inc., Alameda, CA |
www.sybex.com |
BUILDING THE MINIMAL CLASS 345
First, you must change the type of the project. A Windows project can’t be used as a component in another project. Right-click the SimpleClass project and select Properties. On the project’s Properties dialog box, locate the Output drop-down list and change the project’s type from
Windows Application to Class Library, as shown in Figure 8.5. Then close the dialog box. When you return to the project, right-click the TestForm and select Exclude From Project. A class doesn’t have a visible interface, and there’s no reason to include the test form in your project.
Figure 8.5
Setting a project’s properties through the Property Pages dialog box
Now open the Build menu and select Configuration Manager. The current configuration is Debug. Change it to Release, as shown in Figure 8.6. The Debug configuration should be used in testing and debugging the project. When you’re ready to distribute the application (be it a Windows application, a class library, or a control library), you must change the current configuration to Release. When you compile a project in Debug configuration, the compiler inserts additional information in the executable to ease the debugging process.
Figure 8.6
Changing the configuration of a project
From the main menu, select Build Build SimpleClass. This command will compile the SimpleClass project (which is a class) and will create a DLL file. This is the file that contains the class’s code and is the file you must use in any project that needs the functionality of the SimpleClass class. The DLL file will be created in the \Obj\Release folder under the project’s folder.
Copyright ©2002 SYBEX, Inc., Alameda, CA |
www.sybex.com |
346 Chapter 8 BUILDING CUSTOM CLASSES
Let’s use the SimpleClass.dll file in another project. Start a new Windows application, open the Project menu, and add a reference to the SimpleClass. Select Project Add Reference and, in the dialog box that appears, switch to the Projects tab. Click the Browse button and locate the Class.dll file. Select the name of the file and click OK to close the dialog box.
The SimpleClass component will be added to the project. You can now declare a variable of the SimpleClass type and call its properties and methods:
Dim obj As New SimpleClass obj.Age = 45 obj.property2 = 5544 MsgBox(obj.Negate())
If you want to keep testing the SimpleClass project, add the TestForm to the project (right-click the project’s name, select Add Add Existing Item, and select the TestForm in the project’s folder). Then change the project’s type back to Windows application, and finally change its configuration from Release to Debug.
Firing Events
Methods and properties are easy to implement, and you have seen how to implement them. Classes can also fire events. It’s possible to raise events from within your classes, although not quite as common. Controls have many events, because they expose a visible interface and the user interacts through this interface (clicks, drags and drops, and so on). But classes can also raise events. Class events can come from three different sources:
The class itself A class may raise an event to indicate the progress of a lengthy process, or that an internal variable or property has changed value. The PercentDone event is a typical example. A process that takes a while to complete reports its progress to the calling application with this event, which is fired periodically. These are called progress events, and they’re the most common type of class events.
Time events These events are based on a timer. They’re not very common, but you can implement alarms, job schedulers, and similar applications. You can set an alarm for a specific time or an alarm that will go off after a specified interval.
External events External events, like the completion of an asynchronous operation, can also fire events. A class may initiate a file download and notify the application when the file arrives.
Notice that the class can’t intercept events initiated by the user under any circumstances, because it doesn’t have a visible user interface.
Time Events
Let’s look at an example of a simple event, one that’s raised at a specific time. We’ll implement an event in our Minimal class that fires at five o’clock in the afternoon—that is, if an application is using the class at the time. Classes can’t be instantiated at specific times. Even if they could, the events would go unnoticed. Classes must be instantiated by an application. In addition, the application must be executing when the event is fired, so that it can process it. If the application doesn’t
Copyright ©2002 SYBEX, Inc., Alameda, CA |
www.sybex.com |
BUILDING THE MINIMAL CLASS 347
provide a handler for the event, the event will go unnoticed—just like the DragEnter and Enter events of most controls, which are not handled in a typical Windows application.
The first problem we face is that the class’s code isn’t running constantly to check the time periodically. It executes when a member of the class is called and then returns control to your application. To make your class check the time periodically, you must embed a Timer control in your class. But the class doesn’t have a visible interface, so you can’t place a Timer control on it. The solution is to instantiate a Timer control from within the class’s code and program its Elapsed event so that it fires every so often. In our example, we’ll implement an event that’s fired every day at five o’clock. This is a reminder for the end of a shift, so it need not be extremely precise. We’ll set the Timer control to fire a Tick event every 10 seconds. If this were a real-time application, you’d have to fire Tick events more often. The following line creates an instance of the Timer control:
Dim WithEvents tmr As System.Timers.Timer
This declaration must appear outside any procedure. The WithEvents keyword is crucial here. Controls and classes that raise events must be declared with the WithEvents keyword, or else the application won’t see the events. Controls will fire them, but the class won’t be watching out for events. While methods and properties are an integral part of the class and you don’t have to request that these members be exposed, the events are not exposed by default. Moreover, the statements that declare object variables with the WithEvents keyword must appear outside any procedure.
The Timer control is disabled by default, and we must enable it. A good place to insert the Timer’s initialization code is the class’s New() procedure, which is called when the class is instantiated:
Public Sub New()
tmr = New WinForms.Timer() tmr.Interval = 10000 tmr.Enabled = True
End Sub
Our timer is ready to go and will fire an event every 10,000 milliseconds, or 10 seconds. The shorter the interval, the more time spent in processing the Timer’s Elapsed event.
The Timer’s Elapsed event will be fired every 10 seconds, and you must now program this event. What do we want to do in this event? Check the time and, if it’s five o’clock, raise the TeaTime event. Before a class can raise an event, you must declare it with a statement like the following:
Public Event TeaTime()
This declaration must also appear outside any procedure in your class’s code. Now you can program the Elapsed event handler (Listing 8.15) and raise the event when appropriate. Because we can’t be sure that the Timer will fire its event at five o’clock precisely, we check the time and, if it’s after 1700 hours and no later than 120 seconds after that time, we fire the event.
Listing 8.15: The Timer’s Tick Event Handler
Private Sub tmr_Elapsed(ByVal sender As Object, _
ByVal e As System.Timers.ElapsedEventArgs) Handles tmr.Elapsed ‘Console.WriteLine(DateDiff(DateInterval.Second, Now(), _
DateAdd(DateInterval.Hour, 17, System.DateTime.Today)))
Copyright ©2002 SYBEX, Inc., Alameda, CA |
www.sybex.com |
348 Chapter 8 BUILDING CUSTOM CLASSES
If DateDiff(DateInterval.Second, Now(), DateAdd( _ DateInterval.Hour, 17, System.DateTime.Today)) < 120 Then
tmr.Enabled = False RaiseEvent TeaTime(Me)
End If End Sub
Notice that once the event is raised, we disable the timer, or else the same event would fire again and again (Figure 8.7). The long statement I’ve commented out displays the number of seconds from the moment it’s executed to five o’clock. Use this value to adjust the second statement, and make the class fire the event at any time.
Figure 8.7
Declaring and raising an event in the class’s code
The code uses the DateDiff() function, which calculates the difference between the current time and 1700 hours in seconds. If this difference is less than two minutes, the class raises the TeaTime event. The syntax of the DateDiff() function is complicated, but here’s an explanation of its arguments. The first argument is a constant value that tells the DateDiff() function what time unit to use in reporting the difference. In our example, we want to express the difference in seconds. The following two arguments are the two date (or time) values to be subtracted. The first of the two arguments is the current date and time: the second is a date/time value that represents the current date at five o’clock. This value is constructed with the following expression:
DateAdd(DateInterval.Hour, 17, System.DateTime.Today)
This statement returns the current date with a time value of 17:00.00 (something like 2001-08-13 17:00:00). This value is then compared to the current date and time.
Programming the Class’s Event
How do we intercept the event in our main application? As you may have guessed, we’ll instantiate the class with the WithEvents keyword. Declare a second variable of the Minimal type in the test form with the WithEvents keyword:
Dim WithEvents TimerObj As Minimal
Copyright ©2002 SYBEX, Inc., Alameda, CA |
www.sybex.com |
BUILDING THE MINIMAL CLASS 349
After this declaration, the TimerObj variable will appear in the list of objects of the editor window, and its TeaTime event will appear in the list of events, as you see in Figure 8.8. You can now program the event in your application and use it any way you see fit.
Figure 8.8
Programming a class’s event
The events raised by a class may pass additional arguments to the program that handles the event. The sender argument is passed by default and contains information about the object that raised the event—so that you can write an event handler for multiple events. Place a new button on the form, name it Initialize Timer, and enter the following code in its Click event handler (this is the code behind the button of that name on the test form):
Private Sub bttnInitTimer_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles bttnInitTimer.Click TimerObj = New Minimal()
End Sub
This subroutine creates a new instance of the TimerObj variable. This variable was declared outside the procedure, but it wasn’t instantiated. Before this statement is executed, no events will take place. I’ve inserted the statement that prints the time difference (seconds left until five o’clock) in the Timer’s Tick event so that you can see what’s going on.
Let’s see how the application uses the class. Start the application and wait for 10 seconds. You might expect to see something in the Output window, but nothing will appear. The Console.WriteLine statement in the Timer control’s Tick event handler isn’t executed, because the TimerObj variable hasn’t been instantiated yet.
Click one of the buttons on the form other than the Initialize Timer button. Every 10 seconds, a new double value will appear in the Output window. This is the number of seconds left until (or passed since) five o’clock. The event, however, isn’t raised. An instance (or more) of the Minimal class has been created, so the class’s code is executing, and it prints the number of seconds left until the next TeaTime event in the Output window. However, the TimerObj variable (the one declared with the WithEvents keyword) has not been instantiated yet, so even if the class fires the event, your application won’t handle it. Since none of the variables of the Minimal type was declared with the WithEvents keyword, the application isn’t receiving notifications about the event—should it happen. The class’s code, however, is running, and it prints a value every 10 seconds.
Now click the Initialize Timer button, wait for a few seconds, and the message will pop up— provided you’re testing the application around five o’clock. Only the TimerObj variable was declared with the WithEvents keyword, and this is the only one that can intercept the event.
Copyright ©2002 SYBEX, Inc., Alameda, CA |
www.sybex.com |
350 Chapter 8 BUILDING CUSTOM CLASSES
Before I end this example, I should show you how to test the application without having to wait forever. I ran the application at 1:57 P.M., and the value printed by the first statement in the Tick events was 10,900 or so (the numbe r of seconds to five o’clock). Then I stopped the application, changed the value 120 in the code to a value a little smaller than the one in the Output window (10,850), and restarted the application. A few moments later, the event was fired (it took more than 10 seconds, so I was sure the code was working properly). If you’re running the application after five o’clock, the values will be negative, so adjust the comparison accordingly.
You have the basics for writing classes to fire alarms at specified intervals, a specific time, or even a time interval after a certain operation. You can also use this class (with some modifications) to monitor the system and raise an event if certain things happen. You could check a folder periodically to see if a file was added or deleted. There’s no reason to write code for this, because the Framework exposes the FileSystemWatcher class, which does exactly that (the FileSystemWatcher class is discussed in Chapter 13). But you may wish to monitor a printer, know when a new user logs in, or keep track of any other operation or action you can detect from within your code.
Passing Parameters through Event Arguments
Events usually pass some information to the routine that processes them. The TeaTime event is a trivial example that doesn’t include any information, but in most cases there will be some information you’ll want to pass to the application. The arguments of an event are declared just like the arguments of a procedure. The following statement declares an event that’s fired when the class completes the download of a file. The event passes three parameter values to the application that intercepts it:
Public Event CompletedDownload(ByVal fileURL As String, _
ByVal fileName As String, ByVal fileLength As Long)
The parameters passed to the application through this event are the URL from which the file was downloaded, the path of a file where the downloaded information was stored, and the length of the file. To raise this event from within a class’s code, call the RaiseEvent statement as before, passing three values of the appropriate type, as shown next:
RaiseEvent CompletedDownload(“http://www.server.com/file.txt”, _
“d:\temp\A90100.txt”, 144329)
In the following section, you’ll find actual examples of events that pass arguments to the application that uses the class. You will also see how you can cancel an asynchronous operation from within your application by setting one of the arguments of the event.
Progress Events
Progress events can be fired in two ways. If you know the duration of the operation, you can fire progress events when every five or ten percent of the operation completes. If the operation takes place from within a loop, you can fire the progress event after a certain number of iterations. A more complicated case is when you don’t know in advance how long the operation may take. This may happen when you download a file of unknown size. In this case, you can fire progress events every so many seconds and report the number of bytes downloaded, or some other indication that might help the application display some form of progress. Reporting the progress as a percentage of the total work
Copyright ©2002 SYBEX, Inc., Alameda, CA |
www.sybex.com |
BUILDING THE MINIMAL CLASS 351
done is out of the question, so this progress event isn’t of much help to the developer or to the user of the application. It simply tells the user that the class is running in the background, not much more.
To demonstrate progress events, I’ve prepared an application (DirScanner on the companion CD) that goes through an entire drive and adds up the sizes of all files. This is a lengthy operation even on the fastest Pentium system, so it will give you a chance to monitor the progress events. Since the class has no way of knowing the total number of folders (or files) on the disk ahead of time, it can only report the number of folders scanned so far.
The following code scans recursively all folders on the hard drive. The process of scanning a folder, including its subfolders, is quite simple—if you’re familiar with recursion, that is. The code is discussed in detail in Chapter 18, but don’t worry about understanding how it works right now. Just focus on the ScanProgress event, which is fired each time a new folder has been scanned. The application that receives the event can update a display with the total number of folders scanned so far, which is a good indication of the progress of the operation. It’s not an indicator of the time left to complete the operation, but sometimes this is all you can do. When you search for a specific file with the Find utility, for example, all you get is a list of folders scanned so far at the bottom of the window.
The DirScanner class (shown in Listing 8.16) calls the ScanFolder() function, which accepts as argument the name of the folder to be scanned and returns the total size of all the files in this folder. ScanFolder() scans the files in the folder passed as argument. Then it goes through the subfolders under the same folder and does the same. To do so, it calls itself by passing each subfolder’s name as argument. By the time it’s done, the totalSize local variable holds the sum of the sizes of all files under the initial folder.
Listing 8.16: The DirScanner Class
Imports System.IO
Imports System.IO.Directory Public Class DirScanner
Public Event ScanProgress(ByVal foldersScanned As Integer) Private totalSize As Long
Private nFolders As Integer
Public Function ScanFolder(ByVal folder As String) As Long Dim file, dir As String
Dim FI As FileInfo
For Each file In Directory.GetFiles(folder) FI = New FileInfo(file)
totalSize = totalSize + FI.Length Next
For Each dir In Directory.GetDirectories(folder) nFolders = nFolders + 1
RaiseEvent ScanProgress(nFolders) ScanFolder(dir)
Next
Return totalSize End Function
End Class
Copyright ©2002 SYBEX, Inc., Alameda, CA |
www.sybex.com |
352 Chapter 8 BUILDING CUSTOM CLASSES
The ScanProgress event is declared at the Class level (outside the ScanFolder() procedure), and it passes an Integer value to the calling application; this is the number of folders scanned so far. The ScanFolder() function maintains two private variables, where it stores the number of folders scanned so far (the nFolders variable) and the total size of the files. The nFolders value is reported to the application through the event’s argument. The code of the ScanFolder() function is straightforward, except for the line that raises the event. The event is raised every time the function runs into a new folder and before it starts scanning it.
To test the DirScanner class and its event, add a button on the test form and enter the code of Listing 8.17 in its Click event handler.
Listing 8.17: Testing the DirScanner Class
Dim WithEvents objDirScanner As DirScanner
Protected Sub Button1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs)
Dim folder As String objDirScanner = New DirScanner() folder = “C:\Program Files” MsgBox(“Your files occupy “ & _
objDirScanner.ScanFolder(folder).Tostring & “ bytes on the drive”)
End Sub
The application calls the ScanFolder() method and, while the method is executing, it receives progress events. The last statement in this subroutine will be executed after the ScanFolder() method completes its execution and returns control to the Click event handler. In the meantime, the events raised by the class are processed by the objDirScanner_ScanProgress handler, which is shown in the following code. To program this event handler, select the name of the objDirScanner variable in the object list and the ScanProgress event in the event list of the editor’s window. The code shown here uses the information passed through these events to update the caption on the application’s form.
Public Sub objDirScanner_ScanProgress(ByVal foldersScanned As System.Integer) _ Handles objDirScanner.ScanProgress
Me.Text = “Scanned “ & foldersScanned.ToString & “ folders so far” End Sub
Another method of reporting the progress of a lengthy operation is to raise an event every so often during the operation. The following pseudocode segment outlines the class’s code that raises an event every eventDuration seconds:
If Now.TimeOfDay < (newInterval + eventDuration) Then newInterval = Now.TimeOfDay
RaiseEvent Progress(foldersScanned) End If
Copyright ©2002 SYBEX, Inc., Alameda, CA |
www.sybex.com |
BUILDING THE MINIMAL CLASS 353
When an application initiates an operation that may take a while to complete, it usually provides a Cancel button, which the user can click to interrupt the process. But how do we notify the class that it must abort the current operation? This is done through the progress event, with the help of an additional argument. Many event handlers include a Cancel argument, which the application can set to True to indicate its intention to interrupt the execution of the current operation in the class. Let’s revise our progress event in the DirScanner class to include a Cancel argument.
Tip Notice that the Cancel argument doesn’t pass information from the class to the application; it passes information the other way. We want the class to be able to read the value of the Cancel argument set by the application, so we must pass this argument by reference, not by value. If you pass the Cancel argument by value, its value won’t change. Even if the application sets it to True, it’s actually setting the value of the copy of the Cancel variable, and your class will never see this value.
Instead of revising the code of the existing ScanProgress event, we’ll add another event, the ScanTimerProgress event. Add the event declaration and the ScanTimerFolder() function from Listing 8.18 to your class.
Listing 8.18: The ScanTimerFolder Method
Public Function ScanTimerFolder(ByVal folder As String) As Long Dim file, dir As String
Dim FI As FileInfo
Dim interval As Double = 3000
If start = 0 Then start = Now.TimeOfDay.TotalMilliseconds Dim cancel As Boolean
For Each file In Directory.GetFiles(folder) FI = New FileInfo(file)
totalSize = totalSize + FI.Length Next
For Each dir In Directory.GetDirectories(folder)
If Now.TimeOfDay.TotalMilliseconds > (start + interval) Then RaiseEvent ScanTimerProgress(nFolders, cancel)
If cancel Then Exit Function
start = Now.TimeOfDay.TotalMilliseconds End If
nFolders = nFolders + 1 ScanTimerFolder(dir)
Next
Return totalSize End Function
The code is the same, except for the If statement that examines the value of the cancel argument. If cancel is True, then the program aborts its execution. To test the new progress event, add a second button on the form and enter the code of Listing 8.19 in its Click event handler.
Copyright ©2002 SYBEX, Inc., Alameda, CA |
www.sybex.com |