Chapter 15. Remoting

ASP.NET is a giant step forward in the evolution of programming models because it vastly simplifies the development of Web applications. For companies seeking to build traditional thin-client applications—applications that rely on browsers to display HTML generated on servers—ASP.NET is the right tool for the job. Its benefits include shorter development cycles and software that is more scalable, more maintainable, and more robust.

Despite the importance of thin-client applications in today’s market, there still exist applications that benefit from, and sometimes require, a tighter coupling of client and server. In recent years, literally thousands of companies have deployed distributed applications built on DCOM (Distributed Component Object Model), CORBA (Common Object Request Broker Architecture), Java RMI (Remote Method Invocation), and other remoting technologies. These types of applications won’t go away anytime soon, nor will the advantages that they enjoy over thin-client applications. Closely coupled applications are better suited for two-way communication between clients and servers than are conventional Web applications. They also tend to utilize network bandwidth more efficiently because they can use lean binary protocols in lieu of HTTP. And close coupling facilitates stateful connections between clients and servers, which in turn simplifies the task of building stateful applications.

Many people feel that the sweet spot for intranet applications built with the .NET Framework in coming years won’t be traditional thin-client applications but “rich-client” applications—applications that feature Windows forms on the client (better to overcome the limitations of HTML), managed components on the server, and pluggable protocol channels in between. One reason for this belief is that the .NET Framework includes a robust and easy-to-use remoting infrastructure that supports DCOM-style applications. This infrastructure is physically manifested as a set of types in the System.Runtime.Remoting namespace and its descendants. Get to know these types and you can be building closely coupled rich-client applications in no time. And you can do so without all the hassles that come with COM programming—apartments, IDL (Interface Definition Language), reference counting, lack of exception handling, incompatible languages and data types, and so on.

For an author who has spent the past several years of his life working with COM and helping companies overcome the problems and limitations thereof, it’s fitting that the final chapter of this book be devoted to .NET remoting. In this writer’s opinion, .NET remoting is a better COM than COM. Let’s see if that assertion holds up.

Remoting Basics

Remoting begins with the class or classes you want to remote. A conventional class can be used only by clients running in the same application domain. A remotable class can be used by clients in other application domains, which can mean other application domains in the client’s process, application domains in other processes, or application domains on other machines. How hard is it to write a remotable class? Not hard at all. All you have to do is derive from System.MarshalByRefObject.

The following class can be instantiated only in a client’s own application domain:

public class LocalClass
{
  ...
}

The following class can be instantiated in a client’s application domain or in a remote application domain:

public class RemotableClass : MarshalByRefObject
{
  ...
}

When a client creates a remote instance of RemotableClass, the .NET Framework creates a proxy in the client’s application domain, as shown in Figure 15-1. The proxy looks and feels like the real object. Calls received by the proxy, however, are transmitted to the remote object through a channel connecting the two application domains. We say that an object served by a proxy has been marshaled by reference because the object isn’t copied to the client’s application domain; the client merely holds a reference to the object. That reference is the proxy.

Marshal-by-reference remoting architecture.
Figure 15-1. Marshal-by-reference remoting architecture.

The second step in remoting an object is to have a server process register the remotable class so that it can be activated from another application domain. Depending on how the object is to be activated, the server registers the remotable class by calling one of two static System.Runtime.Remoting.RemotingConfiguration methods: RegisterActivatedServiceType or RegisterWellKnownServiceType. The following statement uses RegisterWellKnownServiceType to register RemotableClass for remote activation:

RemotingConfiguration.RegisterWellKnownServiceType (
    typeof (RemotableClass),       // Remotable class
    "RemoteObject",                // URI of remotable class
    WellKnownObjectMode.SingleCall // Activation mode
);

The first parameter identifies the remotable class. The second specifies the uniform resource identifier (URI) that the client will use to activate the object—that is, the URI that the client will use to tell the server to activate an instance of RemotableClass. The third and final parameter specifies the activation mode. The two possible choices are WellKnownObjectMode.SingleCall, which creates a new instance of RemotableClass for each and every call placed by a client, and WellKnownObjectMode.Singleton, which creates one instance of RemotableClass to process all calls from all clients.

To make RemotableClass available to remote clients, the server process must also create and register a channel. The channel provides a conduit for communication between an object and a remote client. The .NET Framework class library includes two channels for use on the server side: System.Runtime.Remoting.Channels.Tcp.TcpServerChannel, which accepts TCP connections from remote clients, and System.Runtime.Remoting.Channels.Http.HttpServerChannel, which accepts HTTP connections. The following statements create a TcpServerChannel that listens on port 1234 and register it with the .NET Framework:

TcpServerChannel channel = new TcpServerChannel (1234);
ChannelServices.RegisterChannel (channel);

These statements register an HttpServerChannel that listens on port 1234:

HttpServerChannel channel = new HttpServerChannel (1234);
ChannelServices.RegisterChannel (channel);

TcpServerChannel is the more efficient of the two because TCP is a leaner protocol than HTTP. HttpServerChannel, however, is the channel of choice for applications that use Internet Information Services (IIS) as a remote activation agent—a topic that’s discussed later in this chapter.

A client application that wants to create a remote instance of RemotableClass has to do some registration of its own. First it must register a client channel. The .NET Framework provides two client channel types: TcpClientChannel and HttpClientChannel. The former lets a client talk to a server that’s listening with a TcpServerChannel; the latter enables it to communicate with a server listening with an HttpServerChannel. Second, if the client wants to use the new operator to instantiate the remote object, it must register the remote class in the local application domain. RemotingConfiguration.RegisterWellKnownClientType registers a class on the client that’s registered with RemotingConfiguration.RegisterWellKnownServiceType on the server. The following statements create and register a client-side TCP channel. They also register RemotableClass as a valid type in the client’s application domain:

TcpClientChannel channel = new TcpClientChannel ();
ChannelServices.RegisterChannel (channel);

RemotingConfiguration.RegisterWellKnownClientType (
    typeof (RemotableClass),            // Remotable class
    "tcp://localhost:1234/RemoteObject" // URL of remotable class
);

The second parameter passed to RegisterWellKnownClientType specifies the URL where the remote class is located. The protocol (tcp in this example) must match the protocol of the channels registered in the application domains. The machine identifier—localhost—identifies the server that exports RemotableClass and thus identifies the machine on which the object will be created. You can replace localhost with a machine name or an IP address. The port number following the machine identifier must match the port number that the server is listening on—that is, the port number that the server registered with TcpServerChannel. Finally, after the machine identifier and port number comes the object URI, which must match the URI that the server passed to RegisterWellKnownServiceType.

Once both the client and server have performed the requisite registrations, all that remains is for the client to instantiate RemotableClass with the new operator:

