Cross Browser Clipboard : DataGrid with Excel support – Part 2

In the previous post we saw how to implement Copy and Paste clipboard operations, completely in Silverlight with cross browser support. In this post, we will extend the functionality to support multiple rows and introduce some Excel like enhancements. We will finish with a reusable DataGridCopyPasteService that can be used to provide any DataGrid with cross browser clipboard (copy/paste) functionality.

ClipboardHelperWe will start with the solution from last post that shows the basic one row copy paste functionality in DataGrid with Excel support. If you recall, we coded three different techniques to implement clipboard functionality. We will extend each one in turn to provide multi row support.

Excel Clipboard FormatICopyPasteObject

When copied from Excel, each cell is separated by a tab(\t) and reach row is separated by newline characters(\r\n). You can test this out by copying rows from Excel and pasting into notepad. So all we need to do to provide multi row support is to format copy data where rows are separated by newline. Similarly when parsing paste data, we first need to split using newline and then tab.

ICopyPasteObject

So far we have used code external to item in order to build data for copy and to paste data back into item. It would be more advantageous to move that functionality into item itself. This will allow item to do its own validation and also accommodate future changes to item itself. ICopyPasteObject interface provides methods to get data to copy from item and to set paste data back into item.

Add a new interface ICopyPasteObject as shown:

public interface ICopyPasteObject {

    string[] CopyData();

    void PasteData(string[] dataFields);

}

Next modify Person class to implement above interface as shown

public enum Fields {
    FirstName,
    LastName,
    Age,
    City
}

public string[] CopyData() {
    return new string[] { FirstName, LastName, Age.ToString(), City };
}

public void PasteData(string[] dataFields) {
    BeginEdit();
    FirstName = dataFields[(int)Fields.FirstName];
    LastName = dataFields[(int)Fields.LastName];
    Age = int.Parse(dataFields[(int)Fields.Age]);
    City = dataFields[(int)Fields.City];
    EndEdit();
}

Private Copy Paste Functionality

In order to provide DataGrid scoped Copy and Paste functionality, we subscribe to KeyDown event and look for appropriate Key combinations. Replace existing code (in Tab1.xaml.cs) with following code

List<Person> _copyFromPersons;
void peopleDataGrid_KeyDown(object sender, KeyEventArgs e) {
    if (peopleDataGrid != e.OriginalSource) {
        return;
    }
    // Copy uisng Ctrl-C
    if (e.Key == Key.C &&
        ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
        || (Keyboard.Modifiers & ModifierKeys.Apple) == ModifierKeys.Apple)
        ) {
        _copyFromPersons = new List<Person>();
        foreach (Person person in peopleDataGrid.SelectedItems) {
            _copyFromPersons.Add(person.Clone());
        }
    }
    // Paste using Ctrl-V
    else if (e.Key == Key.V &&
        ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
        || (Keyboard.Modifiers & ModifierKeys.Apple) == ModifierKeys.Apple)
        ) {
        if (null == _copyFromPersons ) {
            Dispatcher.BeginInvoke(() => MessageBox.Show("Please select one or more Persons to copy from"));
            return;
        }
        //Paste
        int personToCopy = 0;
        foreach (Person pasteToPerson in peopleDataGrid.SelectedItems) {
            pasteToPerson.BeginEdit();
            pasteToPerson.FirstName = _copyFromPersons[personToCopy].FirstName;
            pasteToPerson.LastName = _copyFromPersons[personToCopy].LastName;
            pasteToPerson.Age = _copyFromPersons[personToCopy].Age;
            pasteToPerson.City = _copyFromPersons[personToCopy].City;
            pasteToPerson.EndEdit();
            if (++personToCopy >= _copyFromPersons.Count) {
                break;
            }
        }
        // optionally ask user if to auto insert
        bool autoInsert = false;
        if (_copyFromPersons.Count > peopleDataGrid.SelectedItems.Count) {
            autoInsert = true;
        }
        if (autoInsert) { // && _copyFromPersons.Count > peopleDataGrid.SelectedItems.Count) {
            Person pasteToPerson = null;
            // assumes Person does internal validation and maintains data state (state management)
            for (int i = personToCopy; i < _copyFromPersons.Count; i++) {
                // insert new person
                pasteToPerson = _data.GetPersonForPaste();
                pasteToPerson.BeginEdit();
                pasteToPerson.FirstName = _copyFromPersons[i].FirstName;
                pasteToPerson.LastName = _copyFromPersons[i].LastName;
                pasteToPerson.Age = _copyFromPersons[i].Age;
                pasteToPerson.City = _copyFromPersons[i].City;
                pasteToPerson.EndEdit();
            }
        }
    } 
    // Clear clipboard (similar to Excel)
    else if (e.Key == Key.Escape) {
        _copyFromPersons = null;
    }
 
}

