Manish Dalal's blog

Exploring .net!

November 2008 - Posts

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:
Cross Browser Copy and Paste in DataGrid with Excel support – Part 1

Silverlight 2 is a cross browser platform(plug-in), providing developers with a familiar .net programming model for building RIAs. However it is also a relatively young platform. This came to surface other day when a tester came to me complaining about our new Silverlight enabled web application. The tester was not happy, since he could not copy data from Silverlight DataGrid to Excel. He could copy data from rest of the application (a traditional asp.net web application) except from the Silverlight module!

So I decided to look into providing the standard clipboard functionality. This post chronicles my attempt to build Copy/Paste functionality, road blocks that I encountered to provide cross browser functionality and the ultimate solution that provides reusable clipboard functionality in multiple browsers, all within Silverlight!

Disclaimer: Clipboard functionality described here only works as long you are working with text data. Also I only tested in Internet Explorer 7 (Windows Vista SP1), Firefox 3.03(Windows Vista SP1 and Mac OS X 10.5.5) and Google Chrome 0.3.154.9(Windows Vista SP1). If you have resources to test on other browsers/ operating systems, please let me know how it works in other browsers.Person People Class Diagram

Setup

Create a new SilverlightApplication and the corresponding Web Application to host and test the SilverlightApplication. I will reuse the Person and People object model from my previous series on Building Business Application. For simplicity, I have removed all validation, and replaced with a simple rule where LastName and City fields are required. (If you will like to explore detail validation, please see my various posts on validation here and here).

Basic DataGrid

Add Person and People classes as shown

Person.cs

public class Person : INotifyPropertyChanged, IEditableObject {
 
    #region Constructors
    public Person() {
    }
 
    public Person(string firstName, string lastName, int age, string city): this() {
        this._firstName = firstName;
        this._lastName = lastName;
        this._age = age;
        this._city = city;
    }
    #endregion
 
    #region Properties
    private string _firstName;
    public string FirstName {
        get { return _firstName; }
        set {
            if (value == _firstName) return;
            _firstName = value;
            RaisePropertyChanged("FirstName");
        }
    }
 
    private string _lastName;
    public string LastName {
        get { return _lastName; }
        set {
            if (value == _lastName) return;
            _lastName = value;
            RaisePropertyChanged("LastName");
        }
    }
 
    private int _age;
    public int Age {
        get { return _age; }
        set {
            if (value == _age) return;
            if (value < 0 || value > 200) {
                throw new Exception("Age must be between 0 and 200");
            } 
            _age = value;
            RaisePropertyChanged("Age");
        }
    }
 
    private string _city;
    public string City {
        get { return _city; }
        set {
            if (value == _city) return;
            _city = value;
            RaisePropertyChanged("City");
        }
    }
    #endregion
 
    #region INotifyPropertyChanged Members
    public event PropertyChangedEventHandler PropertyChanged;
 
