Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
(ebook) Visual Studio .NET Mastering Visual Basic.pdf
Скачиваний:
120
Добавлен:
17.08.2013
Размер:
15.38 Mб
Скачать

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

Simple-

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