RemotableClass rc = new RemotableClass ();

This action creates a proxy in the client’s application domain and returns a RemotableClass reference that physically refers to the proxy but logically refers to the remote object. Let’s prove that this works by testing it out in a real application.

Your First Remoting Application

The application in Example 15-2 demonstrates the basic steps involved in activating an object in a remote application domain. It is centered on a remotable class named Clock. Clock has one public method—GetCurrentTime—that retrieves the current wall-clock time and returns it as a string. The application contains three constituent parts:

  • A DLL named ClockServer.dll that houses the Clock class

  • A server named TimeServer.exe that registers Clock for remote activation

  • A client named TimeClient.exe that activates Clock remotely

TimeServer and TimeClient are console applications. When executed, TimeClient displays the current time. It gets the time by instantiating a Clock object and calling Clock.GetCurrentTime.

Example 15-2. A simple remoting application.

ClockServer.cs

using System;

public class Clock : MarshalByRefObject
{
    public string GetCurrentTime ()
    {
        return DateTime.Now.ToLongTimeString ();
    }
}

TimeServer.cs

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

class MyApp
{
    static void Main ()
    {
        TcpServerChannel channel = new TcpServerChannel (1234);
        ChannelServices.RegisterChannel (channel);

        RemotingConfiguration.RegisterWellKnownServiceType
            (typeof (Clock), "Clock", WellKnownObjectMode.SingleCall);

        Console.WriteLine ("Press Enter to terminate...");
        Console.ReadLine ();
    }
}

TimeClient.cs

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

class MyApp
{
    static void Main ()
    {
        TcpClientChannel channel = new TcpClientChannel ();
        ChannelServices.RegisterChannel (channel);

        RemotingConfiguration.RegisterWellKnownClientType
            (typeof (Clock), "tcp://localhost:1234/Clock");

        Clock clock = new Clock ();
        Console.WriteLine (clock.GetCurrentTime ());
    }
}

Here’s a script for building and testing the application:

  1. Use the commands below to build ClockServer.dll, TimeServer.exe, and TimeClient.exe, or simply copy them from the CD that comes with this book:

    csc /t:library clockserver.cs
    csc /r:clockserver.dll timeserver.cs
    csc /r:clockserver.dll timeclient.cs
  2. Open a console window and start TimeServer. Do not press the Enter key to terminate the application.

  3. Open a second console window and run TimeClient. The current time should be displayed in the window.

  4. Terminate TimeServer by switching back to the first console window and pressing the Enter key.

TimeClient runs in one process, and TimeServer runs in another. Because Clock is instantiated in the application domain in which it is registered, calls from TimeClient cross process boundaries to TimeServer.

When most people hear the term “remoting,” they think of calls going from machine to machine. With some minor tweaking, the application in Example 15-2 can be modified to work across machines. To demonstrate, pick two computers on your network, one to act as the server (machine A), and the other to act as the client (machine B). Then do the following:

  1. Copy TimeServer.exe and ClockServer.dll to a directory on machine A.

  2. Replace localhost in the URL that TimeClient passes to RemotingConfiguration.RegisterWellKnownClientType with machine A’s name or IP address.

  3. Rebuild TimeClient.exe.

  4. Copy the modified version of TimeClient.exe to a directory on machine B. Copy ClockServer.dll to the same directory.

  5. Open a console window on machine A and start TimeServer.

  6. Open a console window on machine B and run TimeClient. To prove that the time displayed on machine B came from machine A, temporarily change the clock on machine A and verify that TimeClient displays the modified time.

  7. Terminate TimeServer on machine A.

Were you surprised by step 4—surprised that you had to copy ClockServer.dll to the client machine? To create a proxy for a remote Clock object, the .NET Framework has to have metadata describing the Clock class. It gets that metadata from the DLL. If you don’t copy the DLL to the client machine, the framework can’t activate a remote Clock object because it can’t create the proxy. You can prove it by temporarily deleting ClockServer.dll from machine B and running TimeClient again. This time, the .NET Framework throws an exception because it has no metadata describing Clock.

Programmatic vs. Declarative Configuration

TimeServer and TimeClient use information embedded in their source code to register channels and remotable classes. The drawback to performing registrations this way is that if any of the registration data changes (as it did when you modified TimeClient to point to a remote server), you have to modify the source code and rebuild the executables.

That’s why the .NET Framework supports an alternate form of registration that is declarative rather than programmatic. Declarative registration takes information from CONFIG files and is enacted by calling the static RemotingConfiguration.Configure method.

Example 15-3 contains modified versions of TimeServer and TimeClient that demonstrate how declarative configuration works. Functionally, this application is identical to the one in Example 15-2. Internally, it’s different. Rather than call RegisterChannel, RegisterWellKnownServiceType, and RegisterWellKnown­ClientType, the revised source code files call RemotingConfiguration.Configure and pass in the names of configuration files. Now modifying the client to activate a Clock object on another machine is a simple matter of using Notepad or the text editor of your choice to edit TimeClient.exe.config. No source code changes are required.

Example 15-3. A remoting application that uses declarative configuration.

ClockServer.cs

using System;

public class Clock : MarshalByRefObject
{
    public string GetCurrentTime ()
    {
        return DateTime.Now.ToLongTimeString ();
    }
}

TimeServer.cs

using System;
using System.Runtime.Remoting;

class MyApp
{
    static void Main ()
    {
        RemotingConfiguration.Configure ("TimeServer.exe.config");
        Console.WriteLine ("Press Enter to terminate...");
        Console.ReadLine ();
    }
}

TimeServer.exe.config

<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown mode="SingleCall" type="Clock, ClockServer"
          objectUri="Clock" />
      </service>
      <channels>
        <channel ref="tcp server" port="1234" />
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

TimeClient.cs

using System;
using System.Runtime.Remoting;

class MyApp
{
    static void Main ()
    {
        RemotingConfiguration.Configure ("TimeClient.exe.config");
        Clock clock = new Clock ();
        Console.WriteLine (clock.GetCurrentTime ());
    }
}

TimeClient.exe.config

<configuration>
  <system.runtime.remoting>
    <application>
      <client>
        <wellknown type="Clock, ClockServer"
          url="tcp://localhost:1234/Clock" />
      </client>
      <channels>
        <channel ref="tcp client" />
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

Declarative registration has its drawbacks, too. Registration information embedded in an EXE isn’t easily changed by an end user. Registration information encoded in a text file, however, can be modified by anyone savvy enough to run Notepad. It can also be deleted altogether. Choosing between declarative and programmatic registration means choosing which quality is more important to you: convenience or robustness. The right choice depends on the scenario and is ultimately left up to you.

Server Activation vs. Client Activation

