Click here to Skip to main content
Click here to Skip to main content

Copy/Paste For Databound Objects

By , 11 Oct 2012
 

Introduction

This article will follow a simple scenario, a Restaurant object class, which has a few properties, including a List<> of HealthScore objects. The HealthScore objects are what will be copied and pasted. As the class stands in the example, it could be marked Serializable, but the assumption of the article is that the abstract concept object would contain further items that exclude it from being binary serializable. For this reason we also implement a sister object, ClipboardHealthScore. This sister object we will convert back and forth for storing on the Clipboard. We will also see how to hijack the windows message que to capture changes in the Clipboard, allowing us to dynamically control a paste button based on whether the Clipboard contains valid pasteable data.

Background

In order to have a custom object stored in the system Clipboard, it has to be binary serializable. Often in data binding objects, you may have inclusions that that exclude it from serialization eligibility, such as events, specialized getters or setters for properties, and other things of this nature. For this reason, our assumption is that our objects we want to store do not meet serialization requirements.

Using the code

First, let's take a minute to discuss the data object we are using for copy and paste functions. The items we are interested in are of the HealthScore type. Now, as I said before, in this case we could simply use our data object, because it would qualify for binary serialization, but lets assume that it wouldn't, as a real life object would most likely be more complex. For this reason, we implement a sister class to facilitate storage in the Clipboard. Firstly, our data object:

public class HealthScore
{
    private int _value;
    public HealthScore() { _value = 0; }
    public HealthScore(int value) { _value = value; }
    public int Value { get { return _value; } set { _value = value; } }
}

Our sister class, ClipboardHealthScore will be nearly identical, except we will extend IComareable<HealthScore> and include an int property SourceIndex. This will allow us to sort the collection before we store it in the Clipboard. This is important, because when you step through selected cells in the DataGridView, you get them in the order in which they were selected, not in order of how they appear in the table. Sorting by index will allow our paste to be in the same order as from the source table. Extending IComparable<> requires implementation of the CompareTo(ClipboardHealthScore a, ClipboardHealthScore b) method, which needs to return 1, -1, or zero based on whether <a> is considered greater than <b>, 1 for yes, -1 for no, and zero if they are equal. Additionally we need to mark this class Serializable

[Serializable]
public class ClipboardHealthScore : IComparable<ClipboardHealthScore>;
{
    private int _value;
    private int _sourceindex;
    public ClipboardHealthScore() { _value = _sourceindex= 0; }
    public ClipboardHealthScore(int value, int sourceindex) { _value = value; _sourceindex = sourceindex; }
    public int Value { get { return _value; } set { _value = value; } }
    public int SourceIndex { get { return _sourceindex; } set { _sourceindex = value; } }
    public int CompareTo(ClipboardHealthScore a, ClipboardHealthScore b)
    {
        if (a.SourceIndex > b.SourceIndex) return 1;    
        else if (a.SourceIndex < b.SourceIndex) return -1;
        else return 0;
    }
}

Additionally, a normal List<T> collection is not serializable, only because its base class is not marked so. For this reason we need to create a class that extends List<T> to store our ClipboardHealthScore items in, and flag it Serializable: 

[Serializable]
public class ClipboardHealthScoreCollection : List<ClipboardHealthScore>
{
    public ClipboardHealthScoreCollection() { }
}

Now to copy, we will fetch selected HealthScores, convert them to ClipboardHealthScores and store them on the Clipboard. To paste, we will retrieve the ClipboardHealthScores back from the Clipboard and convert them back to normal HealthScores, and add them to our data object. 

First, lets copy our selected items: 

private void copybutton_Click(object sender, EventArgs e)
{
    // First lets store each selected cell's row index in a list
    // making sure not to store duplicates
    List<Int32> selectedindexes = new List<Int32>();
    foreach (DataGridViewCell cell in this.dataGridView1.SelectedCells)
    { 
        if (!selectedindexes.Contains(c.RowIndex)) selectedindexes.Add(cell.RowIndex);
    }
    // Now we step through these indices converting each item to the ClipboardHealthScore
    // and  putting it into a collection, then add the collection to the Clipboard
    ClipboardHealthScoreCollection copyitems = new ClipboardHealthScoreCollection();
    foreach(int i in selectedindexes)
    {
        copyitems.Add(new ClipboardHealthScore(this.healthScoreBindingSource[i].Value, i));
    } 
    copyitems.Sort();
    DataObject dobj = new DataObject();
    dobj.SetData(typeof(ClipboardHealthScoreCollection), copyitems);
    Clipboard.SetDataObject(dobj);
}

