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 explain 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 a while 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!)
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.