The .NET Framework distinguishes between two types of remotable objects: server-activated objects and client-activated objects. Server-activated objects are registered with RemotingConfiguration’s RegisterWellKnownServiceType and RegisterWellKnownClientType methods. The applications in Example 15-2 and Example 15-3 rely on server-activated objects. Client-activated objects are registered with RegisterActivatedServiceType and RegisterActivatedClientType instead. Server-activated objects are called “server activated” because when the client calls new, only a proxy is created. The objects themselves aren’t created (“activated”) until a method call is placed through the proxy. In other words, the server, not the client, decides when to physically create the objects. Client-activated objects, on the other hand, are created on the server the moment the client calls new. The differences between server-activated objects and client-activated objects, however, run deeper than these simple descriptions suggest.

A practical difference between server-activated objects and client-activated objects is that the latter can be activated with nondefault constructors (constructors that accept parameters), but the former can not. Server-activated objects don’t support nondefault constructors because calls to new don’t map 1-to-1 to object instantiations; new creates a proxy but doesn’t create the corresponding object. Client-activated objects, on the other hand, can be activated with nondefault constructors because new instantiates both the proxy and the object.

A more fundamental difference between server-activated objects and client-activated objects has to do with how clients and objects are paired together. When you register a server-activated object, you specify an activation mode that determines whether a new object instance is created to service every request or one object instance is created to service all requests. The two supported activation modes are

  • WellKnownObjectMode.SingleCall, which creates a unique object instance for each request

  • WellKnownObjectMode.Singleton, which creates one object instance and uses it to service all requests

Circumstances usually dictate which activation mode is appropriate. For example, if a remote object provides a “one-shot” service to its clients (such as the current time of day) and has no need to preserve state between method calls or to share state among its clients, then SingleCall is the right choice. A single-call object can’t easily preserve state between method calls because if one client places 10 method calls through the same proxy, 10 different object instances are created to satisfy those calls. A single-call object can’t easily share state among clients either, because if 10 different clients activate an instance of the same class and place one call each through their respective proxies, again the .NET Framework creates 10 different object instances to service the clients’ requests.

By contrast, the .NET Framework creates a singleton object one time and uses it to service all requests from all clients. If 10 clients activate a singleton object and place 10 calls each to it through their proxies, just one object instance is created. That instance receives all 100 calls placed by the clients. Because a singleton object hangs around between method calls and is shared by all active clients, it’s perfectly capable of retaining state between method calls and even of disbursing state among its clients. One client could pass some data to the object, for example, and the object could store the data in a field. Another client could then call the object and retrieve the data.

One nuance to be aware of regarding singleton objects is that the .NET Framework makes no attempts to synchronize calls placed to them. If two clients call a singleton at exactly the same time, the calls will arrive on two different threads and be processed concurrently. The implication? Unless you know beyond the shadow of a doubt that calls from clients will never overlap (you normally don’t), the object had better be thread-safe. For a refresher on thread synchronization mechanisms supported by the .NET Framework, refer to Chapter 14.

Client-activated objects offer a third option that serves as a middle ground between single-call server-activated objects and singleton server-activated objects. Each call to new placed by a client creates a new instance of a client-activated object that serves that client and that client only. Client-activated objects can preserve state from one method call to the next because they don’t get discarded following each request. They are not, however, suitable for sharing state between clients because every client that creates a client-activated object receives a brand new object instance that is unique to that client.

The combination of single-call server-activated objects, singleton server-activated objects, and client-activated objects gives you three different activation models to choose from. Which model is correct? That depends on the application and what it’s designed to do. For applications such as the ones in Example 15-2 and Example 15-3, single-call server-activated objects make sense because the application doesn’t need to preserve state between method calls or share state among clients. For the distributed drawing application at the end of this chapter, however, singleton server-activated objects fit the bill nicely because the design of the application requires that all clients connect to a common object instance. For an application that doesn’t require all clients to connect to the same object but that holds per-client state on the server, a client-activated object should be used instead.

The application shown in Example 15-4 demonstrates how client activation works and why client-activated objects are sometimes appropriate in the first place. It publishes a remotable client-activated class named Stopwatch. Stopwatch has two methods: Start and Stop. Because Stop returns the number of milliseconds elapsed since the last call to Start, a client can implement a software stopwatch by calling Start and Stop in succession.

Start records the current time in a private field named mark. Stop subtracts mark from the current time and returns the difference in milliseconds. In this example, a single-call server-activated object wouldn’t do because the call to Start would be processed by one Stopwatch instance and the call to Stop would be processed by another. A singleton server-activated object wouldn’t fare any better. If two different clients were executed concurrently and one called Start right after the other, the value written to mark by the first would be overwritten by the second. Consequently, the first client would receive a bogus count from Stop. This is a great example of an application that benefits from a client-activated object because it needs to retain state between method calls but also needs to pair each client with a unique object instance.

To run the application, compile the three source code files and run StopwatchServer. (Be sure to compile Stopwatch.cs with a /t:library switch first and include a /r:stopwatch.dll switch when compiling StopwatchServer.cs and StopwatchClient.cs.) Then, in a separate console window, run StopwatchClient, wait a couple of seconds, and press Enter to display the elapsed time.

Example 15-4. An application that uses a client-activated object.

Stopwatch.cs

using System;

public class Stopwatch : MarshalByRefObject
{
    DateTime mark = DateTime.Now;

    public void Start ()
    {
        mark = DateTime.Now;
    }

    public int Stop ()
    {
        return (int) ((DateTime.Now - mark).TotalMilliseconds);
    }
}

StopwatchServer.cs

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

class MyApp
{
    static void Main ()
    {
        TcpServerChannel channel = new TcpServerChannel (1234);
        ChannelServices.RegisterChannel (channel);

        RemotingConfiguration.RegisterActivatedServiceType
            (typeof (Stopwatch));

        Console.WriteLine ("Press Enter to terminate...");
        Console.ReadLine ();
    }
}

StopwatchClient.cs

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

class MyApp
{
    static void Main ()
    {
        TcpClientChannel channel = new TcpClientChannel ();
        ChannelServices.RegisterChannel (channel);

        RemotingConfiguration.RegisterActivatedClientType
            (typeof (Stopwatch), "tcp://localhost:1234");

        Stopwatch sw = new Stopwatch ();
        sw.Start ();

        Console.WriteLine ("Press Enter to show elapsed time...");
        Console.ReadLine ();

        Console.WriteLine (sw.Stop () + " millseconds");
    }
}

The Activator.GetObject and Activator.CreateInstance Methods

The new operator isn’t the only way to activate remote objects. The .NET Framework offers alternative activation mechanisms in the form of static methods named GetObject and CreateInstance. Both are members of the System.Activator class. GetObject is used to activate server-activated objects, while CreateInstance is used to activate client-activated objects.

