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.
We 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 Format
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.
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
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.

Source Code: CopyPasteMultiRow.zip
Technorati Tags:
Silverlight