When user executes copy command (ctrl-c), we save a snapshot of one or more selected items to a private List. Note that we take a snapshot (deep clone of data) instead of saving just a reference so that we have a fixed copy even if user subsequently changes data. When user executes paste command (ctrl-v), we copy data from the snapshot list and copy over to (one or more) selected rows.

Excel like Auto Insert

In Excel if you try to paste rows that are more than selected rows, Excels prompts you if you will like to insert new rows. We can provide similar feature by auto inserting rows when data to paste is more than user selected rows. (One behavior change is that we have no control over where data is inserted). In above code, we first check to see if we have any rows left over (to paste) after pasting over selected items. If that is the case, then we create new Person(s) and then copy data over.

Internet Explorer Only Copy Paste Functionality using Clipboard object

Internet Explorer provides access to ClipboardData object for clipboard operations and we can access it using Silverlight Html Bridge functionality.

Modify KeyDown in Tab2.xaml.cs as shown:

void peopleDataGrid_KeyDown(object sender, KeyEventArgs e) {
    if (peopleDataGrid != e.OriginalSource) {
        return;
    }
    // Copy uisng Ctrl-C
    if (e.Key == Key.C &&
        ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
        || (Keyboard.Modifiers & ModifierKeys.Apple) == ModifierKeys.Apple)
        ) {
        //Build data for clipboard
        StringBuilder textData = new StringBuilder();
        foreach (Person person in peopleDataGrid.SelectedItems) {
            if (_data.IsEmptyPerson(person)) {
                continue;
            }
            textData.Append(string.Join("\t", person.CopyData()));
            textData.Append(Environment.NewLine);
        }
        //Copy data to clipboard
        ScriptObject clipboardData = (ScriptObject)HtmlPage.Window.GetProperty("clipboardData");
        if (clipboardData != null) {
            bool success = (bool)clipboardData.Invoke("setData", "text", textData.ToString());
        } else {
            Dispatcher.BeginInvoke(() => MessageBox.Show("Sorry, this functionality is only avaliable in Internet Explorer."));
            return;
        }
    }
    // Paste using Ctrl-V
    else if (e.Key == Key.V &&
        ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
        || (Keyboard.Modifiers & ModifierKeys.Apple) == ModifierKeys.Apple)
        ) {
        //Get Data from Clipboard
        ScriptObject clipboardData = (ScriptObject)HtmlPage.Window.GetProperty("clipboardData");
        string textData = null;
        if (clipboardData != null) {
            textData = (string)clipboardData.Invoke("getData", "text");
        } else {
            Dispatcher.BeginInvoke(() => MessageBox.Show("Sorry, this functionality is only avaliable in Internet Explorer."));
            return;
        }
        //Parse data and build persons
        string[] rows = textData.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
        if (null == rows || 0 == rows.Length) {
            Dispatcher.BeginInvoke(() => MessageBox.Show("Please select one or more Persons to copy from"));
            return;
        } 
        //Paste
        int personToCopy = 0;
        foreach (Person pasteToPerson in peopleDataGrid.SelectedItems) {
            pasteToPerson.PasteData(rows[personToCopy++].Split(new string[] { "\t" }, StringSplitOptions.None));
            if (personToCopy >= rows.Length) {
                break;
            }
        }
        // optionally ask user if to auto insert
        bool autoInsert = false;
        if (rows.Length > peopleDataGrid.SelectedItems.Count) {
            autoInsert = true;
        }
        if (autoInsert) {
            Person pasteToPerson = null;
            // assumes Person does internal validation and maintains data state (state management)
            for (int i = personToCopy; i < rows.Length; i++) {
                // insert new person
                pasteToPerson = _data.GetPersonForPaste();
                pasteToPerson.PasteData(rows[i].Split(new string[] { "\t" }, StringSplitOptions.None));
            }
        }
    }
    // Clear clipboard (similar to Excel)
    else if (e.Key == Key.Escape) {
        ScriptObject clipboardData = (ScriptObject)HtmlPage.Window.GetProperty("clipboardData");
        if (clipboardData != null) {
            bool success = (bool)clipboardData.Invoke("setData", "text", string.Empty);
        } else {
            Dispatcher.BeginInvoke(() => MessageBox.Show("Sorry, this functionality is only avaliable in Internet Explorer."));
            return;
        }
    }
}

 

