
Introduction
Do you have two computers at your desk? If so, have you ever wanted to
Ctrl-C on one computer and Ctrl-V on the other? It's especially tempting
to do this when the two computers share a common keyboard and monitor via a KVM
switch. This application makes it easy to share the clipboard between two
computers on a local network. The application is written in C# using .NET
remoting. This article will explain the remoting implementation and the
clipboard processing, as well as explaining the use of a delegate to get around
a problem with threading.
Using the Application
ClipShare acts as both a client and server, so it must be running on both
computers that will be sharing the clipboard. On the computer that has
the clipboard data that you want to share (the client), specify the name of the
computer you want to send to and then select the "Send Clipboard" button.
The clipboard will be packaged up, sent over the LAN, and automatically placed
on the clipboard of the computer at the receiving end (the server). For
convenience, the application has an icon in the system tray so you can also
select "send clipboard" from the icon's context menu. If you want to
temporarily disallow other computers from sending their clipboard to your
computer, deselect the "allow incoming" checkbox or context menu item.
Setting Up .NET Remoting
This application uses .NET remoting in order to transfer the clipboard
contents. First we need to do some initialization to make the application
a remoting server. This code snippet (from the form's constructor)
enables remoting and then registers a class, RemoteClipboard (discussed
later), with the RemotingConfiguration so that it can be activated
remotely:
TcpChannel channel = new TcpChannel(4820);
hannelServices.RegisterChannel(channel);
RemotingConfiguration.RegisterWellKnownServiceType(typeof(RemoteClipboard),
"ClipShare",
WellKnownObjectMode.Singleton);
This application also serves as a remoting client. To send the clipboard
data, the client activates an instance of the RemoteClipboard
class on the server using the same port number and service name that was
registered above. We will use this instance to invoke the SendClipboard
method, so we save it in a member variable for use later:
private void InitRemoteObject()
{
try
{
string location = "tcp://" + computerName.Text + ":4820/ClipShare";
m_remoteClipboard = (RemoteClipboard) Activator.GetObject(
typeof(RemoteClipboard), location);
computerName.Modified = false;
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
The RemoteClipboard Class
The RemoteClipboard class is activated remotely and used to transfer the
clipboard contents from one computer to the other. It must inherit from
MarshalByRefObject so that it can be activated remotely. It
has one method, SendClipboard, that takes an ArrayList
containing the clipboard data passed in from the client, and places it on the
server's clipboard.
Problems Right off the Bat
Unfortunately, the SendClipboard method isn't allowed to place
data on the clipboard. The method from the .NET framework used to set the
contents of the clipboard, Clipboard.SetDataObject(), can only be
run in a Single Threaded Apartment (STA), but since the RemoteClipboard
object is activated remotely, it's running in a Multi Threaded Apartment
(MTA). If you try to call Clipboard.SetDataObject() directly
from SendClipboard, an exception is thrown.
Delegates to the Rescue
In order to get around this problem, we need to have the form place the data on
the clipboard instead of the RemoteClipboard object. Since
Form.Main() is declared with the [STAThread] attribute,
it can call SetDataObject without any problem. To get the
form to process the data in its own thread, we need to call the Invoke
method on the form, passing in a delegate to one of the form's
methods (AddToClip). The RemoteClipboard class
has a static method, SetOnClipReceive, called once during
initialization, to give it the form and delegate to use when calling Invoke.
Here's a picture to help describe what's going on conceptually, followed
by the RemoteClipboard class in its entirety:

public delegate void ClipEventHandler(ArrayList clipData);
public class RemoteClipboard : MarshalByRefObject
{
private static ClipEventHandler m_OnClipReceive;
private static Form m_receiverForm;
public static void SetOnClipReceive(Form receiver, ClipEventHandler theCallback)
{
m_receiverForm = receiver;
m_OnClipReceive = theCallback;
}
public void SendClipboard(ArrayList clipData)
{
object[] clipObjects = {clipData};
m_receiverForm.Invoke(m_OnClipReceive, clipObjects);
}
}
The SetOnClipReceive function is called from the form's constructor:
RemoteClipboard.SetOnClipReceive(this, new ClipEventHandler(this.AddToClip));
Special note to Forms developers: I included the RemoteClipboard
class in the same source file as the form since it is small and convenient to
do so. I originally had the RemoteClipboard class defined above
the form in the source file. As soon as I did this (although I never made
that connection), the form could no longer access its resources, such as the
icon for the system tray. It took me quite awhile to figure out that
the Form must be defined first in the source file. According
to Microsoft, this is by design (see
Q318603).
Packaging up the Clipboard Contents
To access the clipboard data, use Clipboard.GetDataObject(), which
returns an instance of the IDataObject interface.
Unfortunately, this object is not serializable, so it can't be passed as a
parameter to the SendClipboard method. Instead, we iterate through
each format on the clipboard, and if it is serlializable, put the clipboard
data item in an array list, paired with its format string. The ArrayList
is then passed to the RemoteClipboard object using the SendClipboard
method.
private void SendClipboardToRemote()
{
try
{
...
ArrayList dataObjects = new ArrayList();
IDataObject clipboardData = Clipboard.GetDataObject();
string[] formats = clipboardData.GetFormats();
for (int i=0; i < formats.Length; i++)
{
object clipboardItem = clipboardData.GetData(formats[i]);
if (clipboardItem != null && clipboardItem.GetType().IsSerializable)
{
Console.WriteLine("sending {0}", formats[i]);
dataObjects.Add(formats[i]);
dataObjects.Add(clipboardItem);
}
else
Console.WriteLine("ignoring {0}", formats[i]);
}
if (dataObjects.Count > 0)
{
Cursor.Current = Cursors.WaitCursor;
m_remoteClipboard.SendClipboard(dataObjects);
Cursor.Current = Cursors.Default;
}
else
MessageBox.Show(this, "Nothing on clipboard, or contents not supported",
"ClipShare");
}
catch (Exception ex)
{
string message = String.Format("Unable to send data: {0}", ex.Message);
MessageBox.Show(this, message, "ClipShare");
}
}
Receiving the Clipboard
On the receive end, we iterate through the array list and add each clipboard
data item to a new DataObject, which then gets placed on the
clipboard via Clipboard.SetDataObject. The AddToClip
method shown here is the delegate that gets invoked by the SendClipboard
method (see above):
public void AddToClip(ArrayList theData)
{
if (!allowIncomingCB.Checked)
throw new Exception("Remote computer has disabled clipboard sharing");
try
{
DataObject dataObj = new DataObject();
for (int i = 0; i < theData.Count; i++)
{
string format = (string)theData[i++];
dataObj.SetData(format, theData[i]);
}
Clipboard.SetDataObject(dataObj, true);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
You may have noticed that the call to SendClipboard by the client
is in a try/catch block, so the exception that is thrown in AddToClip
on the server: ("Remote computer has disabled clipboard sharing") will get
propagated back to the client and shown in a message box. Also
interesting to note, but not surprising, is that if AddToClip
is invoked asynchronously with BeginInvoke instead of Invoke,
the exception will not get propagated and you will get an unhandled exception
error.
Limitations
Unfortunately, not all formats retrieved from the IDataObject are
serializable. For example, the windows metafile format is not, so
transferring to or from drawing programs is limited to bitmap formats.
Also, if you copy a file or directory, the location placed on the clipboard
uses drive letters instead of UNC so they can't be pasted on the remote
computer. I imagine that it wouldn't be hard to add pre-processing to
change the path to use UNC before the clipboard is sent. Finally, I made
a half-hearted attempt at getting a left mouse click on the system tray icon to
show the context menu in addition to a right mouse click (this is
commented out in the source file if you download it). This doesn't seem
to be directly supported by the NotifyIcon class, so is not easily
done.
Conclusion
When I started writing this application, I thought it would serve as a quick
introduction to remoting, but as is often the case, especially when coming up
to speed on a new programming environment, it turned out to take longer than I
expected. But running into problems isn't all bad since solving them is
part of the learning process. (Next time I'll know not to define a class above
my form!)