Microsoft Visual C++ .NET 2003 Kick Start

When the client application wants to tell the remoted object something, there's no problem: just call a method. The implementations in the remoted class can do anything, including updating the database, adding entries to an event log, and so on. But what if something happens on the server that the client application should know about?

For example, you might write a client application used by order takers. It might submit orders to the server application using remoting, and get price lists and availability by calling some other methods of a remoted class from time to time. How can the server application notify the clients of the remoted objects that it's time to get a new price listor even push a price list back down to them? The answer lies with .NET events.

Basics of .NET Events

Events are a simple and robust notification technique used within the .NET runtime much as Windows messages were used by Windows programmers before .NET existed. For example, when the user clicks a button on a GUI, an event is generated. A method in your code handles that event and reacts to the button click. This behavior is extensibleyou can add and react to your own events. You can even do this over the remoting boundary.

To keep the example as simple as possible, this section adds a custom event to the server application that is raised by clicking a button. The client application will handle the event by displaying a message box.

Long ago, C and C++ programmers arranged event callbacks using function pointers. These were extremely powerful, but like so much C++ power they could be hard to use. The .NET Framework supports a type-safe function pointer implementation called a delegate , and does so across programming languages. Events and delegates combine to create an easy-to-use publish-and-subscribe mechanism. Event sources keep a list of handlers that might be interested in the events they raise, and with a single line of code the source can raise the event to all the handlers at once.Event listeners create a delegate object and pass it to the source to add to the list of handlers. It's all made as simple as possible, and it all works beautifully over remoting.

Changes to the Remoting Server

To use .NET events on the server, the client creates an appropriate delegate and passes it to the server, which adds it to a list of handlers. These handlers will then be invoked when the user clicks a button on the server form.

There are a number of steps involved in adding events to a remoted application. You can complete them in whatever order you prefer, but the application will not be usable until they have all been done. The steps are as follows :

  • Define the delegate to be passed from the client to the server.

  • Define a class, available to the server, that represents the event handler on the client.

  • Add the event handler list to a suitable class on the server, in this case the existing Greeter class.

  • Change the Greeter class so that the server application can reach the instance that was created for the remote client.

  • Add a button to the server form.

  • Code a handler for the button click that alerts all listeners.

  • Change the server remoting configuration file to support two-way communication.

  • Implement a handler in the client for the event that comes from the server.

  • Change the client remoting configuration file to support two-way communication.

  • Write code in the client that creates a delegate and passes it to the server to be added to the list.

Each of these steps is described in detail in this section.

Defining a Delegate

A delegate is often described as a type-safe function pointer. A delegate that represents a function that takes a String* can't be used to point to a function that takes different parameters. Similarly, a delegate that represents a function that returns a String* can't be used to point to a function with a different return type. The signature and return type of the function are part of the definition of the delegate. This is why delegates are called type-safe.

When working with events, both the code that raises the event (the event source) and the code that handles the event (the event sink) define a delegate. This is not an object or an instance, but rather an idea or definition. In managed C++, you define a delegate with the __delegate keyword. Add this line to Greeter.h in the Greeting project, before the namespace definition:

public __delegate void RemoteAlert(String* str);

Any function that takes a String* and returns void can be used to create a RemoteAlert delegate representing that function. The server-side code just works with the delegate and need not be aware of the name or details of the function that was used to create it. This definition is in the Greeting assembly so that the other applications in the solution can all access it.

Defining the Event Handler Base Class

The event handler is a function that matches the signature of the delegate: In this example it must be a void function that takes a String* . The client application implements a class to hold this function, and passes a reference to an instance of the class up to the server, to be added to the list of event handlers maintained there. The class must be known to the server code, either because the server code includes a header file that defines it or because the server code has a reference to an assembly that defines it.

SHOP TALK: WHY USE INHERITANCE WITH EVENT HANDLERS?

The requirement that the handler class be known to the server causes trouble for many developers who are using remoting. They discover that events appear to work only when the server application and client application are in the same folderin other words, when they are not really using remoting. Many have discovered that copying the client application to the remote server machine also enables events to work properly over remoting. Copying a client application to the server machine, even though it runs on a client machine, feels awkward . It also sets you up for frustrating debugging or maintenance work, because you might have to copy files again and again, and forgetting to copy might make the application fail even though there's nothing wrong with the code.

The technique I show in this chapter looks awkward at first glance. But it works, and even better it works without your having to copy the client application to the server machine. It's worth learning.