    protected void OnPropertyChanged(string name) {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
 
    internal void RaisePropertyChanged(string name) {
            OnPropertyChanged(name);
    }
    #endregion
 
    #region IEditableObject Members
    Person _backup;
    bool _editing;
 
    public void BeginEdit() {
        if (!_editing) {
            _editing = true;
            _backup = this.MemberwiseClone() as Person;
        }
    }
 
    public void CancelEdit() {
        if (_editing) {
            FirstName = _backup.FirstName;
            LastName = _backup.LastName;
            Age = _backup.Age;
            City = _backup.City;
            _editing = false;
        }
    }
 
    public void EndEdit() {
        if (_editing) {
            _editing = false;
            this._backup = null;
            if (null != ChangesCommitted) {
                if (!string.IsNullOrEmpty(LastName) && !string.IsNullOrEmpty(City)) {
                    Application.Current.RootVisual.Dispatcher.BeginInvoke(() => OnChangesCommitted());
                }
            }
        }
    }
 
    public event EventHandler<EventArgs> ChangesCommitted;
    protected void OnChangesCommitted() {
        if (null != ChangesCommitted) {
            ChangesCommitted(this, new EventArgs());
        }
    }
    #endregion
 
    #region Clipboard Helper methods
    //public override string ToString() {
    //    return FirstName + " " + LastName;
    //}
 
    //public string ToString(string format) {
    //    if (format.Equals("Copy")) {
    //        return FirstName
    //            + "\t" + LastName
    //            + "\t" + Age
    //            + "\t" + City;
    //    }
    //    return ToString();
    //}
 
    //public static Person Create(string[] dataFields){
    //    return new Person(dataFields[(int)Fields.FirstName]
    //        , dataFields[(int)Fields.LastName]
    //        , int.Parse(dataFields[(int)Fields.Age])
    //        , dataFields[(int)Fields.FirstName]);
    //}
    //public static string[] GetDataFields(Person person) {
    //    return new string[] { person.FirstName, person.LastName, person.Age.ToString(), person.City };
    //}
 
    //public enum Fields {
    //    FirstName,
    //    LastName,
    //    Age,
    //    City
    //}
 
    public Person Clone() {
        // for demo only, please deep clone
        return (Person)this.MemberwiseClone();
    }
    #endregion
}

People.cs

public class People : ObservableCollection<Person> {
    public static People GetTestData() {
        return new People() {
            new Person("Homer", "Simpson", 38, "Springfield"),
            new Person("Marge", "Simpson", 33, "Springfield"),
            new Person("Bart", "Simpson", 8, "Springfield")
        };
    }
 
    private Person emptyPerson;
 
    public People() {
        emptyPerson = new Person();
        emptyPerson.ChangesCommitted += new EventHandler<EventArgs>(emptyPerson_ChangesCommitted);
        base.InsertItem(this.Count, emptyPerson);
    }
 
    void emptyPerson_ChangesCommitted(object sender, EventArgs e) {
        emptyPerson.ChangesCommitted -= new EventHandler<EventArgs>(emptyPerson_ChangesCommitted);
        emptyPerson = new Person();
        emptyPerson.ChangesCommitted += new EventHandler<EventArgs>(emptyPerson_ChangesCommitted);
        base.InsertItem(this.Count, emptyPerson);
    }
 
    protected override void InsertItem(int index, Person item) {
        if (index >= this.Count) {
            index = this.Count - 1;
            if (index < 0) index = 0;
        }
        base.InsertItem(index, item);
    }
 
    protected override void RemoveItem(int index) {
        Person personToRemove = this[index] as Person;
        if (emptyPerson != personToRemove) {
            base.RemoveItem(index);
        }
    }
 
    public Person SelectedPerson { get; set; }
 
    public bool IsEmptyPerson(Person person){
        return (emptyPerson == person);
    }
 
    internal Person GetPersonForPaste() {
        Person newPerson = new Person();
        if (!string.IsNullOrEmpty(emptyPerson.LastName) && !string.IsNullOrEmpty(emptyPerson.City)) {
            base.InsertItem(this.Count, newPerson);
        } else {
            base.Add(newPerson);
        }
        return newPerson;
    }
}

Next add DataGrid to Page.xaml and setup DataContext to display test data. Add reference to System.Windows.Controls.Data and following xaml to Page.xaml

<UserControl x:Class="SilverlightApplication.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    >
    <Grid x:Name="LayoutRoot" Background="White">
        <data:DataGrid x:Name="peopleDataGrid" AutoGenerateColumns="False"
            Margin="10" RowHeight="22"
            ItemsSource="{Binding}" SelectedItem="{Binding SelectedPerson,Mode=TwoWay}" >
            <data:DataGrid.Columns>
                <data:DataGridTextColumn Header="First Name" Binding="{Binding FirstName,Mode=TwoWay}" />
                <data:DataGridTextColumn Header="Last Name" Binding="{Binding LastName,Mode=TwoWay}" />
                <data:DataGridTextColumn Header="Age" Binding="{Binding Age,Mode=TwoWay}" />
                <data:DataGridTextColumn Header="City" Binding="{Binding City,Mode=TwoWay}" />
            </data:DataGrid.Columns>
        </data:DataGrid>
    </Grid>
</UserControl>

Note that peopleDataGrid’s ItemSource is set to Binding, which will bind to DataContext. Also SelectedItem is bound to SelectedPerson on People. Add following code to Page.xaml.cs to setup DataContext

public partial class Page : UserControl {
    People _data;

