In my previous column, I introduced the basics of .NET remoting. I showed how to create a remoted class, host it in a server application, and call methods of the class from a client application.
The communication in that simple example was all client-driven. When the client wanted information from the server, it called methods of the remoted object such as Greet() or Records(). These methods don’t take parameters, but there’s no restriction on remoted objects. You could easily design a system where the client passes parameters to the server as arguments on a method call. For example, the client applications could be used to process orders, and the method calls could pass along information such as item codes, quantities, and ship dates. If the only new information that enters your system is from your client, the remoting model as presented will work perfectly for you.
But, in many systems there is input from several directions at once. For example, the central office might change the prices of the items for which orders are being taken. How can you be sure that all the clients have the most up-to-date prices at all times?
What you want is event handling: The server can raise an event (PriceChanged perhaps) and the client (or multiple clients) can all handle that event. Events are used throughout .NET when one piece of code needs to notify other pieces of code that something has happened. You write an event handler to deal with button clicks and other user interactions with a WinForm application, among other things.
In this sample, I’ll add a button to the server application (a WinForm app with a big Listen button) and have the handler for the click event raise a custom event. I’ll have the remoting client actually handle that event by displaying a message box. Your job is to run with that and create something useful in your own application.
Defining the Event and Delegate
The delegate represents the event handler. It must be known to both the client and the server. I added this line to the file that declares Greeting::Greeter, the remoted object:
public __delegate void RemoteAlert(String* str);
This means that any function that takes a string and returns void can be used as a RemoteAlert delegate. Delegates are type-safe function pointers: A function that takes an integer, or that takes a string and returns another string, cannot be used as a RemoteAlert delegate. The full signature and return type of the function are part of the definition of the delegate.
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.
This is a real problem for many developers: The handler class must be known to the server. They discover that events appear to work only when the server application and client application are in the same folder—in other words, when they are not really 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 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.
I have a solution to this issue that’s a little more trouble at the beginning and then a lot less trouble afterwards. I define a base class (RemoteHandlerBase) with a pure virtual function (I called mine HandleAlert()) that matches the signature of the delegate. This class is in the same namespace and assembly as the remoted object, Greeter. Then, in the client, I define a derived class, RemoteHandler, that overrides and implements HandleAlert(). The instance that’s passed up to the server is actually an instance of RemoteHandler, but upcasts are always allowed, so the server can think of it as an instance of RemoteHandlerBase. When the server calls HandleAlert(), thanks to good old polymorphism, the implementation in the derived class is actually called. It works like a charm and you never have to copy your client application to the server.
Changing the Server
Here is the base class, which is included in the project that defines the remoted object:
namespace Greeting
{
public __gc class RemoteHandlerBase: public MarshalByRefObject
{
public:
void Alert(String* s)
{
//polymorphism rules :-)
HandleAlert(s);
}
protected:
virtual void HandleAlert(String* s)=0;
};
}
Notice that the class must inherit from MarshalByRefObject: A reference will be passed to the server from the client, but the methods will execute on the client. Alert() is a non-virtual wrapper around the virtual function; it will be called from the UI code.
The remoted object needs to change as well: It needs to keep a list of event handlers and to provide a static Alert() function:
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; };
RemoteAlertEvent is actually a list of events. It will take new items with += and maintains the list. I added the static Alert() because the server code actually has no access to Greeter instances: There is one Greeter object, created as a result of client calls, and the server code isn’t using it. (The Greeter class is remoted as a server-activated singleton, so there’s only one instance, but by changing only the configuration file I might make it client activated, and then there could be many instances at once, each associated with a different client application.) Adding a static method saved me from any instance management issues on the server side. It uses the greeters list to access all the instances that have been created. The constructor adds instances to the list. Here are the implementations of the constructor and Alert():
Greeter::Greeter() { if (!greeters) greeters = new Collections::ArrayList(); greeters->Add(this); } void Greeter::Alert(String* s) { for (int i=0; i < greeters->Count; i++) (static_cast<Greeter*>(greeters->get_Item(i))) ->RemoteAlertEvent(s); }
I need the static_cast<> because ArrayList is a collection of Object references, so I need to cast them back to Greeter references as they come out of the collection. If you find this code ugly, you could use an STL collection; just remember the gcroot template that enables you to put managed types into an STL collection. (This is going to be so much neater in Whidbey.)
Notice how Alert() gets hold of each item in the collection, and after casting it, calls the RemoteAlertEvent() event as though it was a function. This will raise the event and invoke all the handlers that have been added to the event. There’s no code here on the server to add handlers to the event, though. You’ll see that on the client side.
So that the event can easily be raised, I added a button to the server form and had the event handler for the button click call the static Alert() method:
private: System::Void Alert_Click(System::Object * sender, System::EventArgs * e) { Greeting::Greeter::Alert("The server Alert button was clicked"); }
The last step on the server side is to change the configuration file. This element needs to be expanded:
<channel ref="tcp" port="9555" />
It ends up looking like this:
<channel ref="tcp" port="9555"> <serverProviders> <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.
Changing the Client
The first step on the client side is to define and implement the handler class, RemoteHandler:
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.
The implementation of HandleAlert() is nice and simple:
void RemoteHandler::HandleAlert(String* msg) { Windows::Forms::MessageBox::Show(msg,"Alert from server"); }
Just as the server configuration file needed to be changed to permit callbacks, so does the client. After the existing <client>…</client> element, I added a channel 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 I am deliberately using callbacks over remoting and that I trust the server application to trigger execution of parts of the client application. The <channel> element specifies a port of 0, so any available port can be used.
The client constructor gains another line of remoting “plumbing.” (It’s still a lot less code than you would write with DCOM.) After the call to Configure(), I added this line to set up the callback channel:
ChannelServices::RegisterChannel(new Tcp::TcpServerChannel ("callback", 9556));
(This needs a using namespace Runtime::Remoting::Channels to find the classes.) Choose any port you like that’s different from the one you’re accessing the remoted object over, and that’s unlikely to be in use by anyone else.
You may be wondering how event handlers get added to the list that the server is keeping. I just use the remoted instance. In the Form1 constructor, there is already a line to create the remote object:
greet = new Greeting::Greeter();
Right after that line, I added:
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 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.
That’s it! The client has code to make an instance of the handler object and add it to the list on the server. It also has an implementation of the handler method. The server has the delegate definition, and code to maintain a list of event handlers then raise the event to them. The configuration files have been tweaked to allow events to pass over remoting.
Trying It Out
If you built the code for my previous column, and made the changes I’ve shown here, you can test it quite simply. Rebuild the entire solution and copy greetingserver.exe, greetingserver.exe.config, and greeting.dll to your second machine. Start the server and click Listen. Go to your first machine and start the client. If you want, make sure that Greet() and GetRecords() still work. Then, on the server, click the new button that raises the event. Nothing should appear to happen on the server. Go back to the client and you should see a message box. If you do, that means you raised an event on the server that was handled on the client. The possibilities for that are tremendous. This is a huge advantage of remoting over Web services and one I encourage you to explore a bit more.
About the Author
Kate Gregory is a founding partner of Gregory Consulting Limited (www.gregcons.com). In January 2002, she was appointed MSDN Regional Director for Toronto, Canada. Her experience with C++ stretches back to before Visual C++ existed. She is a well-known speaker and lecturer at colleges and Microsoft events on subjects such as .NET, Visual Studio, XML, UML, C++, Java, and the Internet. Kate and her colleagues at Gregory Consulting specialize in combining software develoment with Web site development to create active sites. They build quality custom and off-the-shelf software components for Web pages and other applications. Kate is the author of numerous books for Que, including Special Edition Using Visual C++ .NET.