Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 109 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
109
Dung lượng
1,27 MB
Nội dung
CHAPTER 9 ■ WORKING WITH INTERFACES AND COLLECTIONS 279 ■Source Code The CollectionTypes project can be found under the Chapter 9 subdirectory. System.Collections.Specialized Namespace In addition to the types defined within the System.Collections namespace, you should also be aware that the .NET base class libraries provide the System.Collections.Specialized namespace, which defines another set of types that are more (pardon the redundancy) specialized. For example, the StringDictionary and ListDictionary types each provide a stylized implementation of the IDictionary interface. Table 9-5 documents the key class types. Table 9-5. Types of the System.Collections.Specialized Namespace Member of System.Collections.Specialized Meaning in Life CollectionsUtil Creates collections that ignore the case in strings. HybridDictionary Implements IDictionary by using a ListDictionary while the collection is small, and then switching to a Hashtable when the collection gets large. ListDictionary Implements IDictionary using a singly linked list. Recommended for collections that typically contain ten items or fewer. NameValueCollection Represents a sorted collection of associated String keys and String values that can be accessed either with the key or with the index. StringCollection Represents a collection of strings. StringDictionary Implements a hashtable with the key strongly typed to be a string rather than an object. StringEnumerator Supports a simple iteration over a StringCollection. Summary An interface can be defined as a named collection of abstract members. Because an interface does not provide any implementation details, it is common to regard an interface as a behavior that may be supported by a given type. When two or more classes implement the same interface, you are able to treat each type the same way (via interface-based polymorphism) even if the types are defined within unique class hierarchies. VB 2005 provides the Interface keyword to allow you to define a new interface. As you have seen, a type can support as many interfaces as necessary using the Implements keyword. Furthermore, it is permissible to build interfaces that derive from multiple base interfaces. In addition to building your custom interfaces, the .NET libraries define a number of framework- supplied interfaces. As you have seen, you are free to build custom types that implement these predefined interfaces to gain a number of desirable traits such as cloning, sorting, and enumerat- ing. Finally, you spent some time investigating the stock collection classes defined within the System.Collections namespace and examining a number of common interfaces used by the collection- centric types. 5785ch09.qxd 3/31/06 10:50 AM Page 279 5785ch09.qxd 3/31/06 10:50 AM Page 280 CHAPTER 10 ■ ■ ■ Callback Interfaces, Delegates, and Events Up to this point in the text, every application you have developed added various bits of code to Main(), which, in some way or another, sent requests to a given object by invoking its members. However, you have not yet examined how an object can talk back to the entity that created it. In most programs, it is quite common for objects to engage in a two-way conversation through the use of callback interfaces, events, and other programming constructs. Although we most often think of events in the context of a GUI environment (for example, handling the Click event of a button or detecting mouse movement), the truth of the matter is events can be used to allow any two objects in memory to communicate (visible or not). This chapter opens by examining how interface types may be used to enable callback functionality. Although the .NET event architecture is not directly tied to interface-based programming techniques, callback interfaces can be quite useful given that they are language and architecture neutral. Next, you learn about the .NET delegate type, which is a type-safe object that “points to” other method(s) that can be invoked at a later time. As you will see, .NET delegates are quite sophisticated, in that they have built-in support for multicasting and asynchronous (e.g., nonblocking) invocations. Once you learn how to create and manipulate delegate types, you then investigate a set of VB 2005 keywords (Event, Handles, RaiseEvent, etc.) that simplify the process of working with delegate types in the raw. Finally, this chapter examines a new language feature provided by Visual Basic 2005, specifically the ability to build “custom events” in order to intercept the process of registering with, detaching from, and sending an event notification. ■Note You will be happy to know that the event-centric techniques shown in this chapter are found all throughout the .NET platform. In fact, when you are handling Windows Forms or ASP.NET events, you will be using the exact same syntax. Using Interfaces As a Callback Mechanism As you have seen in the previous chapter, interfaces can be used to define a behavior that may be supported by various types in your system. Beyond using interfaces to establish polymorphism across hierarchies, interfaces may also be used as a callback mechanism. This technique enables objects to engage in a two-way conversation using an agreed upon set of members. To illustrate the use of callback interfaces (also termed event interfaces), let’s retrofit the now familiar Car type (first defined in Chapter 6) in such a way that it is able to inform the caller when the engine is about to explode (when the current speed is 10 miles below the maximum speed) and has exploded (when the current speed is at or above the maximum speed). The ability to send and receive these events will be facilitated with a custom interface named IEngineStatus: 281 5785ch10.qxd 3/31/06 10:51 AM Page 281 CHAPTER 10 ■ CALLBACK INTERFACES, DELEGATES, AND EVENTS282 ' The callback interface. Public Interface IEngineStatus Sub AboutToBlow(msg As String) Sub Exploded(msg As String) End Interface In order to keep an application’s code base as flexible and reusable as possible, callback interfaces are not typically implemented directly by the object interested in receiving the events, but rather by a helper object called a sink object. Assume we have created a class named CarEventSink that imple- ments IEngineStatus by printing the incoming messages to the console. As well, our sink will also maintain a string used as a textual identifier. As you will see, it is possible to register multiple sink objects for a given event source; therefore, it will prove helpful to identify a sink by name. This being said, consider the following implementation: ' Car event sink. Public Class CarEventSink Implements IEngineStatus Private name As String Public Sub New(ByVal sinkName As String) name = sinkName End Sub Public Sub AboutToBlow(ByVal msg As String) _ Implements IEngineStatus.AboutToBlow Console.WriteLine("{0} reporting: {1}", name, msg) End Sub Public Sub Exploded(ByVal msg As String) _ Implements IEngineStatus.Exploded Console.WriteLine("{0} reporting: {1}", name, msg) End Sub End Class Now that you have a sink object that implements the event interface, your next task is to pass a reference to this sink into the Car type. The Car holds onto this object and makes calls back on the sink when appropriate. In order to allow the Car to receive the caller-supplied sink reference, we will need to add a public helper member to the Car type that we will call Connect(). Likewise, to allow the caller to detach from the event source, we will define another helper method on the Car type named Disconnect(). Finally, to enable the caller to register multiple sink objects (for the purposes of multicasting), the Car now maintains an ArrayList to represent each outstanding connection. Here are the relevant updates to the Car type: ' This iteration of the Car type maintains a list of ' objects implementing the IEngineStatus interface. Public Class Car ' The set of connected clients. Private clientSinks As New ArrayList() ' The client calls these methods to connect ' to, or detatch from, the event notification. Public Sub Connect(ByVal sink As IEngineStatus) clientSinks.Add(sink) End Sub Public Sub Disconnect(ByVal sink As IEngineStatus) clientSinks.Remove(sink) End Sub End Class 5785ch10.qxd 3/31/06 10:51 AM Page 282 CHAPTER 10 ■ CALLBACK INTERFACES, DELEGATES, AND EVENTS 283 To actually send the events, let’s update the Car.Accelerate() method to iterate over the list of sinks maintained by the ArrayList and send the correct notification when appropriate. Here is the updated member in question: ' The Accelerate method now fires event notifications to the caller, ' rather than throwing a custom exception. Public Sub Accelerate(ByVal delta As Integer) ' If the car is doomed, sent out event to ' each connected client. If carIsDead Then For Each i As IEngineStatus In clientSinks i.Exploded("Sorry! This car is toast!") Next Else currSpeed += delta ' Send out 'about to blow' event? If (maxSpeed - currSpeed) = 10 Then For Each i As IEngineStatus In clientSinks i.AboutToBlow("Careful! About to blow!") Next End If ' Is the car doomed? If currSpeed >= maxSpeed Then carIsDead = True Else ' We are OK, just print out speed. Console.WriteLine("=> CurrSpeed = {0}", currSpeed) End If End If End Sub To complete the example, here is a Main() method making use of a callback interface to listen to the Car events: ' Make a car and listen to the events. Module Program Sub Main() Console.WriteLine("***** Interfaces as event enablers *****") Dim myCar As New Car("SlugBug", 10) ' Make sink object. Dim sink As New CarEventSink("MySink") ' Register the sink with the Car. myCar.Connect(sink) ' Speed up (this will trigger the event notifications). For i As Integer = 0 To 5 myCar.Accelerate(20) Next ' Detach from event source. myCar.Disconnect(sink) Console.ReadLine() End Sub End Module 5785ch10.qxd 3/31/06 10:51 AM Page 283 CHAPTER 10 ■ CALLBACK INTERFACES, DELEGATES, AND EVENTS284 Figure 10-1. Interfaces as event protocols Figure 10-1 shows the end result of this interface-based event protocol. Notice that we call Disconnect() before exiting Main(), although this is not actually necessary for the example to function as intended. However, the Disconnect() method can be very helpful in that it allows the caller to selectively detach from an event source at will. Assume that the application now wishes to register two sink objects, dynamically remove a particular sink during the flow of exe- cution, and continue processing the program at large: Module Program Sub Main() Console.WriteLine("***** Interfaces as event enablers *****") Dim myCar As New Car("SlugBug", 10) ' Make sink object. Console.WriteLine("***** Creating Sinks! *****") Dim sink As New CarEventSink("First Sink") Dim otherSink As New CarEventSink("Second Sink") ' Pass both sinks to car. myCar.Connect(sink) myCar.Connect(otherSink) ' Speed up (this will trigger the events). For i As Integer = 0 To 5 myCar.Accelerate(20) Next ' Detach from first sink. myCar.Disconnect(sink) ' Speed up again (only otherSink will be called). For i As Integer = 0 To 5 myCar.Accelerate(20) Next ' Detach from other sink. myCar.Disconnect(otherSink) Console.ReadLine() End Sub End Module Figure 10-2 shows the update. 5785ch10.qxd 3/31/06 10:51 AM Page 284 CHAPTER 10 ■ CALLBACK INTERFACES, DELEGATES, AND EVENTS 285 Figure 10-2. Working with multiple sinks So! Hopefully you agree that event interfaces can be helpful in that they can be used under any language (VB 6.0, VB 2005, C++, etc.) or platform (COM, .NET, or J2EE) that supports interface-based programming. However, as you may be suspecting, the .NET platform defines an “official” event protocol that is not dependent on the construction of interfaces. To understand .NET’s intrinsic event architecture, we begin by examining the role of the delegate type. ■Source Code The EventInterface project is located under the Chapter 10 subdirectory. Understanding the .NET Delegate Type Before formally defining .NET delegates, let’s gain a bit of historical perspective regarding the Windows platform. Since its inception many years ago, the Win32 API made use of C-style function pointers to support callback functionality. Using these function pointers, programmers were able to configure one function in the program to invoke another function in the application. As you would imagine, this approach allowed applications to handle events from various UI elements, intercept messages in a distributed system, and numerous other techniques. Although BASIC-style languages have histor- ically avoided the complexity of working with function pointers (thankfully), the callback construct is burned deep into the fabric of the Windows API. One of the problems found with C-style callback functions is that they represent little more than a raw address in memory, which offers little by way of type safety or object orientation. Ideally, callback functions could be configured to include additional type-safe information such as the number of (and types of) parameters and the return value (if any) of the method being “pointed to.” Alas, this is not the case in traditional callback functions, and, as you may suspect, can therefore be a frequent source of bugs, hard crashes, and other runtime disasters. Nevertheless, callbacks are useful entities in that they can be used to build event architectures. In the .NET Framework, callbacks are still possible, and their functionality is accomplished in a much safer and more object-oriented manner using delegates. In essence, a delegate is a type-safe object that points to another method (or possibly multiple methods) in the application, which can be invoked at a later time. Specifically speaking, a delegate type maintains three important pieces of information: • The address of the method on which it will make calls • The arguments (if any) required by this method • The return value (if any) returned from this method 5785ch10.qxd 3/31/06 10:51 AM Page 285 Figure 10-3. The BinaryOp delegate under the hood Once a delegate has been defined and provided the necessary information, you may dynamically invoke the method(s) it points to at runtime. As you will see, every delegate in the .NET Framework (including your custom delegates) is automatically endowed with the ability to call their methods synchronously (using the calling thread) or asynchronously (on a secondary thread in a nonblocking manner). This fact greatly simplifies programming tasks, given that we can call a method on a sec- ondary thread of execution without manually creating and managing a Thread object. This chapter will focus on the synchronous aspect of the delegate type. We will examine the asynchronous behavior of delegate types during our investigation of the System.Threading namespace in Chapter 16. Defining a Delegate in VB 2005 When you want to create a delegate in VB 2005, you make use of the Delegate keyword. The name of your delegate can be whatever you desire. However, you must define the delegate to match the signature of the method it will point to. For example, assume you wish to build a delegate named BinaryOp that can point to any method that returns an Integer and takes two Integers as input parameters: ' This delegate can point to any method, ' taking two Integers and returning an ' Integer. Public Delegate Function BinaryOp(ByVal x as Integer, _ ByVal y as Integer) As Integer When the VB 2005 compiler processes a delegate type, it automatically generates a sealed class deriving from System.MulticastDelegate. This class (in conjunction with its base class, System. Delegate) provides the necessary infrastructure for the delegate to hold onto the list of methods to be invoked at a later time. For example, if you examine the BinaryOp delegate using ildasm.exe, you would find the autogenerated class type depicted in Figure 10-3. CHAPTER 10 ■ CALLBACK INTERFACES, DELEGATES, AND EVENTS286 As you can see, the generated BinaryOp class defines three public methods. Invoke() is perhaps the core method, as it is used to invoke each method maintained by the delegate type in a synchronous manner, meaning the caller must wait for the call to complete before continuing on its way. Strangely enough, the synchronous Invoke() method is typically not directly called in code. As you will see in just a bit, Invoke() is called behind the scenes when you make use of the appropriate VB 2005 syntax. 5785ch10.qxd 3/31/06 10:51 AM Page 286 CHAPTER 10 ■ CALLBACK INTERFACES, DELEGATES, AND EVENTS 287 BeginInvoke() and EndInvoke() provide the ability to call the method pointed to by the delegate asynchronously on a second thread of execution. If you have a background in multithreading, you are aware that one of the most common reasons developers create secondary threads of execution is to invoke methods that require a good deal of time to complete. Although the .NET base class libraries provide an entire namespace devoted to multithreaded programming (System.Threading), delegates provide this functionality out of the box. Investigating the Autogenerated Class Type So, how exactly does the compiler know how to define the Invoke(), BeginInvoke(), and EndInvoke() methods? To understand the process, here is the crux of the generated BinaryOp class type, shown in dazzling pseudo-code: ' This is only pseudo-code! NotInheritable Class BinaryOp Inherits System.MulticastDelegate ' Compiler generated constructor. Public Sub New(ByVal target As Object, ByVal functionAddress As System.UInt32) End Sub ' Used for synchronous calls. Public Sub Invoke(ByVal x As Integer, ByVal y As Integer) End Sub ' Used for asynchronous calls on a second thread. Public Function BeginInvoke(ByVal x As Integer, ByVal y As Integer, _ ByVal cb As AsyncCallback, ByVal state As Object) As IAsyncResult End Function Public Function EndInvoke(ByVal result As IAsyncResult) As Integer End Function End Class First, notice that the parameters and return value defined for the Invoke() method exactly match the definition of the BinaryOp delegate. The initial parameters to BeginInvoke() members (two Integers in our case) are also based on the BinaryOp delegate; however, BeginInvoke() will always provide two final parameters (of type AsyncCallback and Object) that are used to facilitate asynchronous method invocations. Finally, the return value of EndInvoke() is identical to the original delegate declaration and will always take as a sole parameter an object implementing the IAsyncResult interface. Let’s see another example. Assume you have defined a delegate that can point to any method returning a String and receiving three Boolean input parameters: Public Delegate Function MyDelegate(ByVal a As Boolean, ByVal b As Boolean, _ ByVal c As Boolean) As String This time, the autogenerated class breaks down as follows: NotInheritable Class MyDelegate Inherits System.MulticastDelegate Public Sub New(ByVal target As Object, ByVal functionAddress As System.UInt32) End Sub Public Function Invoke(ByVal a As Boolean, ByVal b As Boolean, _ ByVal c As Boolean) As String End Function Public Function BeginInvoke(ByVal a As Boolean, ByVal b As Boolean, _ ByVal c As Boolean, ByVal cb As AsyncCallback, _ ByVal state As Object) As IAsyncResult End Function 5785ch10.qxd 3/31/06 10:51 AM Page 287 CHAPTER 10 ■ CALLBACK INTERFACES, DELEGATES, AND EVENTS288 Public Function EndInvoke(ByVal result As IAsyncResult) As String End Function End Class Delegates can also “point to” methods that contain any number of ByRef parameters. For example, assume the following delegate type definition: Public Delegate Function MyOtherDelegate(ByRef a As Boolean, _ ByRef b As Boolean, ByVal c As Integer) As String The signatures of the Invoke() and BeginInvoke() methods look as you would expect; however, check out the EndInvoke() method, which now includes the set of all ByRef arguments defined by the delegate type: NotInheritable Class MyOtherDelegate Inherits System.MulticastDelegate Public Sub New(ByVal target As Object, ByVal functionAddress As System.UInt32) End Sub Public Function Invoke(ByRef a As Boolean, ByRef b As Boolean, _ ByVal c As Integer) As String End Function Public Function BeginInvoke(ByRef a As Boolean, ByRef b As Boolean, _ ByVal c As Integer, ByVal cb As AsyncCallback, _ ByVal state As Object) As IAsyncResult End Function Public Function EndInvoke(ByRef a As Boolean, ByRef b As Boolean, _ ByVal result As IAsyncResult) As String End Function End Class To summarize the story thus far, a VB 2005 delegate definition results in a compiler-generated sealed class containing three methods (as well as an internally called constructor) whose parameter and return types are based on the delegate’s declaration. Again, the good news is that the VB 2005 compiler is the entity in charge of defining the actual delegate definition on our behalf. The System.MulticastDelegate and System.Delegate Base Classes So, when you build a type using the VB 2005 Delegate keyword, you indirectly declare a class type that derives from System.MulticastDelegate. This class provides descendents with access to a list that contains the addresses of the methods maintained by the delegate type, as well as several addi- tional methods to interact with the invocation list. MulticastDelegate obtains additional functionality from its parent class, System.Delegate. Now, do understand that you will never directly derive from these base classes (in fact it is a com- plier error to do so). However, all delegate types inherit the members documented in Table 10-1 (consult the .NET Framework 2.0 documentation for full details). 5785ch10.qxd 3/31/06 10:51 AM Page 288 [...]... for consistency with the C and C++ programming languages In VB 2005, there is no separate preprocessing step Rather, preprocessor directives are processed as part of the lexical analysis phase of the compiler In any case, the syntax of the VB 2005 preprocessor directives is very similar to that of the other members of the C family, in that the directives are always prefixed with the pound sign (#) Table... the language, and be in a perfect position to understand the topic of generics in Chapter 12 The VB 2005 Preprocessor Directives VB 2005, like many other C-based programming languages, supports the use of various tokens that allow you to interact with the compilation process Before examining various VB 2005 preprocessor directives, let’s get our terminology correct The term VB 2005 preprocessor directive”... make use of the AddHandler and RemoveHandler statements, given that VB 2005 supports the WithEvents syntax Again, understand that this approach is very powerful, given that you have the ability to detach from an event source at will When you make use of the WithEvent keyword, you will continuously receive events from the source object until the object dies (which typically means until the client application... under the hood to work with NET delegates, they look and feel much the same as the legacy event-centric keywords of VB 6.0 As you have seen, VB NET now supports the Handles statement, which is used to syntactically associate an event to a given method (as well as enable multicasting) You also examined the process of hooking into (and detaching from) an event dynamically using the AddHandler and RemoveHandler... constants Cool! Now that you understand the role of the VB 2005 preprocessor directives, we can turn our attention to a meatier topic that you should be well aware of: the value type/reference type distinction ■ Source Code The Preprocessor project is located under the Chapter 11 subdirectory Understanding Value Types and Reference Types Like any programming language, VB 2005 defines a number of keywords... (NameOfTheObject_NameOfTheEvent) that could easily break as you renamed the objects in your code base With the VB 2005 Handles keyword, however, the name of your event handlers can be anything you choose 299 5785ch10.qxd 300 3/31/06 10:51 AM Page 300 CHAPTER 10 ■ CALLBACK INTERFACES, DELEGATES, AND EVENTS Multicasting Using the Handles Keyword Another extremely useful aspect of the Handles statement is the fact... types (and various combinations thereof) As you will see, the CLR handles structure and class variables very differently in regards to their memory allocation At this time, you will revisit the ByVal and ByRef keywords to see how they handle value types and reference types under the hood At this time, you will also come to understand the role of boxing and unboxing operations The remainder of this chapter... new programming constructs supported by VB 2005 with the release of NET 2.0 Here you will come to understand the role of operator overloading and the creation of explicit (and implicit) conversion routines To wrap up, you’ll check out the role of two new casting-centric keywords (DirectCast and TryCast) At the conclusion of this chapter, you will have a very solid grounding on the core features of the. .. added to the list of function pointers maintained by the Car’s internal delegate (remember, the Event keyword expands to produce—among other things—a delegate type) Of course, you do not call add_XXX() directly, but rather use the VB 2005 AddHandler statement As well, if you wish to dynamically remove an event handler from the underlying delegate’s invocation list, you can indirectly call the compiler-generated... call back to another object is such a helpful construct, VB 2005 provides a small set of keywords to lessen the burden of using delegates in the raw For example, when the compiler processes the Event keyword, you are automatically provided with registration and unregistration methods that allow the caller to hook into an event notification Better yet, using the Event keyword removes the need to define . 3/31 /06 10: 51 AM Page 29 6 CHAPTER 10 ■ CALLBACK INTERFACES, DELEGATES, AND EVENTS 29 7 Because the ability for one object to call back to another object is such a helpful construct, VB 20 05 provides. deeper into the VB 20 05 Event keyword. Events Under the Hood A VB 20 05 event actually encapsulates a good deal of information. Each time you declare an event with the Event keyword, the compiler. the System.Collections namespace and examining a number of common interfaces used by the collection- centric types. 5785ch09.qxd 3/31 /06 10: 50 AM Page 27 9 5785ch09.qxd 3/31 /06 10: 50 AM Page 28 0 CHAPTER