Uso compartido del portapapeles de Windows mediante servicios web

2007

Publicado: 12 de Marzo de 2007

Brian Trautman

¿Ha trabajado alguna vez en varios equipos y ha deseado poder copiar el contenido del portapapeles de un equipo a otro? A menudo he pensado que sería perfecto si existiera una forma rápida y sencilla de mover fragmentos de texto, capturas de pantalla o incluso archivos a otro equipo simplemente con copiar y pegar. Si le parece interesante, siga leyendo.

Quería que esto funcionara independientemente de si los equipos estaban en línea al mismo tiempo o no y no quería que estuvieran interrumpidos por firewalls, NAT's, etc. así que, opté por una arquitectura basada en servidor en lugar de una arquitectura punto a punto. La arquitectura implicaba que una aplicación cliente transfiriera el contenido del portapapeles al servidor mediante una llamada de servicios web, un servicio web para guardar en caché el contenido del portapapeles; y que otro componente cliente recuperara el contenido del portapapeles del servidor y lo colocara en el portapapeles del equipo local.

Para solucionar este problema, necesitamos el acceso mediante programación al portapapeles tanto en el equipo desde el que se copia como en el equipo al que se copia. Afortunadamente, .NET ofrece un contenedor administrado alrededor de la API nativa del Portapapeles de Windows que nos da acceso a la misma. Los espacios de nombres pertinentes son Clipboard for C# y my.Computer.Clipboard. Como estamos interesados en mover objetos del portapapeles de un equipo a otro, primero necesitamos determinar qué tipo de objetos correspondientes a nuestras acciones se colocan en el portapapeles (copiar texto, imágenes y archivos). La escritura de un fragmento de código rápido con el espacio de nombres Clipboard nos permite repetir con todos los objetos en el portapapeles para varios tipos de acciones para ver con qué estamos trabajando.

En esta página
ContenidoContenido
*

Contenido

Visual 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);
                
}

Visual Basic

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)
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

Después de la inspección de los objetos del portapapeles que corresponden a objetos de texto, imágenes y archivos, era fácil determinar los tipos de objetos primarios sobre los que debemos ocuparnos. Estos son "System.IO.MemoryStream", "System.IO.FileStream", "System.Drawing.Bitmap", y "System.String". Puesto que toda esta información se transferiría al servidor vía servicios web, es sencillo serializar todos objetos a bytes para la transmisión. Esto es necesario por varias razones, inclusive el hecho de que los objetos complejos tales como MemoryStreams no pueden ser simplemente serializados y enviados a través de la llamada a servicio web como Strings. Además, algunos de los objetos son más grandes de lo que permite el servicio web y necesitarían ser divididos en partes más pequeñas para la transmisión y después volverse a unir en el lado de servidor en el orden correcto. Otra vez, cuando el cliente solicita los elementos del portapapeles, necesitaremos desensamblar cada objeto, enviarlo a través del resultado de retorno del servicio web al cliente, y entonces volver a unirlo.

El primer elemento que se debe construir es una función que realice la división de estas corrientes grandes en matrices de bytes más manejables para la transmisión al servicio web. La función de abajo realiza esta tarea enviando grupos del MemoryStream con un tamaño limitado por la constante “byteCount”. Una vez que este límite es alcanzado, el búfer es mandado a través de la llamada a servicio web para el almacenamiento y la unión en el servidor. Una vez que nos quedan 0 bytes que enviar o un número inferior a “byteCount”, enviamos los elementos restantes del búfer e indicamos al servicio web que este objeto en concreto está completo con el marcador “isFinalTransaction”.

Visual 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);
            }

        }

Visual Basic

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
    End Sub

El lado de servidor del servicio web necesita volver a unir todo el portapapeles desde varias matrices de bytes, por lo que es importante que todos los objetos, sus tipos y los formatos se conserven para que el portapapeles funcione correctamente en el equipo de destino. Usamos el clipBoardGuid para determinar si estamos en una nueva publicación de portapapeles o agregando objetos a una instancia ya existente. Usamos el marcador isFinalTranaction para saber si esta matriz de bytes debe formar parte de una transacción existente o es la primera en una transacción nueva. Todos los elementos del portapapeles se guardan en el disco para la recuperación posterior por parte de cualquier cliente que los solicita. El código para esto está abajo.

Visual 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();
    }

Visual Basic

<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

Cada objeto del formato del portapapeles se almacena en disco para la recuperación posterior por parte del cliente. Observe en el screenshot de abajo cómo el nombre del archivo se usa para almacenar el único transactionID correspondiente al objeto, el tipo de objeto y también el formato del portapapeles. Todos estos datos son necesarios para volver a unir los elementos correctamente y colocarlos en el portapapeles de destino.

Ahora que tenemos una representación de cada objeto de formato del portapapeles en el servidor, necesitamos una manera de volver a conseguir cada elemento en el portapapeles de destino. El método siguiente del servicio web ofrece un resultado de retorno del tipo “ClipboardStream”. El objeto ClipboardStream contiene toda información pertinente necesaria para volver a unir cada elemento en el portapapeles de destino. Puesto que un servicio web es un tipo de solicitud-respuesta, el servicio web espera que el cliente continúe llamando al servicio web hasta que los todos los elementos del portapapeles se hayan recibido correctamente. Además, se introduce una complejidad adicional porque cada elemento individual del portapapeles se puede dividir en varios elementos si exceden la longitud máxima establecida por nuestra constante “byteCount”; por lo tanto, el equipo de destino debe realizar un seguimiento de cada solicitud e indicar al servidor dónde se quedó la última transacción vía la variable denominada “currentByte”. El código del servicio web se muestra más abajo.

Visual 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;

    }

Visual Basic

    <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

¿Ha trabajado alguna vez en varios equipos y ha deseado poder copiar el contenido del portapapeles de un equipo a otro? A menudo he pensado que sería perfecto si existiera una forma rápida y sencilla de mover fragmentos de texto, capturas de pantalla o incluso archivos a otro equipo simplemente con copiar y pegar. En este proyecto, exploramos cómo transferir el contenido del portapapeles de un equipo a otro con un servicio web.


Principio de la páginaPrincipio de la página