    public Page() {
        InitializeComponent();
        this.Loaded += new RoutedEventHandler(Page_Loaded);
    }

    void Page_Loaded(object sender, RoutedEventArgs e) {
        _data = People.GetTestData();
        DataContext = _data;
    }
}

F5 and test the application.

image

Adding Private Copy Paste Functionality

To provide DataGrid scoped Copy and Paste functionality, we will subscribe to KeyDown event and look for appropriate Key combinations

public Page() {
    InitializeComponent();
    this.Loaded += new RoutedEventHandler(Page_Loaded);
    peopleDataGrid.KeyDown += new KeyEventHandler(peopleDataGrid_KeyDown);
}

Person _copyFromPerson;
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)
        ) {
        _copyFromPerson = _data.SelectedPerson.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 == _copyFromPerson ) {
            Dispatcher.BeginInvoke(() => MessageBox.Show("Please select a Person to copy from"));
            return;
        }
        Person pasteToPerson = _data.SelectedPerson;
        pasteToPerson.BeginEdit();
        pasteToPerson.FirstName = _copyFromPerson.FirstName;
        pasteToPerson.LastName = _copyFromPerson.LastName;
        pasteToPerson.Age = _copyFromPerson.Age;
        pasteToPerson.City = _copyFromPerson.City;
        pasteToPerson.EndEdit();
    } else if (e.Key == Key.Escape) {
        _copyFromPerson = null;
    }
}

When user presses Ctrl-C to copy, we clone currently selected item and save it to a private variable. Later when user presses Ctrl-V to paste, we copy data from previously stored private variable to currently selected item.

You can use above to provide DataGrid scoped copy/paste functionality in all browsers. You can also extend functionality to work with multiple DataGrids, as long as they are all in the same application.

Excel Support

In order to provide copy/paste support to/from Excel, we need to get to data that is stored in Clipboard. Start Excel and enter following in four cells (Lisa, Simpson, 5, Springfield)

image

Select row and copy to notepad. Note that data is tab separated. This is the default clipboard format for Excel. You can also go other way, create a tab separated values in notepad and paste it into different cells in Excel. We will use same format to copy and paste data from Silverlight DataGrid to Excel and vice-versa.

Adding Internet Explorer Only functionality using Clipboard object

Internet Explorer provides access to ClipboardData object and we can get to it using Silverlight HtmlBridge functionality