When you use GetObject or CreateInstance to activate remote objects, you no longer have to call RegisterActivatedClientType or RegisterWellKnownClientType to register remotable classes on the server. For example, rather than activate a server-activated object this way

RemotingConfiguration.RegisterWellKnownClientType
    (typeof (Clock), "tcp://localhost:1234/Clock");
Clock clock = new Clock ();

you can can activate the object this way:

Clock clock = (Clock) Activator.GetObject
    (typeof (Clock), "tcp://localhost:1234/Clock");

And rather than activate a client-activated object this way

RemotingConfiguration.RegisterActivatedClientType
    (typeof (Stopwatch), "tcp://localhost:1234");
Stopwatch sw = new Stopwatch ();

you can activate it this way:

object[] url = { new UrlAttribute ("tcp://localhost:1234") };
Stopwatch sw = (Stopwatch) Activator.CreateInstance
    (typeof (Stopwatch), null, url);

The big question is why? Why use GetObject or CreateInstance to activate a remote object when you could use new instead? Neither GetObject nor CreateInstance is intrinsically better than new, but both offer one option that new doesn’t. GetObject and CreateInstance can be used to activate a remote type if you possess no knowledge of the type other than a URL and an interface that the type supports. For example, suppose you modify Example 15-2’s Clock class so that it implements an interface named IClock. With GetObject, you could activate a Clock object and get back an IClock interface without referencing the Clock type in your source code:

IClock ic = (IClock) Activator.GetObject
    (typeof (IClock), "tcp://localhost:1234/Clock");

Try the same thing with new, however, and the code won’t compile because new won’t accept an interface name:

// Won’t compile!
RemotingConfiguration.RegisterWellKnownClientType
    (typeof (IClock), "tcp://localhost:1234/Clock");
IClock ic = new IClock ();

CreateInstance can be used in a similar manner to activate a remote client-activated object and return an interface—again, without having intimate knowledge of the type that implements the interface.

Outside the admittedly rare circumstances in which you have metadata describing an interface that a type implements but no metadata describing the type itself, you can use new and GetObject and new and CreateInstance interchangeably.

Object Lifetimes and Lifetime Leases

How long does a remote object live once it’s activated? In DCOM, an object is destroyed (rather, the object destroys itself) when its reference count reaches 0—that is, when the last client calls Release on the last interface pointer. There is no reference counting in .NET remoting. A single-call server-activated object lives for the duration of exactly one method call. After that, it’s marked for deletion by the garbage collector. Singleton server-activated objects and client-activated objects work differently. Their lifetimes are controlled by leases that can be manipulated declaratively or programmatically. Physically, a lease is an object that implements the ILease interface defined in the System.Run­time.Remoting.Lifetime namespace.

The following ILease properties govern the lifetime of the object with which a lease is associated:

Property

Description

Get

Set

InitialLeaseTime

Length of time following activation that the object lives if it receives no method calls

XX

XX

RenewOnCallTime

Minimum value that CurrentLeaseTime is set to each time the object receives a call

XX

XX

CurrentLeaseTime

Amount of time remaining before the object is deactivated if it doesn’t receive a method call

XX

The default lease assigned to singleton server-activated objects and client-activated objects has an InitialLeaseTime of 5 minutes, a RenewOnCallTime equal to 2 minutes, and a CurrentLeaseTime of 5 minutes. If the object receives no method calls, it’s deactivated when CurrentLeaseTime ticks down to 0—that is, after 5 minutes. However, each method call that the object receives in the last 2 minutes of its life extends the lease by setting CurrentLeaseTime to 2 minutes. This effectively means that after an initial grace period whose duration equals InitialLeaseTime, the object must be called at intervals no greater than Renew­OnCallTime or it will go away.

If the default InitialLeaseTime and RenewOnCallTime values aren’t suitable for an application you’re writing, you can change them in either of two ways. The first option is to specify lease times in a CONFIG file. Lease properties set this way affect all remote objects in the server process. The second option is to override MarshalByRefObject.InitializeLifetimeService in the remotable class. Lease properties set that way affect only instances of the overriding class and can be used to assign different lifetimes to different objects.

Here’s an example demonstrating the first option. The following CONFIG file sets InitialLeaseTime and RenewOnCallTime to 20 minutes and 10 minutes, respectively, for all objects in the process:

<configuration>
  <system.runtime.remoting>
    <application>
      <lifetime leaseTime="20M" renewOnCallTime="10M" />
    </application>
  </system.runtime.remoting>
</configuration>

20M means 20 minutes. Other supported suffixes include D for days, H for hours, S for seconds, and MS for milliseconds. A number without a suffix is interpreted to mean seconds. In order for the policies outlined in the CONFIG file to take effect, the server process—the one that registers objects for remote activation—must register the file with RemotingConfiguration.Configure.

The alternative to setting lease options declaratively is to set them programmatically by overriding InitializeLifetimeService. Here’s a remotable class that does just that:

using System;
using System.Runtime.Remoting.Lifetime;

public class RemotableClass : MarshalByRefObject
{
    public override object InitializeLifetimeService ()
    {
        // Get the default lease
        ILease lease = (ILease) base.InitializeLifetimeService ();

        // Modify it
        if (lease.CurrentState == LeaseState.Initial)  {
            lease.InitialLeaseTime = TimeSpan.FromMinutes (20);
            lease.RenewOnCallTime = TimeSpan.FromMinutes (10);
        }
        return lease;
    }
  ...
}

A lease can be modified only if it hasn’t been activated yet (that is, if the object has just been created). The if clause in this sample reads ILease.CurrentState to make absolutely sure that the lease hasn’t been activated.

A remotable class can do away with leases altogether by overriding InitializeLifetimeService and returning null:

using System;

public class Foo : MarshalByRefObject
{
    public override object InitializeLifetimeService ()
    {
        return null;
    }
  ...
}

An object that does this won’t be deleted by the .NET Framework and will continue to run as long as the application domain, process, and machine that host it are running.

A remotable object can acquire a reference to its own lease’s ILease interface at any point during its lifetime by calling the GetLifetimeService method that it inherits from MarshalByRefObject. It can also call ILease.CurrentLeaseTime and ILease.Renew to find out how much time it has left and ask the system to extend the lease. Clients too can acquire ILease interfaces and use them to exert control over leases. The static RemotingServices.GetLifetimeService method takes a client-side object reference as input and returns an ILease interface. Here’s an example demonstrating how to manipulate a lease from the client side:

RemotableClass rc = new RemotableClass ();
ILease lease = (ILease) RemotingServices.GetLifetimeService (rc);
TimeSpan remaining = lease.CurrentLeaseTime;
if (remaining.TotalMinutes < 1.0)
    lease.Renew (TimeSpan.FromMinutes (10));

