Click here to Skip to main content
15,895,011 members
Articles / Programming Languages / C#

Copy/Paste For Databound Objects

Rate me:
Please Sign up or sign in to vote.
4.75/5 (3 votes)
11 Oct 2012CPOL4 min read 11.5K   7  
This article will show you how to implement Copy/Paste functionality in your application, when using Databound objects. It will discuss both how to implement the Copy and Paste functions using the Clipboard, and also how to watch the Windows Message que for changes in the clipboard contents.

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:

C#
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

C#
[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: 

C#
[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: 

C#
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:

C#
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. 

C#
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:

C#
private IntPtr _NextClipboardViewer; 

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

C#
using System.Runtime.InteropServices; 

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

C#
[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. 

C#
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.

C#
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: 

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


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --