Windows Clipboard Sharing Through Web Services

Published 09 March 07 04:16 PM | Coding4Fun 
  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.

    Filed under: ,

    Comment Notification

    If you would like to receive an email when updates are made to this post, please register here

    Subscribe to this post's comments using RSS

    Comments

    # Scott said on April 10, 2007 10:13 AM:

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

    # Robin Nicholl said on April 11, 2007 10:46 AM:

    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.

    # Greg said on April 13, 2007 1:59 PM:

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

    # KenMarshall said on April 15, 2007 11:43 PM:

    I think, that is interesting for all.

    # animalreserve said on April 16, 2007 2:58 AM:

    Everybody had ever thought of starting to work and to earn a lot of money. But as often happens there is a lack of time and opportunity or even the desire to sit in the office all the days. At first glance seems that this is impossible!

    But you haven’s seen what we can offer yet.

    - free schedule of work

    - work almost at home

    - earnings about $7000 per month +bonuses!!!

    - friendly stuff

    - free training

    - and its not all yet:

    The point is that our company is in close association with WWF, Green peace and other unexplored wilderness life-saving organizations. Our company attends to takings for the sake of PAND.

    So you will also carry out an important function for the welfare of your planet.

    Join us, there are more the 200 people!!!

    Join us and stand for you and your planet in good stead!!!

    Join us and you won’t regret!!!

    All detailed information is at our site

    http://animalreserve.net

    # LUKEO said on April 16, 2007 1:13 PM:

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

    # Samp Lee said on April 16, 2007 3:54 PM:

    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.

    # Shamane666 said on May 9, 2007 9:42 PM:

    What role of the Forex in the modern world?

    http://free.hostultra.com/~mummy21/

    # Shamane666 said on May 11, 2007 1:01 PM:

    What role of the Forex in the modern world?

    http://free.hostultra.com/~mummy21/

    # Derosamy said on May 30, 2007 11:10 AM:

    Как заработать в Интернете Новичку. Без вложений и затрат. Все объясняется буквально на пальцах-ну ОЧЕНЬ просто. Идеально подходит для новичков. Конкретные схемы, приемы, методы, которые помогут Вам организовать такую бизнес структуру, которая будет работать автономно 24 часа в сутки, без Вашего участия, и приносить Вам стабильный доход.

    "Заполучи!!! в свои руки настоящего денежного зверя, который будет ежедневно приносить нехилую денежную добычу на твой электронный счёт в различных кибер системах"

    Скачайте БЕСПЛАТНО книгу по следующей ссылки:

    http://kajfa.net/book1.zip

    Приятного Вам чтения!

    С уважением,

    Владимир.

    # StalkerTim said on May 30, 2007 4:51 PM:

    Thanks for your hard work! Nice site! Thank you!

    [url=http://buy-xenical.search-tablets.com]buy xenical[/url]

    # How do you do? Good luck!,mlmmoney,mlmmoney said on June 14, 2007 9:08 AM:

    How do you do?

    Good luck!,How do you do?

    Good luck!,How do you do?

    Good luck!,How do you do?

    Good luck!

    # Techy News Blog » Windows Clipboard Sharing via Web Services said on October 22, 2007 6:54 AM:

    PingBack from http://www.artofbam.com/wordpress/?p=10936

    # Windows Clipboard Sharing via Web Services | Fresh Vista News said on October 22, 2007 11:28 AM:

    PingBack from http://www.artofbam.com/vistanews/?p=812

    # Techy News » Windows Clipboard Sharing via Web Services said on November 20, 2007 9:13 PM:

    PingBack from http://technews.thegeekyblog.com/2007/04/11/windows-clipboard-sharing-via-web-services/

    # Techy News » Windows Clipboard Sharing via Web Services said on March 7, 2008 11:09 PM:

    PingBack from http://www.artofbam.com/wordpress/2007/04/11/windows-clipboard-sharing-via-web-services/

    Leave a Comment

    (required) 
    (optional)
    (required) 
    Page view tracker