MDI Case Study Purchasing - Part VIII - Copy & Paste
Introduction
In Part VIII we will set up our form to be able to Cut, Copy, and Paste items from one document to another. The .NET framework already has everything we need to handle passing data to and from the system Clipboard
, which is how we will hand off items from one document to another.
Copy
The Windows clipboard is a generic object collection, where each type is stored and referenced by a format String. We will be storing our data on the clipboard with a unique string indentifier format. We want this to be the same everytime we use it, so let's define it as a static readonly property. We are going to be storing an instance of ItemCollection
containing the items we want copied. So let's store this format string in ItemCollection
. Go to the ItemCollection
class and add the following
public static readonly String ItemCollectionFormat =
"MDICaseStudyPurchasing.ObjectBase.ItemCollection";
This string could literally be anything really, but it's best to make it something understandable, so if another developer is exploring data on the clipboard, they'll know what that object represents. Now let's create a method in our PurchaseOrderForm
class that will move all currently selected items onto the Clipboard, we'll call it CopySelectedToClipboard()
public void CopySelectedToClipboard()
{
ItemCollection copyItems = new ItemCollection();
foreach (DataGridViewRow r in itemsDataGridView.Rows)
{
if (r.Selected)
{
Item thisItem = _purchaseOrder.Items[r.Index];
if(!copyItems.Contains(thisItem)) copyItems.Add(thisItem);
continue;
}
foreach (DataGridViewCell c in r.Cells)
{
if (c.Selected)
{
Item thisItem = _purchaseOrder.Items[r.Index];
if (!copyItems.Contains(thisItem)) copyItems.Add(thisItem);
continue;
}
}
}
Clipboard.SetData(ItemCollection.ItemCollectionFormat, copyItems);
}
This method steps through, and if the row is selected, or any cell in the row is selected, the item is added to the copyItems collection.
That's it for Copy, now our ItemCollection
object is on the clipboard, and ready to be pasted. Now for Cut. Cut is simply copy, with the addition of deleting the source. From eariler, we've already created a method for deleting a collection of items from a PurchaseOrder
, so all we need to do is call this using the copyItems
collection. Lets create a method for Cut, called CutSelected()
public void CopySelectedToClipboard()
{
ItemCollection copyItems = new ItemCollection();
foreach (DataGridViewRow r in itemsDataGridView.Rows)
{
if (r.Selected)
{
Item thisItem = _purchaseOrder.Items[r.Index];
if(!copyItems.Contains(thisItem)) copyItems.Add(thisItem);
continue;
}
foreach (DataGridViewCell c in r.Cells)
{
if (c.Selected)
{
Item thisItem = _purchaseOrder.Items[r.Index];
if (!copyItems.Contains(thisItem)) copyItems.Add(thisItem);
continue;
}
}
}
Clipboard.SetData(ItemCollection.ItemCollectionFormat, copyItems);
_purchaseOrder.DeleteItems(copyItems);
}
Now for paste, we will be retrieving the collection from the clipboard, and adding those items to the document we're pasting into. First we want to check to see if the clipboard has any data in the format we want, and if so, retrieve it and paste it. Let's create the method PasteFromClipboard()
public void PasteFromClipboard()
{
if (Clipboard.ContainsData(ItemCollection.ItemCollectionFormat))
{
ItemCollection pasteItems =
(ItemCollection)Clipboard.GetData(ItemCollection.ItemCollectionFormat);
_purchaseOrder.AddItems(pasteItems);
purchaseOrderBindingSource.ResetBindings(false);
}
}
This will throw a build error because we don't yet have an AddItems(ItemCollection)
method in PurchaseOrder
, let's create it now
public void AddItems(ItemCollection addItems)
{
OnChanging(new PurchaseOrderChangingEventArgs());
foreach (Item i in addItems) _items.Add(i);
OnChanged(new PurchaseOrderChangedEventArgs());
}
Now we have the guts for Cut, Copy, and Paste, so let's just work on the UI elements. First we'll add menu items to our itemGridMenu
. In PurchaseOrderForm
Design View, select itemGridMenu
, and add a new menu item for Cut, Copy, and Paste, and add a "-" item to seperate these items between Select All and Delete, so that your menu looks like this
Double click each new menu item to create a Click handler, and call our three new methods appropriately
private void copyToolStripMenuItem_Click(object sender, EventArgs e)
{
CopySelectedToClipboard();
}
private void pasteToolStripMenuItem_Click(object sender, EventArgs e)
{
PasteFromClipboard();
}
private void cutToolStripMenuItem_Click(object sender, EventArgs e)
{
CutSelected();
}
Now, let's go back to the itemGridMenu_Opening
handler, and place some control on when these menu items are enabled for use. Cut and Copy should be available on if itemsDataGridView
has items, and at least 1 is selected. Paste should only be available if the Clipboard contains viable data
private void itemGridMenu_Opening(object sender, CancelEventArgs e)
{
.....
cutToolStripMenuItem.Enabled = itemsDataGridView.SelectedRows.Count > 0;
copyToolStripMenuItem.Enabled = itemsDataGridView.SelectedRows.Count > 0;
pasteToolStripMenuItem.Enabled = Clipboard.ContainsData(ItemCollection.ItemCollectionFormat);
}
Before we move on, one more thing we need to do in PurchaseOrderForm
to facilitate controlling when Cut, Copy, and Paste are available in MDIForm
. Let's create a public int property that will return the SelectedRows
count from itemsDataGridView
public int SelectedRowCount
{
get { return itemsDataGridView.SelectedRows.Count; }
}
Now let's move back to MDIForm
, and let's handle the Cut, Copy, and Paste buttons there. First, in Design View, select the Edit->Cut, Edit->Copy, and Edit->Paste menu items, and set their Enabled
property to False, so at application launch they will be disabled. Now, int the OnMdiChildActivate
method, we will control when these menu items get enabled. First, at the top of OnMdiChildActivate
, set the Cut, Copy, and Paste menu items Enabled
to false.
private override void OnMdiChildActivate(object sender, EventArgs e)
{
....
cutToolStripMenuItem.Enabled = copyToolStripMenuItem.Enabled = pasteToolStripMenuItem.Enabled = false;
Then in the if (this.ActiveMdiChild != null)
block, lets add the checks for enabling the buttons
if (this.ActiveMdiChild != null)
{
.....
cutToolStripMenuItem.Enabled = activeChildForm.SelectedItemCount > 0;
copyToolStripMenuItem.Enabled = activeChildForm.SelectedItemCount > 0;
pasteToolStripMenuItem.Enabled = Clipboard.ContainsData(ItemCollection.ItemCollectionFormat);
}
Lastly, we have 2 scenarios to deal with. If the selection of the document changes, we need to alert the MDIForm
so that the Cut and Copy menu items can be enabled or disabled. Also, if the contents of the Clipboard change we need to alert MDIForm
so the Paste menu item can be enabled or disabled. The first scenario we will handle with another customer event in PurchaseOrderForm
, we'll call it SelectionChanged
. The second scenario, we will break into the Windows message loop. Anytime the contents of the clipboard changes, a windows message is sent out to alert all open applications. We will watch for this message, and resond.
First let's deal with selection changes. We don't need anything fance for this event, no special handler delegate or EventArgs class, so we will use the generic EventHandler
delegate, and the generic EventArgs
class. So in the PurchaseOrderForm
class, lets add a public event
public event EventHandler SelectionChanged;
And let's create a method OnSelectionChanged
to handle firing the event
private void OnSelectionChanged(EventArgs e)
{
if (SelectionChanged != null) SelectionChanged(this, e);
}
Now, with PurchaseOrderForm
in Design View, and with itemsDataGridView
selected, go to properties, and click the lightning bolt icon to access Events, and lets double click to create a handler for the SelectionChanged
event for itemsDataGridView
. In the handler we simply need to call OnSelectionChanged
private void itemsDataGridView_SelectionChanged(object sender, EventArgs e)
{
OnSelectionChanged(new EventArgs());
}
Now, in MDIForm
, we need to add a handler to each instance of PurchaseOrderForm
we open, which means we need to add this in ShowNewForm
and OpenFile
. In both methods, right below where we added the Changed
event handler, add the line in ShowNewForm
childForm.SelectionChanged += purchaseOrderForm_SelectionChanged;
And in OpenFile
purchaseOrderForm.SelectionChanged += purchaseOrderForm_SelectionChanged;
And the handler should look like
private void purchaseOrderForm_SelectionChanged(object sender, EventArgs e)
{
PurchaseOrderForm activeChildForm = this.ActiveMdiChild as PurchaseOrderForm;
PurchaseOrderForm sendingForm = sender as PurchaseOrderForm;
if(sendingForm == activeChildForm)
{
cutToolStripMenuItem.Enabled = activeChildForm.SelectedItemCount > 0;
copyToolStripMenuItem.Enabled = activeChildForm.SelectedItemCount > 0;
}
}
Lastly, let's get our MDIForm
listening for clipboard changes. In the source I have 2 classes, I wouldn't bother retyping them, just use them, one is User32.cs and the other is Win32.cs
User32
contains the Interop dllImports we need for listening in on Windows messages. Win32
contains an enumeration of most of the Windows messages values.
In MDIForm
, here are the changes we need to make. First, we need to store a private IntPtr
to keep up with the next window handler that needs to recieve clipboard notifications.
private IntPtr ClipboardNextViewer;
Think of this chain like a Conga line. Each person grabbing the person ahead. When we register, we are "butting in" between 2 people, and when we unregister, we are stepping out and the line reconnects. So we need 2 methods, one to register, and one to unregister
private void RegisterClipboardViewer()
{
ClipboardNextViewer = User32.SetClipboardViewer(this.Handle);
}
private void UnregisterClipboardViewer()
{
User32.ChangeClipboardChain(this.Handle, ClipboardNextViewer);
}
Now to register, we need to override the OnLoad
method of MDIForm
protected override void OnLoad(EventArgs e)
{
RegisterClipboardViewer();
base.OnLoad(e);
}
And to unregister we need to override the OnFormClosed
method of MDIForm
protected override void OnFormClosed(FormClosedEventArgs e)
{
UnregisterClipboardViewer();
base.OnFormClosed(e);
}
And the final piece of the puzzle, we need to override the WndProc(ref Message m)
method of MDIForm
. This method is the one that actually receives and processes dispatched Windows messages.
protected override void WndProc(ref Message m)
{
Win32.Msgs wMsg = (Win32.Msgs)m.Msg;
switch (wMsg)
{
case Win32.Msgs.WM_DRAWCLIPBOARD:
if (this.ActiveMdiChild == null)
{
pasteToolStripMenuItem.Enabled = false;
}
else
{
pasteToolStripMenuItem.Enabled =
Clipboard.ContainsData(ItemCollection.ItemCollectionFormat);
}
User32.SendMessage(ClipboardNextViewer, m.Msg, m.WParam, m.LParam);
break;
case Win32.Msgs.WM_CHANGECBCHAIN:
if (m.WParam == ClipboardNextViewer) ClipboardNextViewer = m.LParam;
else User32.SendMessage(ClipboardNextViewer, m.Msg, m.WParam, m.LParam);
break;
}
base.WndProc(ref m);
}
Without getting too deep into it, WM_DRAWCLIPBOARD
notifies of changed to the contents of the clipboard. WM_CHANGECBCHAIN
notifies of changes in which applications are listening. Back to the Conga analogy, its like the person behind you leaving the line, the person behind them has to grab you.
So that completes Cut, Copy, and Paste. You can test out the app now, and see that everything functions as you would expect a document application to function. Out of the box MDI parent forms tie the cut, copy, and paste menu items to the Ctrl+X, Ctrl+C, and Ctrl+V hot keys, so give that a try, select and item, hit Ctrl+C, and then Ctrl+V and you'll see the item pasted into the form.
That does it for Part VIII. In the next part we will get our Undo/Redo functions working.
Points of Interest
- Cut, Copy, and Paste functionality using the Windows Clipboard
- Windows Message capture using the WndProc method
History
Keep a running update of any changes or improvements you've made here.