For the copy operation, we loop over selected items and assemble data to copy (tab to separate fields and new line to separate items). We then use Internet Explorer specific ClipbordData object to copy data over using SetData method. For paste operations, we first get data using ClipboardData object (GetData method) and we split data into rows. For each row, we build list of fields and paste them over selected item. If there are any rows leftover, we insert new items and paste over data.ClipboardHelper

Cross Browser Copy Paste Functionality

We can use TextBox as a clipboard proxy to provide cross browser clipboard (copy/paste) functionality. When user executes copy command, we just forward KeyEventArgs from DataGrid KeyDown event on to TextBox KeyDown override. In response, TextBox copies data to clipboard. Similarly when user executes paste command, we again forward KeyDown event to TextBox. TextBox in turns pastes data to clipboard.

ClipboardHelper class provides cross browser methods to Get and Set data form clipboard. It internally uses a private copy of TextBox to carry out clipboard operations.

Multi row code is same as for Internet Explorer, replacing Internet Explorer only ClipboardData object with ClipboardHelper for copy and paste operations. Modify KeyDown in Tab3.xaml.cs as shown

void peopleDataGrid_KeyDown(object sender, KeyEventArgs e) {
    if (peopleDataGrid != e.OriginalSource) {
        return;
    }
    // Copy uisng Ctrl-C
    if (e.Key == Key.C &&
        ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
        || (Keyboard.Modifiers & ModifierKeys.Apple) == ModifierKeys.Apple)
        ) {
        //Build data from clipboard
        StringBuilder textData = new StringBuilder();
        foreach (Person person in peopleDataGrid.SelectedItems) {
            if (_data.IsEmptyPerson(person)) {
                continue;
            }
            textData.Append(string.Join("\t", person.CopyData()));
            textData.Append(Environment.NewLine);
        }
        //Copy data to clipboard
        ClipboardHelper.SetData(e, textData.ToString());
    }
    // Paste using Ctrl-V
    else if (e.Key == Key.V &&
        ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
        || (Keyboard.Modifiers & ModifierKeys.Apple) == ModifierKeys.Apple)
        ) {
        //Get Data from Clipboard
        string textData = ClipboardHelper.GetData(e);
        //Parse data and build persons 
        string[] rows = textData.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
        if (null == rows || 0 == rows.Length) {
            Dispatcher.BeginInvoke(() => MessageBox.Show("Please select one or more Persons to copy from"));
            return;
        } 
        //Paste
        int personToCopy = 0;
        foreach (Person pasteToPerson in peopleDataGrid.SelectedItems) {
            pasteToPerson.PasteData(rows[personToCopy++].Split(new string[] { "\t" }, StringSplitOptions.None));
            if (personToCopy >= rows.Length) {
                break;
            }
        }
        // optionally ask user if to auto insert
        bool autoInsert = false;
        if (rows.Length > peopleDataGrid.SelectedItems.Count) {
            autoInsert = true;
        }
        if (autoInsert) { 
            Person pasteToPerson = null;
            // assumes Person does internal validation and maintains data state (state management)
            for (int i = personToCopy; i < rows.Length; i++) {
                // insert new person
                pasteToPerson = _data.GetPersonForPaste();
                pasteToPerson.PasteData(rows[i].Split(new string[] { "\t" }, StringSplitOptions.None));
            }
        }
    }
}