In this example, the client renews the lease if ILease.CurrentLeaseTime reveals that the lease runs out in less than a minute. The TimeSpan value passed to Renew specifies the amount of time to add to the lease. If CurrentLeaseTime is greater than the value passed to Renew, Renew has no effect. But if Current­LeaseTime is less than the value specified in Renew’s parameter list, Renew sets CurrentLeaseTime to the specified value.

ILease also has methods named Register and Unregister that register and unregister sponsors. A sponsor is an object that implements the System.Run­time.Remoting.Lifetime.ISponsor interface. When an object’s lease expires, the .NET Framework doesn’t mark the object for deletion immediately; first it checks with the application domain’s lease manager to see whether any sponsors are registered. If the answer is yes, the lease manager calls the sponsors’ ISponsor.Renewal methods to see whether any of the sponsors are willing to “sponsor” the object and extend its lease. Sponsors can be registered by clients and servers, and they provide a handy mechanism for controlling object lifetimes based on criteria that you define rather than on the passage of time and the frequency of incoming calls. The ClientSponsor class in the FCL’s System.Runtime.Remoting.Lifetime namespace provides a canned implementation of sponsors that you can use without building sponsor classes of your own.

Because sponsors don’t have to reside on the same machine as the objects they serve, the .NET Framework limits the length of time it waits for a response when it calls a sponsor’s Renewal method. The default is 2 minutes. You can modify the time-out interval by setting a lease’s ILease.SponsorshipTimeout property.

The .NET Framework’s reliance on leases to control the lifetimes of remote objects has an important implication that requires some getting used to by COM programmers. COM objects don’t disappear prematurely unless a buggy client releases one interface too many or the host process terminates. In .NET remoting, however, objects can go away without warning. What happens if a client calls an object that no longer exists? The framework throws a RemotingException. A client might respond to that exception by creating a new object instance and trying again. To the extent you can, try to avoid such errors by using leases with sufficiently high InitialLeaseTime and RenewOnCallTime values.

Advanced Remoting

The first half of this chapter imparted the fundamental knowledge you need to activate managed objects remotely. You learned how to make classes remotable, how to register them for remote activation, and how to activate them. You learned about the difference between server-activated objects and client-activated objects, and you learned about the two activation modes—single-call and singleton—supported by server-activated objects. You also learned about the role that channels play in remote activation and how leases control a remote object’s lifetime.

The second half of this chapter builds on what you’ve learned and enriches your understanding of .NET remoting. In it, you’ll learn:

  • How to use IIS as an activation agent in order to avoid having to manually start server processes running

  • How to combine HTTP channels with binary formatters to increase efficiency on the wire

  • How to use events and delegates with remotely activated objects

  • How to place asynchronous method calls to remote objects

This part of the chapter concludes with a distributed drawing application that ties together many of the concepts introduced in the following pages.

Using IIS as an Activation Agent

One of the most striking differences between .NET remoting and DCOM is that the former offers no support for automatically launching server processes. Someone has to start the server process running so that it can register classes for remote activation and listen for activation requests. This behavior contrasts starkly with that of DCOM, which starts new server processes on request when remote clients call CoCreateInstanceEx or other activation API functions.

.NET remoting offers two ways for you to avoid having to manually start server processes. Option number 1 is to implement the server application as a service. You can write a service by deriving from System.ServiceProcess.ServiceBase and overriding key virtual methods such as OnStart and OnStop. The benefit to implementing a remoting server as a service is that you can configure a service to start automatically each time the system starts up. A service can also run absent a logged-on user, meaning that after auto-starting, it’s always running and always available—even when no one is logged in at the server console.

Option number 2 is to use IIS as an activation agent. IIS is a service itself and is always up and running on most Web servers. Moreover, IIS is capable of responding to requests from remote clients who want to activate objects on the server using the .NET remoting infrastructure. Using IIS as an activation agent has several advantages:

  • You don’t have to write a server application to register remotable classes and listen for activation requests; IIS is the server application.

  • You can use IIS to authenticate remote callers and also to safeguard data with Secure Sockets Layer (SSL).

  • You can use IIS for port negotiation. If you deploy two conventional server applications on the same machine, it’s up to you to ensure that the applications use different port numbers. With IIS as the host, however, IIS picks the port numbers, which simplifies deployment and administration.

IIS supports both server-activated objects and client-activated objects. Classes remoted with IIS’s help can be registered programmatically (in Global.asax) or declaratively (in Web.config). The following Web.config file registers the Clock class in Example 15-2 for remote activation using IIS:

<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown mode="SingleCall" type="Clock, ClockServer"
          objectUri="Clock.rem" />
      </service>
    </application>
  </system.runtime.remoting>
</configuration>

Note Clock’s URI: Clock.rem. URIs registered with IIS must end in .rem or .soap because both extensions are mapped to Aspnet_isapi.dll in the IIS metabase and to the .NET remoting subsystem in Machine.config. You can register additional extensions if you’d like, but these are the only two that work out of the box.

Objects activated with IIS always use HTTP channels to communicate with remote clients. Clients must also register HTTP channels. Here’s how a client would create a Clock instance, assuming Clock resides in a virtual directory named MyClock on the local machine:

HttpClientChannel channel = new HttpClientChannel ();
ChannelServices.RegisterChannel (channel);
RemotingConfiguration.RegisterWellKnownClientType
    (typeof (Clock), "http://localhost/MyClock/Clock.rem");
Clock clock = new Clock ();

Notice that no port number is specified anywhere—neither on the client nor on the server. IIS picks the port numbers.

Client-activated objects are registered and activated differently. This Web.config file registers Clock as a client-activated object rather than a server-activated one:

<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <activated type="Clock, ClockServer" />
      </service>
    </application>
  </system.runtime.remoting>
</configuration>

And here’s how a remote client would activate it, once more assuming Clock resides in a virtual directory named MyClock on the local machine:

HttpClientChannel channel = new HttpClientChannel ();
ChannelServices.RegisterChannel (channel);
RemotingConfiguration.RegisterActivatedClientType
    (typeof (Clock), "http://localhost/MyClock");
Clock clock = new Clock ();

Be aware that IIS client activation requires the remotable class to be hosted in a virtual directory other than wwwroot. You can’t use IIS to activate a client-activated object by putting a Web.config file registering the class in wwwroot and the DLL that implements the class in wwwrootin. Instead, you must install Web.config in a separate virtual directory (for example, MyClock) and the DLL in the bin subdirectory (MyClockin). Only then will IIS activate a client-activated object.

HTTP Channels and Binary Formatters

One drawback to using IIS as an activation agent is that you have no choice but to use HTTP channels to link application domains. HTTP is a higher-level protocol than TCP and is also less efficient on the wire. Furthermore, HTTP channels encode calls as SOAP messages, which increases the verbosity of message traffic.

