Introduction
NOTE: This article uses Remoting, a legacy .NET technology that has been replaced by Windows Communication Foundation (WCF).
Most introductory articles about remoting focus on console-based applications, but frequently beginners ask, "How can I do remoting with a GUI?". I was asking the same question several months ago, but I couldn't find any useful introductory code samples on the Web that adequately explained how remoting should be done using a GUI, especially on the server. This article is an attempt to fill that gap.
There are a few introductory articles about remoting that do use GUIs and could be very useful to you, but they do not address all the topics discussed in this article, and only the third article shows a GUI being used for the server.
Prerequisites
This article will give you a good idea of how to use .NET remoting in a GUI application. You do not need to know a whole lot about remoting before reading this article, but you should be familiar with remoting basics- you should understand how a basic remoting console-based application works. A nice intro is at gotdotnet.com.
There are many design considerations when creating a distributed application using remoting, and I suggest you do a lot more research on the topic before creating any full-blown applications with it. A great resource is Advanced .NET Remoting 2nd Edition (Ingo Rammer and Mario Szpuszta, Apress, March 2005). Ingo goes into depth regarding remoting events and delegates which are common in GUI applications. He also provides some very useful advice regarding GUI usage with remoting in his article PRB: GUI application hangs when using non-[OneWay]-Events. This article is meant only as a simple introduction and only illustrates synchronous and asynchronous calls.
High Level Design
Let's start with a high level look at the application we are going to develop. A Client application will make calls on a remote object called Greeter which is housed in the Server's application space. The Server will register itself with the Greeter to be notified when a call is made on the Greeter. The Greeter will notify the Server of the call, and the Server will display the information to its GUI.
Greeter Remote Object
The Greeter
is our remote object, and like all remote objects, it inherits from MarshalByRefObject
. Its lease does not expire since it returns null
to InitializeLifetimeService
. We'll see the Greeter
being instantiated by the Server later on.
using System;
using System.Threading;
namespace guiexample
{
public class Greeter : MarshalByRefObject
{
public delegate void HelloEventHandler(object sender, HelloEventArgs e);
public event HelloEventHandler HelloEvent;
private int mRespondTime;
public int RespondTime
{
get { return mRespondTime; }
set { mRespondTime = Math.Max(0, value); }
}
public Greeter()
{
}
public override Object InitializeLifetimeService()
{
return null;
}
public String SayHello(String name)
{
if (HelloEvent != null)
HelloEvent(this, new HelloEventArgs(name));
Thread.Sleep(mRespondTime);
return "Hello there, " + name + "!";
}
}
}
The Greeter
only implements a single method that can be called remotely: SayHello
. It also has a property called RespondTime
which will be used by the Server to set a delay when responding to a SayHello
call. We're going to delay our response to simulate a slow-returning call and see how it affects the Client's GUI.
The SayHello
method first raises a HelloEvent
when it is called. This will alert any HelloEvent
listeners to who has called this method. The Server will need to register an event handler for the HelloEvent
so it can update its GUI. (Notice that we first checked for null
just in case the Server "forgot" to register for the event.) This allows the Server and Greeter
to be decoupled. This also matches the Observer design pattern where the Greeter
is the subject being observed and the Server is the observer. Any number of events that the observer is interested in could be raised by our Greeter
.
Server
In the Server
's constructor the TCP channel using port 50050 is first created, and then the Greeter
is instantiated and registered with RemotingServices.Marshal
so it can be accessed by our Client. The Server
is interested in knowing when the Greeter
's SayHello
method is called, so it registers a listener for the HelloEvent
event. You will notice that we have not used RemotingConfiguration.RegisterWellKnownServiceType
to register the Greeter
as a Singleton because the Server
needs to actually use the Greeter
itself, not just expose it to clients.
public Server()
{
InitializeComponent();
ChannelServices.RegisterChannel(new TcpChannel(50050));
rmGreeter = new Greeter();
ObjRef refGreeter = RemotingServices.Marshal(rmGreeter, "Greeter");
rmGreeter.RespondTime = Convert.ToInt32(txbRespond.Text);
rmGreeter.HelloEvent += new Greeter.HelloEventHandler(Server_HelloEvent);
}
The Server
's Server_HelloEvent
method is called when the Greeter
raises the HelloEvent
event. This is the Server
's chance to update its GUI. It would be nice if we could update our GUI like this:
private void Server_HelloEvent(object sender, HelloEventArgs e)
{
lblHello.Text = "Saying hello to " + e.Name;
}
But this could cause a deadlock. The reason is because the event is running in a thread that didn't create the Label
control. The control can only be updated with the UI thread. So instead we will call BeginInvoke
on the control and give it a delegate which will run asynchronously on the UI thread that created the control. This is necessary when updating GUI controls in a multithreaded environment. We'll do the same thing later on when updating the client's GUI.
For more information on using Control.BeginInvoke
to update GUI controls, see S. Senthil Kumar's article What's up with BeginInvoke? and Ingo's article PRB: GUI application hangs when using non-[OneWay]-Events.
private delegate void SetLabelTextDelegate(string text);
private void SetLabelText(string text)
{
lblHello.Text = text;
}
private void Server_HelloEvent(object sender, HelloEventArgs e)
{
string text = "Saying hello to " + e.Name;
this.BeginInvoke(new SetLabelTextDelegate(SetLabelText), new object[] {text});
}
The HelloEventArgs
supplies the information needed to update the GUI. In this case we are only interested in who has called the SayHello
method, but we could have enhanced HelloEventArgs
to contain error messages, timestamps, etc. The following is our HelloEventArgs
class which has a read-only property called Name
:
public class HelloEventArgs : EventArgs
{
private string mName;
public string Name
{
get { return mName; }
}
public HelloEventArgs(string name)
{
mName = name;
}
}
Our Server
uses a text box to allow us to control the amount of time before our remote object responds to the client. We don't want the user to enter just any value in the text box, so we perform some data validation using the TextChanged
event before setting the RespondTime
.
private void txbRespond_TextChanged(object sender, System.EventArgs e)
{
try
{
int delay = Convert.ToInt32(txbRespond.Text);
if (delay >= 0)
rmGreeter.RespondTime = delay;
}
catch (Exception) {}
}
Client
The Client
creates a TCP channel in its constructor and obtains a reference to the Greeter
object made available by the Server
.
public Client()
{
InitializeComponent();
ChannelServices.RegisterChannel(new TcpChannel());
rmGreeter = (Greeter)Activator.GetObject(
typeof(guiexample.Greeter), "tcp://localhost:50050/Greeter");
lblResult.Text = "";
}
The Client
has two buttons for calling the remote method SayHello
. The first button, Call Synchronously, uses a synchronous call to SayHello
. It is a blocking call - it locks the current thread until a response is returned by the Greeter
. This is not the ideal situation in a GUI because the GUI must continue to listen for messages from the OS such as button clicks, mouse moves, paint updates, etc. Clicking Call Synchronously will cause the GUI to freeze until SayHello
returns. Try clicking the button and then moving the window before the window is updated, to see what I mean.
We use a try
/catch
block around the call to SayHello
just in case our Server
goes down while we are in the process of making the call or if the Server
was never available to start with. It's always a good idea to encapsulate remote method calls in a try
/catch
to make your application more robust.
private void btnCallSynch_Click(object sender, System.EventArgs e)
{
lblResult.Text = "";
lblResult.Refresh();
try
{
lblResult.Text = rmGreeter.SayHello(txbName.Text);
}
catch (Exception ex)
{
MessageBox.Show(this, "Unable to call SayHello. " +
" Make sure the server is running.\n"
+ ex.Message, "Client", MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
The second button, Call Asynchronously, uses an asynchronous call to SayHello
. Actually it is just using a delegate to simulate a synchronous call, but the net effect is that our GUI is no longer locked when making a remote call. This is probably what you should do anytime your GUI client needs to call a remote method. You'll notice that when we call BeginInvoke
, we pass the text the user has entered into our text box.
As an aside, you may be wondering about OneWay calls. OneWay calls allow you to call a void function without blocking the calling thread. Unfortunately these types of calls can cause serious performance hits when the recipients of these calls go down. For more information on why you should avoid OneWay calls, read the section entitled Why [OneWay] Events Are a Bad Idea from Ingo's book Advanced .NET Remoting.
private void btnCallAsynch_Click(object sender, System.EventArgs e)
{
lblResult.Text = "";
AsyncCallback cb = new AsyncCallback(this.SayHelloCallBack);
SayHelloDelegate d = new SayHelloDelegate(rmGreeter.SayHello);
IAsyncResult ar = d.BeginInvoke(txbName.Text, cb, null);
}
When the SayHello
method completes, the Client
's SayHelloCallBack
method will be called. We can pull out the return value from SayHello
by calling EndInvoke
. Just like the synchronous call to SayHello
, we have placed the call to EndInvoke
in a try
/catch
block in case our Server
wasn't running.
The SayHelloCallBack
method doesn't try to update the Label
directly because it is not running in the UI thread that created the Label
control. It instead calls BeginInvoke
so it will asynchronously call SetLabelText
to change the Label
's text. We did something similar with the Server
.
private delegate void SetLabelTextDelegate(string text);
private void SetLabelText(string text)
{
lblResult.Text = text;
}
public void SayHelloCallBack(IAsyncResult ar)
{
SayHelloDelegate d = (SayHelloDelegate)((AsyncResult)ar).AsyncDelegate;
try
{
string text = (string)d.EndInvoke(ar);
this.BeginInvoke(new SetLabelTextDelegate(SetLabelText),
new object[] {text});
}
catch (Exception ex)
{
MessageBox.Show(this, "Unable to call SayHello." +
" Make sure the server is running.\n" +
ex.Message, "Client", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
Running the Application
Download the application and run the server (server.exe) and the client (client.exe). If you want to run the application on different computers, you'll need to change "localhost" to the names of the computers the client and server are running on, and rebuild the application.
I have provided a make.bat file that uses the C# command-line compiler csc to rebuild the application:
csc /t:library /r:System.Runtime.Remoting.dll Greeter.cs HelloEventArgs.cs
csc /r:Greeter.dll /r:System.Runtime.Remoting.dll
/target:winexe /out:server.exe Server.cs
csc /r:Greeter.dll /r:System.Runtime.Remoting.dll
/target:winexe /out:client.exe Client.cs
Improvements
There are several improvements we could make to our application that don't improve its execution speed but do improve its design: use the Singleton design pattern to ensure the Greeter
is only instantiated once by the Server
, use interfaces to enforce proper use of the Greeter
by the Server
and Client
, and use configuration files to allow for easier changes in deployment.
Singleton Design Pattern
Our Greeter
should only be instantiated once by the Server
. We can control this by using the Singleton design pattern. Jon Skeet discusses implementing the Singleton design pattern in C#.
We need to make the Greeter
's constructor private
so that we can enforce how many times the Greeter
can be instantiated. We'll also provide an Instance
property that the Server
will use to instantiate the Greeter
.
static Greeter mInstance = null;
private Greeter()
{
}
public static Greeter Instance
{
get
{
if (mInstance == null)
mInstance = new Greeter();
return mInstance;
}
}
The Server
will now instantiate the Greeter
like so:
Greeter g = Greeter.Instance;
Now we are assured that the Server
cannot accidentally create several Greeter
s.
Using Interfaces
The current implementation allows a Client
to change the RespondTime
and listen for HelloEvent
s. We may want the Server
and Client
to have separate views of the Greeter
to enforce proper implementation. By creating separate interfaces for the Client
and Server
to Greeting, we can prevent the Client
from having access to RespondTime
and HelloEvent
s.
Let's first create an interface for the Client
that allows the Client
to only call SayHello
.
public interface IClientGreeter
{
String SayHello(String name);
}
Now let's create an interface for the Server
to allow it access to RespondTime
and HelloEvent
.
public interface IServerGreeter
{
int RespondTime
{
get;
set;
}
event Greeter.HelloEventHandler HelloEvent;
}
The Greeter
needs to implement both these interfaces and modify how ResponseTime
and SayHello
are declared:
public class Greeter : MarshalByRefObject, IServerGreeter, IClientGreeter
...
int IServerGreeter.RespondTime
...
String IClientGreeter.SayHello(String name)
...
Now we need to change the way the Client
and Server
access the Greeter
. In the Client
, we change our private
reference from a Greeter
to an IClientGreeter
, and we change how we activate the Greeter
.
private IClientGreeter rmGreeter;
...
rmGreeter = (guiexample.IClientGreeter)Activator.GetObject(
typeof(guiexample.IClientGreeter), "tcp://localhost:50050/Greeter");
We'll make similar code changes on the Server
:
private IServerGreeter rmGreeter;
...
Greeter g = new Greeter();
ObjRef refGreeter = RemotingServices.Marshal(g, "Greeter");
rmGreeter = (IServerGreeter)g;
Now if you tried to set the RespondTime
on the Client
, your code wouldn't compile.
A nice benefit to using interfaces is that you can completely decouple the implementation of the remote object from the client. If we create a DLL for our IClientGreeter
interface, we can deploy just the IClientGreeter.dll to the client. Any changes we make to the Greeter
on the server (keeping the interface intact) will not require any redeployment of code or files to the client.
We would compile our system like so:
csc /t:library /r:System.Runtime.Remoting.dll
Greeter.cs HelloEventArgs.cs IServerGreeter.cs IClientGreeter.cs
csc /t:library /r:System.Runtime.Remoting.dll IClientGreeter.cs
csc /r:Greeter.dll /r:System.Runtime.Remoting.dll
/target:winexe /out:server.exe Server.cs
csc /r:IClientGreeter.dll /target:winexe /out:client.exe Client.cs
The server would need Greeter.dll and server.exe, and the client would need IClientGreeter.dll and client.exe to execute.
Configuration Files
When you are first learning about remoting, it is much easier to understand when all the channel and objects are explicitly created directly in the source code. But later when you want to run your programs on different computers or use different ports, you find that it's a real pain to have to change your code and recompile. That's where configuration files come into play.
Let's create a server.exe.config file for the Server
:
<configuration>
<system.runtime.remoting>
<application>
<channels>
<channel ref="tcp" port="50050" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
Let us replace the RegisterChannel
call with the following in the Server
's constructor:
RemotingConfiguration.Configure("server.exe.config");
And let's create a client.exe.config file for the Client
:
<configuration>
<appSettings>
<add key="GreeterUrl" value="tcp://localhost:50050/Greeter" />
</appSettings>
<system.runtime.remoting>
<application>
<channels>
<channel ref="tcp" port="0" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
This allows us to remove the RegisterChannel
call from the Client
's constructor and to remove the hard-coded URL to the Greeter
remote object:
RemotingConfiguration.Configure("client.exe.config");
Greeter g =
(Greeter)Activator.GetObject(typeof(guiexample.Greeter),
System.Configuration.ConfigurationSettings.AppSettings["GreeterUrl"]);
Now we can deploy our application and just change the config files whenever our server's URL or port number changes.
Conclusion
I've presented some very elementary concepts when using remoting in a GUI environment that I have been unable to find anywhere else on the Web.
Here are the important points to remember when using GUIs with remoting:
- The server should instantiate the remote object and make it accessible to the client using
RemotingServices.Marshal
.
- The remote object should use events to update the server's GUI.
- Asynchronous calls are better in a GUI environment than synchronous calls because asynchronous calls will not lock the GUI while the remote method call completes.
- All updates to the client's GUI and server's GUI should be made with the UI thread using
Control.BeginInvoke
(or a similar method) when completing an asynchronous call or handling an event from the remote object.
- Use interfaces to cleanly separate the client's view of the remote object and the server's view of the remote object.
I hope you'll find this article helpful. Feel free to leave me any constructive criticism.
Revision History
- 06-03-2005
- Added a few more introductory remoting articles to the list.
- Showed how to compile the system using make.bat.
- Checked for
null
before invoking HelloEvent
.
- Improved interfaces section.
- 05-17-2005
- Added compiler switch to make.bat so server and client applications wouldn't open console windows.
- Added "important points" to the conclusion section.
- 05-11-2005
- Changed code to show
Label
updates with BeginInvoke
.
- 05-07-2005