Windows Clipboard Sharing Through Web Services
- Posted: Mar 09, 2007 at 1:16 PM
- 874 Views
- 6 Comments
Loading User Information from Channel 9
Something went wrong getting user information from Channel 9
Loading User Information from MSDN
Something went wrong getting user information from MSDN
Loading Visual Studio Achievements
Something went wrong getting the Visual Studio Achievements
| 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
Hardware: None
Download: Download
|
|
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)
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.
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.
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.
Follow the Discussion
Oops, something didn't work.
What does this mean?
Following an item on Channel 9 allows you to watch for new content and comments that you are interested in. You need to be signed in to Channel 9 to use this feature.What does this mean?
Following an item on Channel 9 allows you to watch for new content and comments that you are interested in and view them all on your notifications page.sign up for email notifications?
Please fix the width of this webpage. The text is lost(cutoff) on the main article here.
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.
text is truncated on right of each line making article impossible to read
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.
Why can't I print this in IE? Only the first page shows up in the preview.
I think, that is interesting for all.
Remove this comment
Remove this thread
close