Fortunately, the .NET remoting infrastructure uses a pluggable channel architecture that lets you choose the channel type as well as the format in which messages are encoded by the chosen channel. IIS supports only HTTP channels, but it doesn’t require the channel to encode calls as SOAP messages. HTTP channels use SOAP by default because they use formatters named SoapClientFormatterSinkProvider and SoapServerFormatterSinkProvider to serialize and deserialize messages. You can replace these formatters with instances of BinaryClientFormatterSinkProvider and BinaryServerFormatterSinkProvider and encode messages in a more compact binary format. Binary messages utilize network bandwidth more efficiently and still allow you to use IIS as the activation agent.

The following Web.config file registers Clock to be activated by IIS as a single-call server-activated object. It also replaces the default SOAP formatter with a binary formatter on the server side. Changes are highlighted in bold:

<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown mode="SingleCall" type="Clock, ClockServer"
          objectUri="Clock.rem" />
      </service>
      <channels>
        <channel ref="http server">
          <serverProviders>
            <formatter ref="binary" />
          </serverProviders>
        </channel>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

A client that wants to activate instances of Clock that are registered in this way must pair a client-side HTTP channel with a binary formatter too. The following example demonstrates how a client can configure the channel programmatically and then activate a remote instance of Clock:

HttpClientChannel channel = new HttpClientChannel
    ("HttpBinary", new BinaryClientFormatterSinkProvider ());
ChannelServices.RegisterChannel (channel);
RemotingConfiguration.RegisterWellKnownClientType
    (typeof (Clock), "http://localhost/MyClock/Clock.rem");
Clock clock = new Clock ();

A client that prefers declarative configuration would do it this way instead:

RemotingConfiguration.Configure ("Client.exe.config");
Clock clock = new Clock ();

Here are the contents of Client.exe.config:

<configuration>
  <system.runtime.remoting>
    <application>
      <client>
        <wellknown type="Clock, ClockServer"
          url="http://localhost/MyClock/Clock.rem" />
      </client>
      <channels>
        <channel ref="http client">
          <clientProviders>
            <formatter ref="binary" />
          </clientProviders>
        </channel>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

Combining HTTP channels with binary formatters lets you have your cake and eat it too. Using similar tactics, you could combine TCP channels with SOAP formatters to encode message traffic as SOAP messages. You can even build formatters of your own and plug them into existing channels. The modular nature of the .NET Framework’s remoting infrastructure makes all sorts of interesting extensions possible without requiring you to write a ton of code or replace portions of the framework that you have no desire to replace.

Delegates and Events

One of the hallmarks of the .NET Framework’s type system is that it elevates events to first-class type members along with methods, properties, and fields. Better still, the framework’s event infrastructure works with remote objects as well as local objects. A client connects event handlers to events fired by remote objects using the very same syntax that it uses to connect handlers to events fired by local objects. The only catch is that the client must register a server channel as well as a client channel so that it can receive event callbacks. By the same token, the server, which normally registers only a server channel, must register a client channel too so that it can fire events to remote clients.

Suppose you built a Clock class that fires a NewHour event at the top of every hour. Here’s how that class—and a delegate defining the signature of NewHour handlers—might be declared:

public delegate void NewHourHandler (int hour);

public class Clock : MarshalByRefObject
{
    public event NewHourHandler NewHour;
      ...
}

Here’s a Web.config file that registers Clock for remote activation as a server-activated singleton using IIS:

<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown mode="Singleton" type="Clock, ClockServer"
          objectUri="Clock.rem" />
      </service>
      <channels>
        <channel ref="http" />
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

Note the ref attribute accompanying the channel element. The value “http” instantiates a two-way HttpChannel object instead of a one-way HttpServerChannel. The two-way channel is necessary if Clock is to receive calls from remote clients and fire events to them as well.

Here’s client code to create a Clock instance and register a handler for NewHour events:

RemotingConfiguration.Configure ("Client.exe.config");
Clock clock = new Clock ();
clock.NewHour += new NewHourHandler (OnNewHour);
  .
  .
  .
public void OnNewHour (int hour)
{
    // NewHour event received
}

And here’s the CONFIG file referenced in the client’s code, which assumes that Clock is deployed in a virtual directory named MyClock:

<configuration>
  <system.runtime.remoting>
    <application>
      <client>
        <wellknown type="Clock, ClockServer"
          url="http://localhost/MyClock/Clock.rem" />
      </client>
      <channels>
        <channel ref="http" port="0" />
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

The client also registers a two-way HttpChannel, and it specifies a port number of 0. The 0 configures the channel to listen for callbacks but permits the .NET Framework to pick the port number.

A client that receives events from remote objects must have access to metadata describing the objects. In addition, the objects must have metadata describing the client—at least the client components that contain the callback methods. The practical implication is that if you deploy clients on one machine and remote classes on another, you need to put the clients’ binaries in the directory that holds the server components and vice versa.

Asynchronous Method Calls

By default, calls to remote objects are synchronous. A thread that places a synchronous call blocks until the call returns. If the call takes a long time to find its way to the recipient or the recipient takes a long time to process the call once it arrives, the caller waits for a long time as well.

That’s why the .NET Framework supports asynchronous method calls. Asynchronous method calls aren’t limited to remote objects; they work with local objects too. They’re enacted through asynchronous delegates, which make placing asynchronous calls almost as easy as placing synchronous ones. Asynchronous calls return immediately, no matter how long they take to reach their recipients or how long the recipients take to process them.

Suppose a remote object has a CountPrimes method similar to the one in Example 14-2. Counting primes is a CPU-intensive task that can take a long time to complete. Calling CountPrimes as in the following takes more than 10 seconds on my PC—a 1.4 GHz Pentium with 384 MB of memory:

int count = sieve.CountPrimes (100000000);

If called through an asynchronous delegate, however, CountPrimes returns immediately. To call CountPrimes asynchronously, you first declare a delegate whose signature matches CountPrimes’ signature, as shown here:

delegate int CountPrimesDelegate (int max);

You then wrap an instance of the delegate around CountPrimes and call the delegate’s BeginInvoke method:

CountPrimesDelegate del = new CountPrimesDelegate (sieve.CountPrimes);
IAsyncResult ar = del.BeginInvoke (100000000, null, null);

To retrieve the value that CountPrimes returns, you later complete the call by calling the delegate’s EndInvoke method and passing in the IAsyncResult returned by BeginInvoke:

int count = del.EndInvoke (ar);

If CountPrimes hasn’t returned when EndInvoke is called, EndInvoke blocks until it does. Calling BeginInvoke and EndInvoke in rapid succession is morally equivalent to calling CountPrimes synchronously.

