Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Share the Clipboard Using .NET Remoting

0.00/5 (No votes)
14 Oct 2002 1  
Use .NET remoting to send the contents of your clipboard to another computer

Sample Image

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;

    // set the object and method that gets a callback when a clipboard is sent.
    public static void SetOnClipReceive(Form receiver, ClipEventHandler theCallback)
    {
        m_receiverForm = receiver;
        m_OnClipReceive = theCallback;
    }

    // this is the method that will be invoked remotely by the other computer.
    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.

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