Modify KeyDown as shown

   1: void peopleDataGrid_KeyDown(object sender, KeyEventArgs e) {
   2:     if (peopleDataGrid != e.OriginalSource) {
   3:         return;
   4:     }
   5:     // Copy uisng Ctrl-C
   6:     if (e.Key == Key.C &&
   7:         ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
   8:         || (Keyboard.Modifiers & ModifierKeys.Apple) == ModifierKeys.Apple)
   9:         ) {
  10:         Person copyFromPerson = _data.SelectedPerson;
  11:         string textData = copyFromPerson.FirstName + "\t" + copyFromPerson.LastName 
  12:                        + "\t" + copyFromPerson.Age + "\t" + copyFromPerson.City;
  13:         ScriptObject clipboardData = (ScriptObject)HtmlPage.Window.GetProperty("clipboardData");
  14:         if (clipboardData != null) {
  15:             bool success = (bool)clipboardData.Invoke("setData", "text", textData);
  16:         } else {
  17:             Dispatcher.BeginInvoke(() => MessageBox.Show("Sorry, this functionality is only avaliable in Internet Explorer."));
  18:             return;
  19:         }
  20:     }
  21:     // Paste using Ctrl-V
  22:     else if (e.Key == Key.V &&
  23:         ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
  24:         || (Keyboard.Modifiers & ModifierKeys.Apple) == ModifierKeys.Apple)
  25:         ) {
  26:         ScriptObject clipboardData = (ScriptObject)HtmlPage.Window.GetProperty("clipboardData");
  27:         if (null == clipboardData) {
  28:             Dispatcher.BeginInvoke(() => MessageBox.Show("Sorry, this functionality is only avaliable in Internet Explorer."));
  29:             return;
  30:         }
  31:         string textData = null;
  32:         if (clipboardData != null) {
  33:             textData = (string)clipboardData.Invoke("getData", "text");
  34:         } 
  35:         Person copyFromPerson = null;
  36:         string[] rows = textData.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
  37:         if (0 != rows.Length) {
  38:             string[] fields = rows[0].Split(new string[] { "\t" }, StringSplitOptions.None);
  39:             if (4 == fields.Length) {
  40:                 copyFromPerson = new Person(fields[0], fields[1], int.Parse(fields[2]), fields[3]);
  41:             }
  42:         }
  43:         if (null == copyFromPerson) {
  44:             Dispatcher.BeginInvoke(() => MessageBox.Show("Please select a Person to copy from"));
  45:             return;
  46:         }
  47:         Person pasteToPerson = _data.SelectedPerson;
  48:         pasteToPerson.BeginEdit();
  49:         pasteToPerson.FirstName = copyFromPerson.FirstName;
  50:         pasteToPerson.LastName = copyFromPerson.LastName;
  51:         pasteToPerson.Age = copyFromPerson.Age;
  52:         pasteToPerson.City = copyFromPerson.City;
  53:         pasteToPerson.EndEdit();
  54:     } 
  55: }
 
For Ctrl-C, we first build tab separated list of person fields and next use clipboardata to setData to clipboard via Invoke. Conversely on Ctrl-V, we use clipboardData to getdata and parse data using tab into array of fields. That array of fields is used to build private copy person that is used to copy data into currently selected item. To test functionality, Copy Data from Excel and paste it into empty row in DataGrid. New Person (Lisa) is added to DataGrid. Next select first row (Homer) in DataGrid and Paste it into Excel.

Adding Cross Browser functionality image

Access to ClipboardData is limited to Internet Explorer only. If you try to run code in FireFox, clipboardData returns null. However, I did not want to tell my users that copy/paste functionality is only available in Internet Explorer. (Specially after having told them about Silverlight Cross Browser advantage!). So I decided to do some search. I found couple of approaches that use Flash and/or JavaScript, but did not come up with a Silverlight only solution.

If you play around with Silverlight, you will notice that TextBox control provides Copy and Paste functionality in multiple browsers. So I fired up Reflector and tried to see what TextBox was doing. Alas, I quickly ran into brick wall. It calls into underlying system. That was not going to work for our Transparent code. However that got me thinking… If DataGrid will not support, can we use TextBox as a helper proxy? It turns out you can! Instead of trying to use Internet Explorer specific ClipboardData object, just delegate task to TextBox. TextBox does the heavy lifting and interacts with clipboard in multiple browsers.

Add new class call ClipboardTextBox.cs as shown

public class ClipboardTextBox  : TextBox{
    public ClipboardTextBox() {
        AcceptsReturn = true;
    }
    protected override void OnKeyDown(KeyEventArgs e) {
        base.OnKeyDown(e);
    }
    public void ProcessKeyDown(KeyEventArgs e) {
        OnKeyDown(e);
    }
}

Next add ClipboardTextBox control to Page.xaml

<src:ClipboardTextBox x:Name="dataTextBox" Visibility="Collapsed"/>

Also add src as xml namespace declaration to UserControl start tag

xmlns:src="clr-namespace:SilverlightApplication"