Can a client determine whether an asynchronous call has completed before calling EndInvoke? You bet. The IsCompleted property of the IAsyncResult that BeginInvoke returns is true if the call has completed, false if it has not. The following code snippet calls EndInvoke if and only if the call has completed:

CountPrimesDelegate del = new CountPrimesDelegate (sieve.CountPrimes);
IAsyncResult ar = del.BeginInvoke (100000000, null, null);
  .
  .
  .
if (ar.IsCompleted) {
    int count = del.EndInvoke (ar);
}
else {
    // Try again later
}

A client can also use IAsyncResult’s AsyncWaitHandle property to retrieve a synchronization handle. A thread that calls WaitOne on that handle blocks until the call completes.

A client can also ask to be notified when an asynchronous call completes. Completion notifications enable a client to learn when a call completes without polling IsCompleted. The basic strategy is to wrap a callback method in an instance of System.AsyncCallback and pass the resulting delegate to BeginInvoke. When the call completes, the callback method is called. Here’s an example:

CountPrimesDelegate del = new CountPrimesDelegate (sieve.CountPrimes);
AsyncCallback ab = new AsyncCallback (PrimesCounted);
IAsyncResult ar = del.BeginInvoke (100000000, ab, null);
  .
  .
  .
void PrimesCounted (IAsyncResult ar)
{
    // CountPrimes completed
}

After the callback method is called, you still need to complete the call by calling EndInvoke. Only by calling EndInvoke can you get the results of the call and let the system clean up after itself following a successful asynchronous call. You can call EndInvoke from inside the callback method if you’d like.

One-Way Methods

The .NET remoting infrastructure supports a slightly different form of asynchronous method calls that rely on entities known as one-way methods. A one-way method has input parameters only—no out or ref parameters are allowed—or no parameters at all, and it returns void. You designate a method as a one-way method by tagging it with a OneWay attribute:

[OneWay]
public void LogError (string message)
{
  ...
}

OneWay is shorthand for OneWayAttribute, which is an attribute class defined in the System.Runtime.Remoting.Messaging namespace.

Calls to one-way methods execute asynchronously. You don’t get any results back, and you aren’t notified if the method throws an exception. You don’t even know for sure that a one-way method call reached the recipient. One-way methods let you place calls using “fire-and-forget” semantics, which are appropriate when you want to fire off a method call, you don’t want any results back, and the method’s success or failure isn’t critical to the integrity of the application as a whole. Sound intriguing? The application in the next section uses one-way methods to fire notifications to remote objects without affecting the liveliness of its user interface. As you work with .NET remoting, you’ll probably find additional uses for one-way methods.

Putting It All Together: The NetDraw Application

Let’s close with an application that assembles many of the concepts outlined in this chapter into one tidy package. The application is shown in Figure 15-5. It’s a distributed drawing program that links clients together so that sketches drawn in one window appear in the other windows, too. Before you try it, you need to deploy it. Here’s how:

  1. Build the application’s binaries. Here are the commands:

    csc /t:library paperserver.cs
    csc /t:winexe /r:paperserver.dll netdraw.cs

    These commands produce binaries named PaperServer.dll and NetDraw.exe. PaperServer.dll implements a remotable class named Paper and a utility class named Stroke. It also declares a delegate that clients can use to wrap handlers for the NewStroke events that Paper fires. NetDraw.exe is a Windows Forms application that serves as a remote client to Paper objects.

  2. Create a virtual directory named NetDraw on your Web server. Copy Web.config to the NetDraw directory. Create a bin subdirectory in the NetDraw directory and copy both NetDraw.exe and PaperServer.dll to bin.

  3. Create another directory somewhere on your Web server (it needn’t be a virtual directory) and copy NetDraw.exe, NetDraw.exe.config, and PaperServer.dll to it.

Now start two instances of NetDraw.exe and scribble in one of them by moving the mouse with the left button held down. Each time you release the button, the stroke that you just drew should appear in the other NetDraw window. If you’d like to try it over a network, simply move NetDraw.exe, NetDraw.exe.config, and PaperServer.dll to another machine and modify the URL in NetDraw.exe.config to point to the remote server.

The NetDraw application.
Figure 15-5. The NetDraw application.

NetDraw, whose source code appears in Example 15-6, demonstrates several important principles of .NET remoting. Let’s tackle the big picture first. At startup, the client—NetDraw.exe—instantiates a Paper object and registers a handler for the object’s NewStroke events:

VirtualPaper = new Paper ();
NewStrokeHandler = new StrokeHandler (OnNewStroke);
VirtualPaper.NewStroke += NewStrokeHandler;

Upon completion of each new stroke drawn by the user (that is, when the mouse button comes up), NetDraw calls the Paper object’s DrawStroke method and passes in a Stroke object containing a series of x-y coordinate pairs describing the stroke:

VirtualPaper.DrawStroke (CurrentStroke);

DrawStroke, in turn, fires a NewStroke event to all clients that registered handlers for NewStroke events. It passes the Stroke provided by the client in the event’s parameter list:

public void DrawStroke (Stroke stroke)
{
    if (NewStroke != null)
        NewStroke (stroke);
}

The event activates each client’s OnNewStroke method, which adds the Stroke to a collection of Stroke objects maintained by each individual client and draws the stroke on the screen. Because Paper is registered as a singleton, all clients that call new on it receive a reference to the same Paper object. Consequently, a stroke drawn in one client is reported immediately to the others.

