Click here to Skip to main content
15,884,537 members
Articles / Programming Languages / C#

MDI Case Study Purchasing - Part VIII - Copy & Paste

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
2 Dec 2015CPOL7 min read 12.1K   309   7   3

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

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

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

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

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

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

Screenshot 1

Double click each new menu item to create a Click handler, and call our three new methods appropriately

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

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

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

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

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

C#
public event EventHandler SelectionChanged;

And let's create a method OnSelectionChanged to handle firing the event

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

Screenshot 2

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

C#
childForm.SelectionChanged += purchaseOrderForm_SelectionChanged;

And in OpenFile

C#
purchaseOrderForm.SelectionChanged += purchaseOrderForm_SelectionChanged;

And the handler should look like

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

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

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

C#
protected override void OnLoad(EventArgs e)
{
   RegisterClipboardViewer();
   base.OnLoad(e);
}

And to unregister we need to override the OnFormClosed method of MDIForm

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

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

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

 
QuestionDownload link seems to be broken Pin
us47116-Dec-15 23:55
us47116-Dec-15 23:55 
AnswerRe: Download link seems to be broken Pin
stebo07287-Dec-15 2:02
stebo07287-Dec-15 2:02 
GeneralRe: Download link seems to be broken Pin
us47117-Dec-15 20:31
us47117-Dec-15 20:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.