Functionality Comparison Matrix

Technique Pros Cons
Private Clipboard Functionality 1. Easy to code and manage data
2. Cross browser compatible
1. Limited to application only, no cross application support
IE Clipboard Object Functionality 1. Full access to Clipboard
2. Can be initiated from code
1. Limited to Internet Explorer only
TextBox based Cross browser Clipboard Functionality 1. Cross browser support 1. Can be initiated by user only
2. Text only

DataGridCopyPasteService

DataGridCopyPasteService Lets factor out the clipboard(copy/paste) functionality into a reusable construct. We will use the Attached Property to extend DataGrid with Copy and Paste functionality. We will also assume that underlying item in DataGrid.ItemSource implements ICopyPasteObject and that it has a parameter less constructor to support auto insert.

Attached Property Basics

An attached property is a dependency property that is defined by one object but settable on other object. Attached properties are defined as a specialized form of dependency property that do not have the conventional property wrapper on the type defining the property. In addition, storage is provided by the type on which the property is set and not the defining type.

In XAML, you set attached properties by using the syntax AttachedPropertyProvider.PropertyName. Attached property value itself is stored by the object on which it is set. Think of the object as providing a name value pair dictionary, where property is the key and points to the value, to be used by property definer or even other objects.

For details on attached property and it various uses, please see my previous post Prevention : The first line of defense, with Attach Property Pixie dust!

Add DataGridCopyPasteService.cs as shown:

public class DataGridCopyPasteService {
 
    #region IsEnabledProperty
    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(DataGridCopyPasteService),
                                            new PropertyMetadata(OnIsEnabledChanged));
 
    public static bool GetIsEnabled(DependencyObject d) {
        return (bool)d.GetValue(IsEnabledProperty);
    }
 
    public static void SetIsEnabled(DependencyObject d, bool value) {
        d.SetValue(IsEnabledProperty, value);
    }
 
    private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
        FrameworkElement element = d as FrameworkElement;
        if ((bool)e.OldValue) {
            element.KeyDown -= new KeyEventHandler(element_KeyDown);
        }
        if ((bool)e.NewValue) {
            element.KeyDown += new KeyEventHandler(element_KeyDown);
        }
    }
    #endregion
 
    #region KeyDown hanlder
    const string TAB = "\t";
 
    /// <summary>
    /// Handles the KeyDown event of the datagrid element control.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.Windows.Input.KeyEventArgs"/> instance containing the event data.</param>
    private static void element_KeyDown(object sender, KeyEventArgs e) {
        DataGrid dataGrid = sender as DataGrid;
        if (null == dataGrid || dataGrid != e.OriginalSource) {
            return;
        }
        //
        // Copy uisng Ctrl-C
        if (e.Key == Key.C &&
            ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
            || (Keyboard.Modifiers & ModifierKeys.Apple) == ModifierKeys.Apple)
            ) {
            //Build data for clipboard
            StringBuilder textData = new StringBuilder();
            foreach (ICopyPasteObject item in dataGrid.SelectedItems) {
                textData.Append(string.Join(TAB, item.CopyData()));
                textData.Append(Environment.NewLine);
            }
            //Copy data to clipboard
            ClipboardHelper.SetData(e, textData.ToString());
        }
            // Paste using Ctrl-V
        else if (e.Key == Key.V &&
            ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
            || (Keyboard.Modifiers & ModifierKeys.Apple) == ModifierKeys.Apple)
            ) {
            //Get Data from Clipboard
            string textData = ClipboardHelper.GetData(e);
 
            //Parse data and build persons
            string[] rows = textData.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
            if (null == rows || 0 == rows.Length) {
                dataGrid.Dispatcher.BeginInvoke(() => MessageBox.Show("Please select one or more item to copy from"));
                return;
            }
            // Paste
            int row = 0;
            foreach (ICopyPasteObject item in dataGrid.SelectedItems) {
                item.PasteData(GetFields(rows[row++]));
                if (row >= rows.Length) {
                    return;
                }
            }
            // auto insert, optionally ask user
            bool autoInsert = false;
            if (rows.Length > dataGrid.SelectedItems.Count) {
                autoInsert = true;
            }
            if (autoInsert) { 
                IList list = dataGrid.ItemsSource as IList;
                Type itemType = dataGrid.SelectedItem.GetType();
                // assumes item does internal validation and maintains data state (state management)
                for (int i = row; i < rows.Length; i++) {
                    object item = Activator.CreateInstance(itemType);
                    ((ICopyPasteObject)item).PasteData(GetFields(rows[row++]));
                    list.Add(item);
                }
            }
        }
    }
    #endregion
 
    #region Helper Methods
    /// <summary>
    /// Gets the fields from given row.
    /// </summary>
    /// <param name="row">row to get fields from</param>
    /// <returns>string[] of data fields</returns>
    /// <remarks>Assumes fields are seperated by \t(tab)</remarks>
    private static string[] GetFields(string row) {
        if (string.IsNullOrEmpty(row)) {
            return new string[] {};
        }
        return row.Split(new string[] { TAB }, StringSplitOptions.None);
    }
    #endregion
}