To make the event handler class known to the server, without the server having a reference to the entire client application, use a base class for the handler class. Define the base class in the class library, as part of the Greeting namespace. The server can add a reference to the class library and it will then gain access to the definition of this base class. The client will implement a class that inherits from this base class, and pass a reference to it (as a base class pointer) to the server. The server does not need to know about the specific base class that was implemented in the client application. This lessens the coupling between the client and the server and simplifies deployment and maintenance.

Add a class to the Greeting solution and name it RemoteHandlerBase . Edit remotehandlerbase.h so that it reads like this:

#pragma once using namespace System; namespace Greeting { public __gc class RemoteHandlerBase: public MarshalByRefObject { public: void Alert(String* s) { HandleAlert(s); } protected: virtual void HandleAlert(String* s)=0; }; }

The class must inherit from MarshalByRefObject , because instances of a class derived from it will be passed from the client to the server, to be added to the handler list. It has two methods: Alert() and HandleAlert() . HandleAlert() does the actual work (it handles the Alert event when it is raised), and it's pure virtual in this base class. An implementation will be provided in any class that derives from RemoteHandlerBase . Alert() must be nonvirtual, and it just calls the virtual HandleAlert() function; the compiler will ensure that the appropriate derived class version is executed.

Changing the Greeter Class

The Greeting class library has grown from one class to two to support events. The next step is to change the Greeter class so that it maintains a list of events, and so that the server application can access a Greeter instance to raise an event.

Revise the class definition in Greeter.h so that it reads as follows:

public __gc class Greeter: public MarshalByRefObject { public: __event RemoteAlert* RemoteAlertEvent; String* Greet(String* name); String* Greet(); Data::DataSet* Records(); static void Alert(String* s); Greeter(); private: static Collections::ArrayList* greeters=0; };

The new lines to be added are in bold text.

The RemoteAlert variable is decorated with the __event keyword. This indicates that it is actually a list of event handlers that match the RemoteAlert signature. New event handlers can be added to this list with the += operator. The Greeter class needs no code to manage or maintain this list; that code is all generated for you behind the scenes when you use the __event keyword.

The Alert method is a static method: When it is called it will invoke all the handlers for all the Greeter objects that have been created. This is not the only way to use events in remoting; a different application might have a nonstatic Alert method, so that each Greeter object alerted its own listeners. However, in this example application, the client object has one Greeter instance, obtained through remoting, and the server application has no reference to that object. Using a static method to access all instances provides access to the client's Greeter object from within the server application.

A constructor is added to the Greeter class so that it can maintain an instance list. This is the static variable called greeters . As each Greeter object is created, it will add itself to the list. That is how the Alert() method can reach all the Greeter objects that have been created.

In Greeter.cpp, add this implementation of the constructor:

Greeter::Greeter() { if (!greeters) greeters = new Collections::ArrayList(); greeters->Add(this); }

The ArrayList collection is an array that will grow itself as more items are added. It's easy to use; this code just adds another Greeter reference to the list.

Next, add this implementation of Alert() :

void Greeter::Alert(String* s) { for (int i=0; i < greeters->Count; i++) (static_cast<Greeter*>(greeters->get_Item(i)))->RemoteAlertEvent(s); }

This code loops through the ArrayList , using get_Item() to access each item in the list in turn . The item is cast to a Greeter* ( ArrayList keeps the collection as Object* references) and then this code raises each Greeter 's event. The events are raised by calling the RemoteAlertEvent member variable as though it were a function. (It's actually an instance of a class, generated by the __event keyword, that overrides the ( operator, also called the function call operator.) Whether one event handler has been added to the list, or several, the list of events will take care of notifying each handler in turn that the event has been raised, and passing along the parameter: in this case a String* that was passed to Alert() .

Modifying the Form

Using the toolbox, drag another button onto Form1 of the server application. Change the name and text to Alert . Double-click it to edit the button click handler, and add this code:

private: System::Void Alert_Click(System::Object * sender, System::EventArgs * e) { Greeting::Greeter::Alert("The server Alert button was clicked"); }

This handler just calls the static Alert() method of the Greeter class, which as you've already seen will raise the Alert event to all the handlers that were added to the event lists in any Greeter objects that were created.

Changing the Server Configuration File

In this example application, remoting is controlled through a configuration file. To enable event handling, and passing callback references from client to server, the configuration file must be changed slightly.

Find this element in the app.config file in the GreetingServer project:

