Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Windows Clipboard Sharing Through Web Services

  This application transfers the contents of a machine's clipboard to another machine through the user of ASP .NET Web Services.

Difficulty: Easy
Time Required: 1-3 hours
Cost: Free
Software: Visual Basic or Visual C# Express Editions
Hardware: None
Download: Download

Background

Have you ever worked on multiple machines and wished you could copy the clipboard contents from one machine to the other? I've often found myself wishing that there was a quick and easy way to move text snippets, screenshots, or even files to another machine with a simple copy and paste. If this sounds interesting to you then read on.

I wanted this to work regardless of whether both machines were online at the same time, and not be halted by firewalls, NAT's, etc. so I opted for a server based rather than a peer to peer architecture. The architecture involved a client application to transfer the clipboard contents to the server via a web services call, a web service to cache the clipboard contents, and another client component to retrieve the clipboard contents from the server and place them on the local machine's clipboard.

To tackle this problem we need programmatic access to the clipboard on both the machine to copy from and also the machine we want to copy to. Luckily .NET provides a managed wrapper around the native Windows Clipboard API that gives us access to this. The relevant namespaces are Clipboard for C# and my.Computer.Clipboard. Since we're interested in moving clipboard objects from one machine to another we first need to determine what type of objects get placed onto the clipboard for our various actions (copy text, images, and files). Writing a quick code snippet with the Clipboard namespace allows us to iterate through all objects on the clipboard for various types of actions to see what we're dealing with.

C#
        IDataObject clipData = Clipboard.GetDataObject();
 
        //retrieve an array of strings for all the formats available on the clipboard.
        string[] formats = clipData.GetFormats();
 
        //iterate through the list of formats on the clipboard
        foreach (string format in formats)
        {
            //add each objeoct to an arraylist so we can inspect the object types
            object dataObject = clipData.GetData(format);
 
        }
VB
    Dim clipData As IDataObject = Clipboard.GetDataObject
 
    Dim formats() As String = clipData.GetFormats
 
    'iterate through the list of formats on the clipboard
    For Each format As String In formats
    'add each objeoct to an arraylist so we can inspect the object types
          Dim dataObject As Object = clipData.GetData(Format)
    Next

