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)
{
List<Int32> selectedindexes = new List<Int32>();
foreach (DataGridViewCell cell in this.dataGridView1.SelectedCells)
{
if (!selectedindexes.Contains(c.RowIndex)) selectedindexes.Add(cell.RowIndex);
}
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:
pastebutton.Enable = Clipboard.ContainData(typeof(ClipboardHealthScoreCollection).FullName));
SendMessage(_NextClipboardViewer, m.Msg, m.WParam, m.LParam);
break;
case WM_CHANGECBCHAIN:
if (m.WParam == _NextClipboardViewer) _NextClipboardViewer = m.LParam;
else SendMessage(_NextClipboardViewer, m.Msg, m.WParam, m.LParam);
break;
default:
base.WndProc(m);
break;
}
}
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.