That’s the view from 10,000 feet. The meat, however, is in the details. Here are some highlights to look for as you peruse the source code:

  • Web.config registers the Paper class so that it can be activated remotely. Activation is performed by IIS. Web.config registers a two-way HTTP channel accompanied by binary formatters. The two-way channel enables Paper to receive calls from its clients and fire events to them as well. The binary formatters increase the channel’s efficiency on the wire.

  • At startup, NetDraw registers a channel of its own and registers Paper in the local application domain using NetDraw.exe.config. That file contains the remote object’s URL (http://localhost/NetDraw/Paper.rem). It also registers a two-way HTTP channel with binary formatters so that the client can place calls to remote Paper objects and receive events fired by those objects.

  • Paper’s DrawStroke method is a one-way method, enabling clients to fire off calls to it without waiting for the calls to return. Synchronous calls to DrawStroke would produce a sluggish user interface. Imagine 1000 instances of NetDraw connected over a slow network and you’ll see what I mean.

  • Callbacks emanating from events fired by remote objects execute on threads provided by the .NET Framework. To make sure that its main thread can’t access Strokes while OnNewStroke updates it (or vice versa), NetDraw uses C#’s lock keyword to synchronize access to Strokes.

  • Stroke objects accompany DrawStroke methods and NewStroke events as method parameters. So that instances of Stroke can travel between application domains (and, by extension, between machines), the Stroke class includes a Serializable attribute. That enables the .NET Framework to serialize a Stroke into the channel and rehydrate it on the other side. Stroke is a marshal-by-value (MBV) class, whereas Paper is marshal-by-reference. MBV means no proxies are created; instead, the object is copied to the destination.

  • The Paper class overrides InitializeLifetimeService and returns null so that Paper objects won’t disappear if clients go for 5 minutes without calling them. As an alternative, Paper could simply set its lease’s RenewOnCallTime property to something greater than 5 minutes—say, an hour.

If NetDraw.cs looks familiar to you, that’s because it’s almost identical to Chapter 4’s NetDraw.cs. With a little help from .NET remoting, it didn’t take much to turn a stand-alone application into a distributed application. You provide client and server components; the framework provides the plumbing that connects them. That’s what .NET remoting is all about.

Example 15-6. Source code for a distributed drawing application.

PaperServer.cs

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.Remoting.Messaging;
using System.Collections;

public delegate void StrokeHandler (Stroke stroke);

public class Paper : MarshalByRefObject
{
    public event StrokeHandler NewStroke;

    public override object InitializeLifetimeService ()
    {
        return null;
    }

    [OneWay]
    public void DrawStroke (Stroke stroke)
    {
        if (NewStroke != null)
            NewStroke (stroke);
    }
}

[Serializable]
public class Stroke
{
    ArrayList Points = new ArrayList ();

    public int Count
    {
        get { return Points.Count; }
    }

    public Stroke (int x, int y)
    {
        Points.Add (new Point (x, y));
    }

    public void Add (int x, int y)
    {
        Points.Add (new Point (x, y));
    }

    public void Draw (Graphics g)
    {
        Pen pen = new Pen (Color.Black, 8);
        pen.EndCap = LineCap.Round;
        for (int i=0; i<Points.Count - 1; i++)
            g.DrawLine (pen, (Point) Points[i], (Point) Points[i + 1]);
        pen.Dispose ();
    }

    public void DrawLastSegment (Graphics g)
    {
        Point p1 = (Point) Points[Points.Count - 2];
        Point p2 = (Point) Points[Points.Count - 1];
        Pen pen = new Pen (Color.Black, 8);
        pen.EndCap = LineCap.Round;
        g.DrawLine (pen, p1, p2);
        pen.Dispose ();
    }
}

Web.config

<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown mode="Singleton" type="Paper, PaperServer"
          objectUri="Paper.rem" />
      </service>
      <channels>
        <channel ref="http">
          <clientProviders>
            <formatter ref="binary"/>
          </clientProviders>
          <serverProviders>
            <formatter ref="binary"/>
          </serverProviders>
        </channel>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

NetDraw.cs

using System;
using System.Collections;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.Remoting;
using System.ComponentModel;

class MyForm : Form
{
    Paper VirtualPaper;
    Stroke CurrentStroke = null;
    ArrayList Strokes = new ArrayList ();
    StrokeHandler NewStrokeHandler;

    MyForm ()
    {
        Text = "NetDraw";

        try {
            // Configure the remoting infrastructure
            RemotingConfiguration.Configure ("NetDraw.exe.config");

            // Create a remote Paper object
            VirtualPaper = new Paper ();

            // Connect a handler to the object’s NewStroke events
            NewStrokeHandler = new StrokeHandler (OnNewStroke);
            VirtualPaper.NewStroke += NewStrokeHandler;
        }
        catch (Exception ex) {
            MessageBox.Show (ex.Message);
            Close ();
        }
    }

    protected override void OnPaint (PaintEventArgs e)
    {
        lock (Strokes.SyncRoot) {
            // Draw all currently recorded strokes
            foreach (Stroke stroke in Strokes)
                stroke.Draw (e.Graphics);
        }
    }

    protected override void OnMouseDown (MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left) {
            // Create a new Stroke and assign it to CurrentStroke
            CurrentStroke = new Stroke (e.X, e.Y);
        }
    }

    protected override void OnMouseMove (MouseEventArgs e)
    {
        if ((e.Button & MouseButtons.Left) != 0 &&
            CurrentStroke != null) {
            // Add a new segment to the current stroke
            CurrentStroke.Add (e.X, e.Y);
            Graphics g = Graphics.FromHwnd (Handle);
            CurrentStroke.DrawLastSegment (g);
            g.Dispose ();
        }
    }

    protected override void OnMouseUp (MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left && CurrentStroke != null) {
            // Complete the current stroke
            if (CurrentStroke.Count > 1) {
                // Let other clients know about it, too
                VirtualPaper.DrawStroke (CurrentStroke);
            }
            CurrentStroke = null;
        }
    }

    protected override void OnKeyDown (KeyEventArgs e)
    {
        if (e.KeyCode == Keys.Delete) {
            // Delete all strokes and repaint
            lock (Strokes.SyncRoot) {
                Strokes.Clear ();
            }    
            Invalidate ();
        }
    }

    protected override void OnClosing (CancelEventArgs e)
    {
        // Disconnect event handler before closing
        base.OnClosing (e);
        VirtualPaper.NewStroke -= NewStrokeHandler;
    }

    public void OnNewStroke (Stroke stroke)
    {
        // Record and display a stroke drawn in a remote client
        lock (Strokes.SyncRoot) {
            Strokes.Add (stroke);
        }
        Graphics g = Graphics.FromHwnd (Handle);
        stroke.Draw (g);
        g.Dispose ();
    }

    static void Main ()
    {
        Application.Run (new MyForm ());
    }
}

NetDraw.exe.config

<configuration>
  <system.runtime.remoting>
    <application>
      <client>
        <wellknown type="Paper, PaperServer"
          url="http://localhost/NetDraw/Paper.rem" />
      </client>
      <channels>
        <channel ref="http" port="0">
          <clientProviders>
            <formatter ref="binary" />
          </clientProviders>
          <serverProviders>
            <formatter ref="binary" />
          </serverProviders>
        </channel>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

Microsoft .NET: A Whole New Ball Game

That does it for .NET remoting. That does it for this book, too; you’ve reached the end. You’ve seen up close and personal what programming with the .NET Framework is like. If there’s one thing that should be apparent to you, it’s that Microsoft .NET changes the rules of the game and provides a whole new paradigm for writing and executing code. The old ways have passed away. New ways have taken their place.ftp://ftp.isi.edu/in-notes/rfc2617.txt

As you embark upon your new career as a Microsoft .NET programmer, keep in mind that the .NET Framework isn’t standing still. As you read this, programmers in Redmond, Washington, are busy adding new features and figuring out what the framework must do to adapt to the needs not only of today’s developers, but also of tomorrow’s. Keep an eye on the Microsoft .NET home page—http://www.microsoft.com/net—for late-breaking news.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.239.182