IsEnabled dependency property is registered as an attached property. OnIsEnabledChanged is called whenever IsEnabled property changes. In that function, we get access to dependency object on which IsEnabled is set. We subscribe to KeyDown event for that dependency object. In KeyDown, we handle Ctrl-C and Ctrl-V for copy and paste respectively. In copy, we build data to copy to clipboard and copy resulting data to clipboard with ClipboardHelper.SetData. For paste, we first get data using ClipboardHelper.GetData. Next we parse data and paste it into selected items. If there is more data available then items selected to paste, we auto insert new items; by first creating new item using reflection and then pasting data into it.

Usage

Modify Tab4.xamls as shown
 
<data:DataGrid x:Name="peopleDataGrid" AutoGenerateColumns="False"
    Margin="10" RowHeight="22"
    ItemsSource="{Binding}" SelectedItem="{Binding SelectedPerson,Mode=TwoWay}" 
    src:DataGridCopyPasteService.IsEnabled="true">
 
Also add namespace reference to UserControl tag for DataGridCopyPasteService
 
xmlns:src="clr-namespace:SilverlightApplication"

With DataGridCopyPasteService in place, there in no need to handle KeyDown in code behind. DataGridCopyPasteService takes care of copy and paste operations automatically. Open Tab4.xaml.cs and remove KeyDown handler. F5 and test the application.

Completed Application UI

Source Code: CopyPasteMultiRow.zip

Technorati Tags:
Published Wednesday, November 19, 2008 11:43 AM by manish.dalal
Filed under: , ,

Comments

# Silverlight Cream for November 20, 2008 -- #433

Friday, November 21, 2008 12:16 AM by Community Blogs

In this issue: Andrew Myhre, Silverlight Girl, webgui, Tim Greenfield, Shawn Wildermuth, Manish Dalal

# 2008 November 21 - Links for today &laquo; My (almost) Daily Links

Pingback from  2008 November 21 - Links for today &laquo; My (almost) Daily Links

# re: Cross Browser Clipboard : DataGrid with Excel support – Part 2

Wednesday, December 31, 2008 2:11 AM by Syed Mehroz Alam

Great implementation, Manish. Thanks for the work.

# re: Cross Browser Clipboard : DataGrid with Excel support – Part 2

Monday, June 22, 2009 11:37 AM by Kirk

This looks good, but was a bit surprised when I copy and paste code into Visual Studio - the linebreaks are missing.  Maybe add some SL dogfood here.

Thanks for posting.

# re: Cross Browser Clipboard : DataGrid with Excel support – Part 2

Monday, July 20, 2009 7:14 AM by suresh.kalimuthu

Manish,

The article is great and to the need i am looking for. I have some issues using complex object with ObservableCollection since the System.Collections.ObjectModel is not implemented in the .NET Namespace for ASP.Web. Since I am want to share the same complex object in WCF service for DataContract and Binding in the Silverlight app datagrid.

Any idea will help me?

Thanks

Suresh

Leave a Comment

(required) 
(required) 
(optional)
(required)