Modify KeyDown 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)
        ) {
        Person copyFromPerson = _data.SelectedPerson;
        string textData = copyFromPerson.FirstName + "\t" + copyFromPerson.LastName
                       + "\t" + copyFromPerson.Age + "\t" + copyFromPerson.City;
        dataTextBox.Text = textData.ToString();
        dataTextBox.SelectAll();
        dataTextBox.ProcessKeyDown(e);
    }
    // Paste using Ctrl-V
    else if (e.Key == Key.V &&
        ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
        || (Keyboard.Modifiers & ModifierKeys.Apple) == ModifierKeys.Apple)
        ) {
        dataTextBox.Text = string.Empty;
        dataTextBox.ProcessKeyDown(e);
        string textData = dataTextBox.Text;
        Person copyFromPerson = null;
        string[] rows = textData.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
        if (0 != rows.Length) {
            string[] fields = rows[0].Split(new string[] { "\t" }, StringSplitOptions.None);
            if (4 == fields.Length) {
                copyFromPerson = new Person(fields[0], fields[1], int.Parse(fields[2]), fields[3]);
            }
        }
        if (null == copyFromPerson) {
            Dispatcher.BeginInvoke(() => MessageBox.Show("Please select a Person to copy from"));
            return;
        }
        Person pasteToPerson = _data.SelectedPerson;
        pasteToPerson.BeginEdit();
        pasteToPerson.FirstName = copyFromPerson.FirstName;
        pasteToPerson.LastName = copyFromPerson.LastName;
        pasteToPerson.Age = copyFromPerson.Age;
        pasteToPerson.City = copyFromPerson.City;
        pasteToPerson.EndEdit();
    }
}

Code above is similar to one for Internet Explorer, except for replacement of dataTextBox for clipboardData. For Ctrl-C, we set data into TextBox, select all text and pass KeyEventArgs to TextBox, which results in data being copied to clipboard. For Ctrl-V, we clear TextBox, process KeyDown and read Text to get paste data to process. F5 and run the application. Start FireFox. Copy data from Excel, Paste it into FireFox. Try other way around, copy data from Silverlight DataGrid and paste it into Excel.

image image

We now have a basic (one row) copy and paste functionality that is 100% Silverlight and cross browser. Lets refactor code to create a reusable ClipboadHelper class.

ClipboardHelper

ClipboardHelper provides methods to Get and Set clipboard data that works in multiple browsers. It internally uses an instance of ClipboardTextBox to carry out actual operations. Here is code for ClipboardHelper.cs

public static class ClipboardHelper {

    private static ClipboardTextBox dataTextBox;

    static ClipboardHelper() {
        dataTextBox = new ClipboardTextBox();
    }

    public static void SetData(KeyEventArgs e, string textData) {
        dataTextBox.Text = textData;
        dataTextBox.SelectAll();
        dataTextBox.ProcessKeyDown(e);
    }

    public static string GetData(KeyEventArgs e) {
        dataTextBox.Text = string.Empty;
        dataTextBox.ProcessKeyDown(e);
        return dataTextBox.Text;
    }
}

Usage:
In order to incorporate cross browser clipboard functionality, just add reference to MD.Silverlight.Utilities and call methods on ClipboardHelper class

To copy data, handle KeyDown event with key combinations (Ctrl-C) and call

// set copy data
ClipboardHelper.SetData(e, textData);

To past data, handle KeyDown event with key combinations (Ctrl-V) and call

// get paste data
string textData = ClipboardHelper.GetData(e);

Hopefully ClipboardHelper will enable you to provide cross browser clipboard functionality in Silverlight till copy paste functionality is built into the base framework. In the next post I will extend functionality to support multiple rows and introduce reusable DataGridCopyPasteService, that imparts copy paste functionality to any DataGrid.

Source Code: CopyPaste.zip

Note: Source code includes enhanced demo application with 4 tabs and a reusable MD.Silverlight.Utilities.dll class library.

CopyPasteApp1 

Tab1 show cases for private copy paste functionality

Tab2 show cases Internet Explorer specific copy paste functionality

Tab3 show cases usage of ClipboardTextBox for copy paste functionality

Tab4 show cases usage of ClipboardHelper for cross browser reusable copy paste functionality

 

 

 

Technorati Tags:
ConfigSwitcher: ServiceReferences.ClientConfig Switcher Utility

Silverlight 2 uses ServiceReferences.ClientConfig to store WCF related configuration. It is packaged and deployed along with the application in XAP file. Since XAP is a compressed file container (similar to zip), it is possible to uncompress the XAP file, change the desired configuration setting, and compress the results into a new XAP file for deployment. However if you have multiple sites(dev, qa, staging, prod/release, training) that you need to deploy to, this process can become very cumbersome and error prone.

In order to automate the creation of XAP file for the appropriate target environment, I wrote a small console utility that switches proper configuration file based on the selected solution configuration. You provide different configuration files for each target site, appended with configuration name, like ServiceReferences.ClientConfig.qa, ServiceReferences.ClientConfig.Release and so on.

image

Then, you can create new Configurations using Configuration Manager:

 image

Next you setup the switcher utility in pre and post build

image

Select the desired build configuration

image

Now when you build the Silverlight application, configuration switcher will switch in proper configuration file. It does this by renaming file in pre build stage and renaming it back in post build. For example if you are building a release build, following happens during pre and post build.

Pre build

ServiceReferences.ClientConfig –> rename –> ServiceReferences.ClientConfig.build

ServiceReferences.ClientConfig.Release–> rename –> ServiceReferences.ClientConfig

Post Build

ServiceReferences.ClientConfig –> rename –> ServiceReferences.ClientConfig.Release

ServiceReferences.ClientConfig.build –> rename –> ServiceReferences.ClientConfig

Here is the code for the program:

namespace ConfigSwitcherApp {
    class Program {
        static void Main(string[] args) {
            //
            if (0 == args.Length || args[0] == "Debug" || args[0] == "DEBUG") {
                return;
            }
            string configurationName = args[0];
            string preBuild = args[1];
            string projectDir = args[2];
            //
            if ("True" == preBuild) {
                RunPreBuild(configurationName, projectDir);
            }
            else if ("False" == preBuild){
                RunPostBuild(configurationName, projectDir);
            }

        }

        private static void RunPreBuild(string configurationName, string projectDir) {
            string path = projectDir + "\\ServiceReferences.ClientConfig";
            string newPath = projectDir + "\\ServiceReferences.ClientConfig.build";
            File.Move(path, newPath);
            //
            if (!string.IsNullOrEmpty(configurationName)) {
                path = projectDir + "\\ServiceReferences.ClientConfig." + configurationName;
                newPath = projectDir + "\\ServiceReferences.ClientConfig";
                File.Move(path, newPath);
            } 

        }

        private static void RunPostBuild(string configurationName, string projectDir) {
            string path = null;
            string newPath = null;
            if (!string.IsNullOrEmpty(configurationName)) {
                path = projectDir + "\\ServiceReferences.ClientConfig";
                newPath = projectDir + "\\ServiceReferences.ClientConfig." + configurationName;
                File.Move(path, newPath);
            }
            path = projectDir + "\\ServiceReferences.ClientConfig.build";
            newPath = projectDir + "\\ServiceReferences.ClientConfig";
            File.Move(path, newPath);
            //
        }
    }
}

Usage:

For Pre build

C:\ConfigSwitcherApp.exe $(ConfigurationName) True $(ProjectDir)

For Post build

C:\ConfigSwitcherApp.exe $(ConfigurationName) False $(ProjectDir)

Console Application takes three parameters (all required)

Parameter 1 : Configuration Name for file suffix, use Visual Studio macro - $(ConfigurationName)

Parameter 2 : True for pre build, to switch in target config file and False for post build to switch out target config file (and switch in design time config file)

Parameter 3 : Project directory, use Visual Studio macro - $(ProjectDir)

Source Code: ConfigSwitcherApp.zip

Hopefully this helps you to automate you build process and eliminate errors when deploying to multiple sites.

Technorati Tags:
More Posts