The screenshot below shows the results for having some text from Word copied onto the clipboard. Each format in the string array is a representation of the data on the clipboard. Since the target application (where you're going to paste to) is unknown the clipboard has multiple formats each containing the same data. The objective with this project is to replicate all these formats on the target machine's clipboard.

After inspection of the objects on the Clipboard for text, images, and file objects it was easy to determine that the primary object types we need to be concerned with. These are "System.IO.MemoryStream", “System.IO.FileStream”, "System.Drawing.Bitmap", and "System.String" . Since all this information would be transferred to the server via web services it's simple to serialize all objects to bytes for transmission. This is necessary for a number of reasons including the fact that complex objects such as MemoryStreams cannot be simply serialized and sent over the web service call as Strings can. In addition, some of the objects are larger than the web service call allows, and would need to be broken up into smaller pieces for transmission and then reassembled on the server side in the correct order. Again, when the client requests the clipboard items we'll need to disassemble each object, send it over the web service return result to the client, and then reassemble it.

The first item to build is a base function that handles the breaking up of these large streams into more manageable byte arrays for transmission to the web service. The function below performs this task by sending chunks of the MemoryStream limited in size by the “byteCount” constant. Once this limit is reached the buffer is sent over the web service call for storage and assembly on the server. Once we have either 0 bytes left to send, or a number less than “byteCount” we send the remaining elements of the buffer and signal the web service that this particular object is complete with the “isFinalTransaction” flag.

C#

        private void UploadStreamBlock(string format, string objectType, MemoryStream memStream)
        {
            //each time we enter this function we have a new transaction beginning.  
            //  A transaction represents a comlete
            //object on the clipboard and we'll use this on the server side to know 
            //  how to put the stream back together
            string transactionGuid = System.Guid.NewGuid().ToString();
            memStream.Position = 0;
 
            byte[] buffer = new byte[byteCount];
            bool isFinalTransaction = false;
 
            //while the current stream position plus our byte count is less than the 
            //  length of the stream continue sending as much as we can.
            while ((memStream.Position + byteCount) <= memStream.Length)
            {
                //if we happen to be on the last byte of the stream set the final 
                // transaction flag to true so the server will know that this is the 
                //  last bit of this transaction to expect.
                if (memStream.Position + byteCount == memStream.Length)
                {
                    isFinalTransaction = true;
                }
                //read the stream into our buffer for transmission over the web service.
                memStream.Read(buffer, 0, byteCount);
                ws.InsertMessageStream(buffer, format, objectType, transactionGuid, 
                    isFinalTransaction, clipBoardGUID);
            }
 
            long remainingBytes = memStream.Length - memStream.Position;
            //if we still have remaining bytes left figure out how many and transmit the 
            //  last bit of this ojbect over the web service.
            if ((int)remainingBytes > 0)
            {
                byte[] remainingBuffer = new byte[(int)remainingBytes];
 
                memStream.Read(remainingBuffer, 0, (int)remainingBytes);
                ws.InsertMessageStream(remainingBuffer, format, objectType, 
                    transactionGuid, true, clipBoardGUID);
            }
        }

VB

    Private Sub UploadStreamBlock(ByVal format As String, ByVal objectType As String, _
                                  ByVal memStream As MemoryStream)
        'each time we enter this function we have a new transaction beginning. 
        ' A transaction represents a comlete
        'object on the clipboard and we'll use this on the server side to know how 
        ' to put the stream back together
        Dim transactionGuid As String = System.Guid.NewGuid.ToString
        memStream.Position = 0
        Dim buffer() As Byte = New Byte((byteCount) - 1) {}
        Dim isFinalTransaction As Boolean = False
        'while the current stream position plus our byte count is less than the 
        ' length of the stream continue sending as much as we can.
 
        While ((memStream.Position + byteCount) _
                    <= memStream.Length)
            'if we happen to be on the last byte of the stream set the final 
            ' transaction flag to true so the server will know that this is the 
            ' last bit of this transaction to expect.
            If ((memStream.Position + byteCount) _
                        = memStream.Length) Then
                isFinalTransaction = True
            End If
            'ream the stream into our buffer for transmission over the web service.
            memStream.Read(buffer, 0, byteCount)
            clipService.InsertMessageStream(buffer, format, objectType, transactionGuid, _
                                            isFinalTransaction, clipBoardGUID)
 
        End While
        Dim remainingBytes As Long = (memStream.Length - memStream.Position)
        'if we still have remaining bytes left figure out how many and 
        ' transmit the last bit of this ojbect over the web service.
        If (CType(remainingBytes, Integer) > 0) Then
            Dim remainingBuffer() As Byte = New Byte((CType(remainingBytes, Integer)) - 1) {}
            memStream.Read(remainingBuffer, 0, CType(remainingBytes, Integer))
            clipService.InsertMessageStream(remainingBuffer, format, objectType, _
                                            transactionGuid, True, clipBoardGUID)
        End If

The Server side of the web service needs to put the whole clipboard back together from a number of byte arrays so it's important that all the objects, their types, and formats be preserved for the clipboard to work properly on the target machine. We use the clipBoardGuid to determine if we are on a new clipboard posting or adding objects to an already existing instance. We use the isFinalTranaction flag to know if this byte array should be part of an existing transaction, or is the first in a new transaction. All clipboard items are saved to disk for later retrieval by any client requesting them. The code for this is below.

C#

    [WebMethod]
    public void InsertMessageStream(byte[] buffer, string format, string objectType, 
        string transactionGuid, bool isFinalTransaction, string clipBoardGUID)
    {
        //always base the current directory on the clipboard that we're sending now.
        string clipBoardGUIDDirectory = 
            System.Web.HttpContext.Current.Request.PhysicalApplicationPath + clipBoardGUID;
 
        try
        {
            //if the directory does not exist then delete all the other directories 
            // (clipboard instances) and create a new directory if the directory already 
            // exists then this particular transaction is part of the same clipboard so 
            // don't do anything. this works because othe clipboardDirectory is based 
            // off of the GUID sent from the client.
            if (!Directory.Exists(clipBoardGUIDDirectory))
            {
                string[] dirs = Directory.GetDirectories(
                    System.Web.HttpContext.Current.Request.PhysicalApplicationPath);
                foreach (string dir in dirs)
                {
                    Directory.Delete(dir, true);
                }
                Directory.CreateDirectory(clipBoardGUIDDirectory);
            }
        }
        catch
        {
        }
        //create the filename based on the current transaction, format, and object type.  
        // We will parse this out later so we know how to add this back to the target clipboard.
        string fileName = clipBoardGUIDDirectory + "\\" + transactionGuid 
            + "_" + format + "_" + objectType;
        FileStream fs = new FileStream(fileName, FileMode.Append, FileAccess.Write);
        fs.Position = fs.Length;
        fs.Write(buffer, 0, buffer.Length);
        fs.Close();
    }
VB
    <WebMethod()> Public Sub InsertMessageStream(ByVal buffer() As Byte, ByVal _
            format As String, ByVal objectType As String, ByVal transactionGuid _
            As String, ByVal isFinalTransaction As Boolean, ByVal clipBoardGUID As String)
        'always base the current directory on the clipboard that we're sending now.
        Dim clipBoardDataDirectory As String = _
        (System.Web.HttpContext.Current.Request.PhysicalApplicationPath _
         + "\\Clipboard_Data")
        Dim clipBoardGUIDDirectory As String = (clipBoardDataDirectory + _
                                                ("\\" + clipBoardGUID))
        Try
            'if the directory does not exist then delete all the other directories (clipboard 
            ' instances) and create a new directory if the directory already exists then this 
            ' particular transaction is part of the same clipboard so don't do anything. this 
            ' works because othe clipboardDirectory is based off of the GUID sent from the client.
            If Not Directory.Exists(clipBoardGUIDDirectory) Then
                Dim dirs() As String = Directory.GetDirectories(clipBoardDataDirectory)
                For Each dir As String In dirs
                    Directory.Delete(dir, True)
                Next
                Directory.CreateDirectory(clipBoardGUIDDirectory)
            End If
        Catch
 
        End Try
        'create the filename based on the current transaction, format, and object type.  We 
        ' will parse this out later so we know how to add this back to the target clipboard.
        Dim fileName As String = (clipBoardGUIDDirectory + ("\\" _
                    + (transactionGuid + ("_" _
                    + (format + ("_" + objectType))))))
        Dim fs As FileStream = New FileStream(fileName, FileMode.Append, FileAccess.Write)
        fs.Position = fs.Length
        fs.Write(buffer, 0, buffer.Length)
        fs.Close()
    End Sub

Each clipgoard format object is stored on disk for later retrievel by the client. Note in the screenshot below how the filename is used to store the unique transactionID for the object, the object type and also the clipboard format. All these pieces of information are necessary to reassemble the items correctly and place onto the target clipboard.

Now that we have a representation of each clipboard format object on the server we need a way to get each item back onto the target clipboard. The following web service method provides a return result of type “ClipboardStream”. The ClipboardStream object contains all relevant information necessary to reassemble each item onto the target clipboard. Since a web service is a request-response type relationship the web service expects the client to continue to call the web service until the all the clipboard items have been successfully received. In addition, further complexity is introduced because each individual clipboard item may be split into multiple items if they exceed the maximum length set by our contstant “byteCount”, therefore the target machine must keep track of each request and tell the server where the last transaction left off via the variable named “currentByte”. The web service code is shown below.

C#

    [WebMethod]
    public ClipboardStream GetMessageStream(string transactionGUID, string[] 
        previousTransactionGUIDs, string clipBoardGUID, long currentByte)
    {
        string clipBoardDataDirectory = 
            System.Web.HttpContext.Current.Request.PhysicalApplicationPath + 
            "Clipboard_Data";
        string clipBoardGUIDDirectory = clipBoardDataDirectory + "\\" + 
            clipBoardGUID;
        string currentTransaction = "";
        bool isLastTransaction = false;
 
 
        //if the clipBoardGUID is not empty then we only need to make sure 
        // that the directory still exists.
        if (clipBoardGUID != "")
        {
            //if the directory does not exist throw an exception, it must 
            // have already been deleted.
            if (!Directory.Exists(clipBoardGUIDDirectory))
            {
                throw new Exception("Requested clipboard does not exist." +
                        "It must have been deleted.");
            }
        }
        //if the clipboardGUID is empty then this is the client's first contact 
        // with the server and we need
        //to select the available clipboard GUID to return to the user.
        else
        {
            string[] availableClipBoard = 
                Directory.GetDirectories(clipBoardDataDirectory)[0].Split('\\');
            clipBoardGUID = availableClipBoard[availableClipBoard.Length - 1];
            clipBoardGUIDDirectory += clipBoardGUID;
        }
 
        //we need to get the next transaction.  Each time we finish a transaction 
        // we add it to previousTransactionGUIDs at the client end so we know not 
        // to send it again.
        currentTransaction = GetCurrentTransaction(clipBoardGUIDDirectory, 
            previousTransactionGUIDs);
 
        //if the current transaction is null then we're done and there are no 
        // more to send to the client
        if (currentTransaction == null)
        {
            return null;
        }
 
        //open the filestream and set it to the position requested by the client.
        FileStream fs = new FileStream(currentTransaction, FileMode.Open);
        fs.Position = currentByte;
 
        //determind if this is the last transaction or not for this object 
        // so we can let the client know.
        long numBytesToRead = fs.Length - currentByte;
        if (numBytesToRead > byteCount)
        {
            numBytesToRead = byteCount;
            isLastTransaction = false;
        }
        else
        {
            isLastTransaction = true;
        }
 
        //read the filestream bytes to the buffer and populate the object to 
        // return to the client.
        byte[] buffer = new byte[numBytesToRead];
        fs.Read(buffer, 0, (int)numBytesToRead);
        fs.Close();
 
 
        FileInfo fi = new FileInfo(currentTransaction);
        ClipboardStream clipboardStream = new ClipboardStream();
        clipboardStream.Buffer = buffer;
        clipboardStream.ClipBoardID = clipBoardGUID;
        clipboardStream.Format = fi.Name.Split('_')[1];
        clipboardStream.ObjectType = fi.Name.Split('_')[2];
        clipboardStream.IsLastTransaction = isLastTransaction;
        clipboardStream.TransactionID = currentTransaction;
 
        return clipboardStream;
    }
VB
    <WebMethod()> Public Function GetMessageStream(ByVal transactionGUID As String, _
            ByVal previousTransactionGUIDs() As String, ByVal clipBoardGUID As String, _
            ByVal currentByte As Long) As ClipboardStream
        Dim clipBoardDataDirectory As String = _
            (System.Web.HttpContext.Current.Request.PhysicalApplicationPath + "Clipboard_Data")
        Dim clipBoardGUIDDirectory As String = clipBoardDataDirectory
        Dim currentTransaction As String = ""
        Dim isLastTransaction As Boolean = False
        'if the clipBoardGUID is not empty then we only need to make sure that 
        ' the directory still exists.
        If (clipBoardGUID <> "") Then
            'if the directory does not exist throw an exception, it must have 
            ' already been deleted.
            If Not Directory.Exists(clipBoardGUIDDirectory) Then
                Throw New Exception("Requested clipboard does not exist." + _
                                    "It must have been deleted.")
            End If
        End If
        'if the clipboardGUID is empty then this is the client's first contact 
        ' with the server and we need
        'to select the available clipboard GUID to return to the user.
        Dim availableClipBoard() As String = Directory.GetDirectories _
            (clipBoardDataDirectory)(0).Split(Microsoft.VisualBasic.ChrW(92))
        clipBoardGUID = availableClipBoard((availableClipBoard.Length - 1))
        clipBoardGUIDDirectory = (clipBoardGUIDDirectory + "\" + clipBoardGUID)
        'we need to get the next transaction.  Each time we finish a transaction 
        ' we add it to previousTransactionGUIDs
        'at the client end so we know not to send it again.
        currentTransaction = GetCurrentTransaction(clipBoardGUIDDirectory, _
                                                   previousTransactionGUIDs)
        'if the current transaction is null then we're done and there are no 
        ' more to send to the client
        If (currentTransaction Is Nothing) Then
            Return Nothing
        End If
        'open the filestream and set it to the position requested by the client.
        Dim fs As FileStream = New FileStream(currentTransaction, FileMode.Open)
        fs.Position = currentByte
        'determind if this is the last transaction or not for this object so 
        ' we can let the client know.
        Dim numBytesToRead As Long = (fs.Length - currentByte)
        If (numBytesToRead > byteCount) Then
            numBytesToRead = byteCount
            isLastTransaction = False
        Else
            isLastTransaction = True
        End If
        'read the filestream bytes to the buffer and populate the object to 
        ' return to the client.
        Dim buffer() As Byte = New Byte((numBytesToRead) - 1) {}
        fs.Read(buffer, 0, CType(numBytesToRead, Integer))
        fs.Close()
 
        Dim fi As FileInfo = New FileInfo(currentTransaction)
        Dim clipboardStream As ClipboardStream = New ClipboardStream
        clipboardStream.Buffer = buffer
        clipboardStream.ClipBoardID = clipBoardGUID
        clipboardStream.Format = fi.Name.Split(Microsoft.VisualBasic.ChrW(95))(1)
        clipboardStream.ObjectType = fi.Name.Split(Microsoft.VisualBasic.ChrW(95))(2)
        clipboardStream.IsLastTransaction = isLastTransaction
        clipboardStream.TransactionID = currentTransaction
        Return clipboardStream
    End Function

The last remaining piece for our project to function correctly is the client code that receives the clipboard contents from the server and reassembles each item in the correct order, adding it to the clipboard with the correct format as each is completed. The client code for this is shown below.  

C#
        string[] transactionGuids = null;
 
        ClipboardService.ClipboardStream clipBoardStream = 
            new WindowsApplication1.ClipboardService.ClipboardStream();
        DataObject dataObject = new DataObject();
        clipBoardStream.ClipBoardID = "";
        clipBoardStream.IsLastTransaction = false;
        clipBoardStream.TransactionID = "";
        long currentByte = 0;
 
        Clipboard.Clear();
 
        //while we don't get null back keep on contacting the web service 
        // to get the next ojbect.
        while (clipBoardStream != null)
        {
            MemoryStream memStream = new MemoryStream();
            //while this is not the last transaction keep on contacting 
            // the web service to get the rest of this particular object.
            while (clipBoardStream.IsLastTransaction == false)
            {
                //contact the web service to get the next transaction
                clipBoardStream = clipService.GetMessageStream(
                    clipBoardStream.TransactionID, transactionGuids, 
                    clipBoardStream.ClipBoardID, currentByte);
 
                if (clipBoardStream != null)
                {
                    //write the results to the memory stream
                    memStream.Write(clipBoardStream.Buffer, 0, 
                        clipBoardStream.Buffer.Length);
                    //increment the current byte so next time we contact 
                    // the webservice we'll pick up where we left off
                    currentByte = memStream.Position;
 
                    //if it is the last transaction then we need to place 
                    // this item onto the clipboard.
                    if (clipBoardStream.IsLastTransaction)
                    {
                        //handle the clipBoardStream appropriately and add it 
                        // to the dataObject for posting to the clipblard.
                        HandleFinalTransaction(clipBoardStream, memStream, ref dataObject);
 
                        //resize the transactionGuids array as necessary and 
                        // add the current transaction so next time we contact 
                        // the web service we won't get this one again.
                        if (transactionGuids == null)
                        {
                            Array.Resize(ref transactionGuids, 1);
                        }
                        else
                        {
                            Array.Resize(ref transactionGuids, 
                                transactionGuids.Length + 1);
                        }
                        transactionGuids[transactionGuids.Length - 1] = 
                            clipBoardStream.TransactionID;
                    }
                }
                else
                {
                    break;
                }
            }
 
            if (clipBoardStream != null)
            {
                clipBoardStream.IsLastTransaction = false;
                currentByte = 0;
            }
        }
 
        Clipboard.SetDataObject(dataObject, true);
VB
        Dim transactionGuids() As String = Nothing
        Dim clipBoardStream As ClipboardService.ClipboardStream = _
            New ClipboardVB.ClipboardService.ClipboardStream
        Dim dataObject As DataObject = New DataObject
        clipBoardStream.ClipBoardID = ""
        clipBoardStream.IsLastTransaction = False
        clipBoardStream.TransactionID = ""
        Dim currentByte As Long = 0
        Clipboard.Clear()
        'while we don't get null back keep on contacting the web service 
        ' to get the next ojbect.
 
        While (Not (clipBoardStream) Is Nothing)
            Dim memStream As MemoryStream = New MemoryStream
            'while this is not the last transaction keep on contacting the
            ' web service to get the rest of this particular object.
 
            While (clipBoardStream.IsLastTransaction = False)
                'contact the web service to get the next transaction
 
                clipBoardStream = clipService.GetMessageStream( _
                    clipBoardStream.TransactionID, transactionGuids, _
                    clipBoardStream.ClipBoardID, currentByte)
                If (Not (clipBoardStream) Is Nothing) Then
                    'write the results to the memory stream
                    memStream.Write(clipBoardStream.Buffer, 0, _
                                    clipBoardStream.Buffer.Length)
                    'increment the current byte so next time we contact 
                    ' the webservice we'll pick up where we left off
                    currentByte = memStream.Position
                    'if it is the last transaction then we need to place 
                    ' this item onto the clipboard.
                    If clipBoardStream.IsLastTransaction Then
                        'handle the clipBoardStream appropriately and add 
                        ' it to the dataObject for posting to the clipblard.
                        HandleFinalTransaction(clipBoardStream, memStream, dataObject)
                        'resize the transactionGuids array as necessary and 
                        ' add the current transaction so next time we contact
                        ' the web service we won't get this one again.
                        If (transactionGuids Is Nothing) Then
                            Array.Resize(transactionGuids, 1)
                        Else
                            Array.Resize(transactionGuids, (transactionGuids.Length + 1))
                        End If
                        transactionGuids((transactionGuids.Length - 1)) = _
                            clipBoardStream.TransactionID
                    End If
                Else
                    Exit While
                End If
 
            End While
            If (Not (clipBoardStream) Is Nothing) Then
                clipBoardStream.IsLastTransaction = False
                currentByte = 0
            End If
 
        End While
        Clipboard.SetDataObject(dataObject, True)

Conclusion

In this project we've shown how any object (string, bitmap, or file) from the Clipboard can be serialized to an array of bytes for transmission over a web service call. We've also show how to store these clipboard items on disk on the server, and then retrieve them later on for assembly onto the target clipboard. I've often thought that this would be an interesting idea for a hosted ASP (Application Service Provider) service, where a hosting company would host the web service of this project for customer use. Look out for an upcoming article on how to add ASP .NET membership class functionality providing authentication and allowing multiple users to use the same instance of the web service.

For working samples of the client application and web service in both C# and VB check out the DOWNLOAD.

Bio

As President and principal founder of Personify Design Brian oversees the operations of the design and development businesses. Brian has more than 10 years experience in the technology industry. In his current role Brian's expertise lies in developing and architecting end to end customer solutions involving web application technologies such as SQL Server and ASP .NET. When not writing code Brian enjoys sailing in the Puget Sound on Far Niente, a 36 foot Catalina MKII cutter.

Tags:

Follow the Discussion

  • ScottScott

    Please fix the width of this webpage.  The text is lost(cutoff)  on the main article here.

  • Robin NichollRobin Nicholl

    The lines on this page are all cut off, so that I cannot read the last 2-4 words on each line.

    I cannot scroll horizontally along the lines and I cannot widen the middle panel (where the text appears), so I don't know a good way to see this article to read it. (Maybe View Source?)

    I am viewing this page in IE6 SP2 on XP at 1024x768.

  • GregGreg

    text is truncated on right of each line making article impossible to read

  • Samp LeeSamp Lee

    Nice formatting.  The panel of Tags, Archives, etc blots out the right side of paragraphs 2 and 3 in my view.  Not the author's fault maybe but mildly irritating - as am I.

    An interesting article though.

  • LUKEOLUKEO

    Why can't I print this in IE? Only the first page shows up in the preview.

  • KenMarshallKenMarshall

    I think, that is interesting for all.

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.