<channel ref="tcp" port="9555" />

Replace it with this expanded version:

<channel ref="tcp" port="9555"> <s> <formatter ref="binary" typeFilterLevel="Full" /> </serverProviders> </channel>

This change is required in version 1.1 of the .NET Framework and up; older samples or articles that cover remoting will not mention it. Security settings in version 1.1 do not support callbacks by default, because they might represent a vulnerability. Client code is letting server code decide to trigger the execution of client code. You have to deliberately turn the feature on.

Implementing the Client Event Handler

Add a class called RemoteHandler to the GreetingClient project. This class inherits from RemoteHandlerBase and implements the HandleAlert() method. Edit remotehandler.h so that it reads like this:

#pragma once using namespace System; namespace GreetingClient { public __gc class RemoteHandler: public Greeting::RemoteHandlerBase { protected: void HandleAlert(String* msg); }; }

Because RemoteHandler inherits from RemoteHandlerBase , which in turn inherits from MarshalByRefObject , instances of this class can be passed over remoting by reference. These references are used to invoke the HandleAlert() method when the event is raised.

Edit remotehandler.cpp, adding this implementation of HandleAlert() :

namespace GreetingClient { void RemoteHandler::HandleAlert(String* msg) { Windows::Forms::MessageBox::Show(msg,"Alert from server"); } }

This method is part of the client application. The message box it displays will appear on the screen of the client computer when the Alert button is clicked on the server computer.

Changing the Client Configuration File

Just as the remoting configuration file for the server application required changes to support events over remoting, so does the remoting configuration file for the client. Open app.config in the GreetingClient solution, and after the <client> tag that is already there, add this element:

<channel ref="tcp" port="0"> <serverProviders> <formatter ref="binary" typeFilterLevel="Full" /> </serverProviders> </channel>

As in the server configuration file, this element takes care of the security restrictions, making it clear that you are deliberately using callbacks over remoting and that you trust the server application to trigger execution of parts of the client application. The <channel> element specifies a port of 0, which means that any available port can be used.

In the constructor for the Form1 class in GreetingClient , add this line after the call to Configure :

Runtime::Remoting::Channels::ChannelServices::RegisterChannel( new Runtime::Remoting::Channels::Tcp::TcpServerChannel("callback", 9556));

This line sets up the listening channel on which the callbacks will come. Choose a port that is not the same as the port in the configuration file, and that you are confident is not already in use on the network.

Creating a Handler and Adding It to the List on the Server

The constructor for the client Form1 is where the event handler will be passed to the server. As soon as the form is created, this client will be listening for Alert events from the server. Add these lines after the line that creates a Greeter object:

RemoteHandler* rh = new RemoteHandler(); Greeting::RemoteHandlerBase* rhb = static_cast<Greeting::RemoteHandlerBase*>(rh); greet->RemoteAlertEvent += new RemoteAlert(rhb, &RemoteHandler::Alert);

This code creates an instance of the RemoteHandler class, defined here in the client. It then casts that instance to a RemoteHandlerBase* , because the server is only aware of the RemoteHandlerBase class. ( RemoteHandlerBase is in the Greeting assembly, and the client has a reference to that assembly, so client code knows about both RemoteHandler and RemoteHandlerBase .) The final line of this code snippet creates a delegate using the special C++ syntax. The first parameter to the delegate constructor is a pointer to the object, and the second parameter uses the pointer-to-member syntax to create a function pointer. Once constructed , the delegate is added directly to the event handler list in the Greeting object by accessing the public variable and using the += operator.

Build the application, and copy greetingserver.exe, greetingserver.exe.config, and greeting.dll to a working folder on the other machine. Make sure the client configuration file contains the name or IP address of that other machine. On the second machine, run the server application by double-clicking it and then click the Listen button. On the local machine, start the client application, either by double-clicking the .EXE or by pressing F5 in Visual Studio. You can confirm that the buttons (Greet, Greet Name, and Get Records) still work as before. Then go to the remote machine and click the Alert button on the server. A message box like the one in Figure 14.4 should appear on the client machine. Nothing should appear to happen on the server machine.

Figure 14.4. The client application displays a message box when it receives an event from the server.

Using events involves a certain amount of setup (changing configuration files, creating a base class for the event handler, adding event handler lists to a class, and so on), but once this infrastructure is in place, you can add as many custom events and handlers to your system as you want. The server can then notify all the client applications about changes or updates whenever they happen.

Категории