Not too terribly difficult. Now let's handle pasting these items into our dataset:

private void pastebutton_Click(object sender, EventArgs e)
{
    DataObject dobj = (DataObject)Clipboard.GetDataObject();

    if (dobj.GetDataPresent(typeof(ClipboardHealthScoreCollection)))
    {
        ClipboardHealthScoreCollection pastescores = 
          (dobj.GetData(typeof(ClipboardHealthScoreCollection))as ClipboardHealthscoreCollection);
  
        HealthScore newscore;
        foreach(ClipboardHealthScore hs in pastescores)
        {
            newscore = new HealthScore();
            newscore.Value = hs.Value;
            AddHealthScore(newscore);
        }
    }
}

Finally, lets touch on how to set your form up to listen for Clipboard changes, allowing you to handle your paste controls accordingly. If you are making an MDI application, I recommend you set the MDI parent form as  the listener. 

First, we need to define two constant int variables that define the Windows messages we want to listen for. 

private const int WM_DRAWCLIPBOARD = 0x0308;
privaet const int WM_CHANGECBCHAIN = 0x030D;

You'll also need an IntPtr variable to hold the handle of the next listener:

private IntPtr _NextClipboardViewer; 

You'll need to add a using statement for Interop Services:

using System.Runtime.InteropServices; 

Next you need to set up the PInvoke methods you'll need to set up the clipboard listener:

[DllImport("User32.dll"), CharSet = CharSet.Auto)]]
private static extern IntPtr SetClipboardViewer(IntPtr newviewer);  
 
[DllImport("User32.dll"), CharSet = CharSet.Auto)] 
private static extern IntPtr ChangeClipboardChain(IntPtr hwnd, IntPtr newviewer);
 
[DllImport("User32.dll"), CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hwnd, int m, IntPtr wParam, IntPtr lParam); 

SetClipboardViewer will insert  the given handle into the Clipboard alert chain, and return the handle of the next viewer in line. We will use this method to set up your form as the listener, I recommend calling this in the Form_Load event. 

private void Form_Load(object sender, EventArgs e)
{
    _NextClipboardViewer = SetClipboardViewer(this.Handle);
}

ChangeClipboardChain will remove the given handle from the chain, and attach the last viewer to the specified next viewer. We will use this when our listener form closes. If you aren't handling the Form_Closing event, you can do this here. If you are, and you have criteria that cancels form closing, I recommend using this method in Form_Closed instead. That way we only detach when we close the form for sure.

private void Form_Closed(object sender, FormClosedEventArgs e)
{
    ChangeClipboardChain(this.Handle, _NextClipboardViewer);
}

SendMessage will send a windows message on down the line. We will use this when we intercept a message. Once we take our actions we will pass the message on down the line.

So now we need to override the WndProc(ref m); method. If the incoming message matches one of the two types we've defined, we want to handle them, and pass them on: 

protected override void WndProc(ref m)
{ 
 switch(m.Msg) 
 {
    case WM_DRAWCLIPBOARD:
        // here a change has been made in the contents of clipboard, lets evaluate
        pastebutton.Enable = Clipboard.ContainData(typeof(ClipboardHealthScoreCollection).FullName));
        SendMessage(_NextClipboardViewer, m.Msg, m.WParam, m.LParam);
        break;
    case WM_CHANGECBCHAIN: 
        // here a change has been made in the viewer chain
        if (m.WParam == _NextClipboardViewer) _NextClipboardViewer = m.LParam;
        else SendMessage(_NextClipboardViewer, m.Msg, m.WParam, m.LParam);
        break;
    default:
        base.WndProc(m);  
        break;
 }
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

stebo0728
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130513.1 | Last Updated 11 Oct 2012
Article Copyright 2012 by stebo0728
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid