Manish Dalal's blog

Exploring .net!
Silverlight 3 Custom Sorting with Paging Support

This post updates the custom sorting technique I had originally posted for Silverlight 2 and adds paging support. image

Introduction

Silverlight 3 includes PagedCollectionView class, which provides grouping, sorting, and paging functionality for any data source that implements the IEnumerable interface. This works as long as all the data that you need to sort (and/or page) is already fetched to the client. However when you are retrieving only a limited set of data from the server at a time, you need an alternative solution that does sorting on the server. 

One way to achieve this is to use .net RIA services. In particular, DomainDataSource control provides automatic server sorting and paging support. However, if you are not using .net RIA services, than you will need to implement ICollectionView to handle sorting and IPagedCollectionView to handle paging. This post updates the SortableCollectionView class that I had introduced in the previous custom sorting post and adds paging support by implementing IPagedCollectionView as PagedSortableCollectionView. I will assume that you have already read the previous article!

Custom Sorting

SortableCollectionView class implements ICollectionView and provides custom sorting support. Sorting functionality encompasses CanSort and SortDescriptions properties. For custom sorting, we need to listen for changes to SortDescriptions collection, and in turn refresh the data for new sort options. (A SortDescription defines the direction and the property name to be used as the criteria for sorting a collection.)image

For Silverlight 3, SortableCollectionView was updated to provide implementation for several methods that were previously not used. In addition, we no longer need the CustomSortDescription class as the framework will automatically call Refresh method any time there is a change in the SortDescriptions collection. This functionality is enabled by implementing DeferRefresh method and utilizing the DeferRefreshHelper class. As the name implies, DeferRefresh method just delays the automatic refresh of data till all the needed changes are completed and ready to process. When the DeferRefreshHelper object is disposed, we fire the OnRefresh event, which is our cue that we need new data from server.

Following code snippet shows the DeferRefresh method returning DeferRefreshHelper object with Refresh method passed as callback:
 
public IDisposable DeferRefresh() {
    return new DeferRefreshHelper(() => Refresh());
}

private class DeferRefreshHelper : IDisposable {
    private Action _callback;

    public DeferRefreshHelper(Action callback) {
        _callback = callback;
    }

    public void Dispose() {
        _callback();
    }
}
 
Refresh method fires OnRefresh event and passes in RefreshEventArgs, providing access to SortDescriptions collection. A call is made to the server to get new data, passing in proper sort descriptors information. In the callback, we clear existing data and load new sorted data.
 
public void Refresh() {
    if (null != OnRefresh) {
        OnRefresh(this, new RefreshEventArgs() { SortDescriptions = SortDescriptions });
    }
}
imagePaging

Silverlight 3 introduced a new interface IPagedCollectionView to support paging and the DataPager control that can utilize the IPagedCollectionView and provides UI to page the data. IPagedCollectionView includes method to navigate to different pages. PagedSortableCollectionView implements the IPagedCollectionView interface on the base SortableCollectionView. The main method that does paging is MoveToPage:

public bool MoveToPage(int pageIndex) {
    if (pageIndex < -1) {
        return false;
    }
    if ((pageIndex == -1) && (this.PageSize > 0)) {
        return false;
    }
    if ((pageIndex >= this.PageCount) || (this._pageIndex == pageIndex)) {
        return false;
    }
    //
    try {
        IsPageChanging = true;
        if (null != PageChanging) {
            PageChangingEventArgs args = new PageChangingEventArgs(pageIndex);
            OnPageChanging(args);
            if (args.Cancel) return false;
        }
        //
        _pageIndex = pageIndex;
        Refresh();
        IsPageChanging = false;
        OnPropertyChanged("PageIndex");
        OnPageChanged(EventArgs.Empty);
        return true;
    } finally {
        IsPageChanging = false;
    }
}

Whenever there is a change in paging, Refresh method is called, which in turn fires OnRefresh event and we get new data from server.

Sample Applicationimage

Sample application is similar to the one from previous post. On the server side there is no change, I have used that same Person model that I had used earlier. Similarly I have used the same PeopleService to get data. PeopleService exposes a GetData method that build list of persons and then optionally sorts them and returns requested paged data back to the client.

On the client side, PeopleViewModel exposes PagedSortableCollectionView as DataList for data biding. It also subscribes to the OnRefresh event and calls the server to get data. Following code snippet shows the client GetData method which is used to build paging and sorting parameters and subsequently fetch data from server using the PeopleService.GetData method.

private void GetData() {
    int take = DataList.PageSize;
    int skip = DataList.PageIndex * DataList.PageSize;
    //
    Dictionary<string, string> sortBy = new Dictionary<string, string>();
    foreach (SortDescription sortDesc in DataList.SortDescriptions) {
        sortBy.Add(sortDesc.PropertyName, (sortDesc.Direction == ListSortDirection.Ascending ? "ASC" : "DESC"));
    }
    //
    PeopleServiceClient proxy = new PeopleServiceClient();
    proxy.GetDataCompleted +=new EventHandler<GetDataCompletedEventArgs>(proxy_GetDataCompleted);
    proxy.GetDataAsync(skip, take, sortBy);
}

image

Steps to get started
  1. Include PagedSortableCollectionView,  SortableCollectionView and RefreshEventArgs classes in your Silverlight client code.
  2. Handle the OnRefresh event and call server with sorting and paging information.
  3. Clear existing data and reload the results.

Source code: CustomSortingSL3.zip

Technorati Tags:
Silverlight 3 ComboBox Control

This post outlines technique for displaying ComboBox control with .net RIA services to handle Foreign Key scenarios.

When ComboBox control was introduced as part of Silverlight 2, I blogged about a technique to use ComboBox control in foreign key scenarios, to workaround the lack of SelectedValue\SelectedValuePath property. I further enhanced the technique to handle cascading selection as outlined in this post. Fast forward to Silverlight 3. ComboBox control still does not have SelectedValue\SelectedValuePath property and the same technique still works. Moreover, if you are using Silverlight 3 with .net RIA services, majority of the code is automatically generated by the framework. In fact, with Silverlight 3 and .net RIA services there is no requirement for SelectedValuePath property!

Background

In business applications with relation database as backend data store, foreign keys are often used to form association between entities. When working with such an entity in UI, a friendly description is shown to user, while system handles the actual foreign key value behind the scene. For ComboBox, this translates to displaying foreign key entity with a friendly description (perhaps using a custom template), and storing the results of selection as foreign key value to the base entity. However, ComboBox does not provide a SelectedValuePath property to set selected foreign key value back to the base entity. Simplest workaround is to use SelectedItem property. By adding Foreign key entity as a property to the base entity, we can data bind SelectedItem to the foreign key entity, automating the whole process.

With .net RIA services, you can do this easily, just by setting Include attribute on the associated foreign key entity. This results in foreign key entity being added a property to the base entity, which can then be data bound to SelectedItem property. Now when SelectedItem sets new foreign key entity, property setter in turn sets related foreign key value property, as indicated by the Association attribute. All of this code is already in place, generated automatically by the .net RIA services code generator. All you have to do is to data bind to SelectedItem and take care of ItemsSource.

Modelimage

I will use the same Address/City model that I had previously used for Silverlight 2 ComboBox post, so that you can contrast the code, except that this time I will start with a database model. Model consists of Address and City tables.

Applicationimage

Create a Silverlight Navigation Application. Next add the ADO.NET Entity Data Model as AddressModel and generate the AddressEntities from database tables, Address and City.

Next add a Domain Service Class as AddressService, adding Address and City entities. Select Enable editing and also generate associated meta data.

In the class AddressMetadata, decorate City field with Include attribute.

internal sealed class AddressMetadata {

    // Metadata classes are not meant to be instantiated.
    private AddressMetadata() {
    }

    public Guid AddressId;
    [Include]
    public City City;
...

This will ensure that City entity in included as a property in the Address class and it is sent to client. Also modify GetAddress method in AddressService class to include City in retrieved data.

public IQueryable<Address> GetAddress() {
    return this.Context.Address.Include("City");
}

Compile and build the application.

Auto generated City Property

Find the Generated Code folder (unhide by clicking Show All Code for client application) and navigate to City field in Address entity class. Note that City property is decorated with Association attribute that ties Address.CityId foreign key to City.CityId primary key. Also note the Set property assessor for City property. When ever a new City value is set, it also automatically sets CityId property to proper foreign key value. Following is the auto generated code for the City Property:

[Association("City_Address", "CityId", "CityId", IsForeignKey = true)]
[XmlIgnore()]
public City City {
    get {
        if ((this._city == null)) {
            this._city = new EntityRef<City>(this, "City", this.FilterCity);
        }
        return this._city.Entity;
    }
    set {
        City previous = this.City;
        if ((previous != value)) {
            this.ValidateProperty("City", value);
            if ((previous != null)) {
                this._city.Entity = null;
                previous.Address.Remove(this);
            }
            this._city.Entity = value;
            if ((value != null)) {
                value.Address.Add(this);
                this.CityId = value.CityId;
            } else {
                this.CityId = default(Guid);
            }
            this.RaisePropertyChanged("City");
        }
    }
}

So far, .net RIA services has generated all the code necessary for City property and tying it to corresponding CityId foreign key property. All that is now needed for us to do is to add code to allow ComboBox to find a City entity in the ItemsSource entity list.

Shared City Partial Class

Add shared City partial class as City.shared.cs to the Silverlight server web application.

public partial class City {

    public override bool Equals(object obj) {
        if (null == obj) return false;
        City cityToCompare = obj as City;
        if (null == cityToCompare) return false;
        return CityId.Equals(cityToCompare.CityId);
    }

    public override int GetHashCode() {
        return CityId.GetHashCode();
    }
}

This code will be copied over to client and is used to find City entity from list of cities.

City List Provider

Next add class CityListProvider to the Silverlight client Application.

public class CityListProvider {
    AddressContext _dc;
    public AddressContext DomainContext {
        set {
            _dc = value;
            _dc.Load<City>(_dc.GetCityQuery());
        }
    }
    public EntityList<City> CityList {
        get {
            return _dc.Cities;
        }
    }
}

ComboBox XAML

Add a new page to display Address data using DataGrid. Following shows City column XAML snippet:

<data:DataGridTemplateColumn Header="City">
    <data:DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding City.CityName}" />
        </DataTemplate>
    </data:DataGridTemplateColumn.CellTemplate>
    <data:DataGridTemplateColumn.CellEditingTemplate>
        <DataTemplate>
            <ComboBox ItemsSource="{Binding CityList, Source={StaticResource cityListProvider}}"
                      SelectedItem="{Binding City, Mode=TwoWay}"
                      DisplayMemberPath="CityName" />
        </DataTemplate>
    </data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>

Note that ComboBox control’s SelectedItem is directly bound to the City property. User is shown friendly “CityName” by setting DisplayMemberPath. Also add CityListProvider as a resource to the root Grid control.

Modify code behind to set DataGrid ItemsSource to Address property on AddressContext (domain context) and load addresses using LoadAddress method of address domain context.

AddressContext _dc;

void AddressList_Loaded(object sender, RoutedEventArgs e) {
    _dc = new AddressContext();
    addressDataGird.ItemsSource = _dc.Addresses;
    //
    CityListProvider cityListProvider = LayoutRoot.Resources["cityListProvider"] as CityListProvider;
    cityListProvider.DomainContext = _dc;
    //
    _dc.Load<Address>(_dc.GetAddressQuery());
}

imageCompile and test the application. Note that appropriate City field is automatically selected when ComboBox drop down opens, thanks to Equality override in City class. When a new City is selected from the drop down, CityId is auto updated to reflect the new selection.

Use of ComboBox for Implementing foreign key scenarios is now completely automatic with aid of .net RIA service. Framework generates necessary Foreign Key entity property and also generates corresponding code to keep underlying foreign key value in sync.

Steps to get started
  1. Use Include attribute on foreign key entity. Also include foreign key entity in data retrieval.
  2. Provide Equality override code to find entity in entity list
  3. Data bind ItemsSource to entity list provider
  4. Data bind SelectedItem directly to foreign key entity

Source Code: SL3ComboBox.zip (560KB) - (Updated Sep-21-2009)

Technorati Tags:
Silverlight ObjectDataSource

A DataSource control represents a data object that acts as a data-interface for the data bound controls. In terms of MVVM pattern, it represents the ViewModel. Silverlight 3 introduces the DomainDataSource as part of .net RIA Data Services, that can be used as a binding source for DataGrid, DataForm and other data-bound controls. However, if you are still working with Silverlight 2 or researching alternatives , this post introduces ObjectDataSource.

ObjectDataSourceObjectDataSource

Typically when developing business applications that entail editing one or more entity, you have to add support for state management, implement  IEditableObject,  and wire up the validation logic, among other essential stuff. And you have to repeat the same process for every entity that needs editing. ObjectDataSource provides one way to encapsulate above functionality and make it reusable.

If you have followed my previous posts on building business applications with Silverlight, you know that it takes a fair amount code for add, update, delete and validation functionality. The ObjectDataSource works with custom business objects and supports automatic data retrieval, add, update, delete, custom paging, custom sorting and custom validation of data, declaratively without extensive code.

It wraps the underlying entity in a DataItem class, and provides automatic state management, as well as implementing IEditableObject support. ObjectDataSource provides a DataList property to facilitate DataBinding to DataGrid and a SelectedItem property for formview scenarios. You can either manually load data or implement IObjectDataProvider to automate data loading and paging.

Baseline Application

Before going into details of ObjectDataSource, lets first build a simple baseline starter application. Create a new SilverlightApplication and the corresponding Web Application to host and test the SilverlightApplication. We will use a WCF service to provide data to client.

First add a reference for System.Runtime.Serialization. Next create a data contract, as a Person class that will be sent back to the client as shown:

[DataContract]
public class Person {
    [DataMember]
    public string PersonId { get; set; }
    [DataMember]
    public string FirstName { get; set; }
    [DataMember]
    public string LastName { get; set; }
    [DataMember]
    public int Age { get; set; }
}

Next add a Silverlight-enabled WCF service and call it PeopleSevice. Add method GetData to the code behind PeopleService.svc.cs as shown.

[OperationContract]
public List<Person> GetData() {
    List<Person> personList = new List<Person>();
    for (int i = 0; i < 10; i++) {
        personList.Add(new Person() {
            PersonId = Guid.NewGuid().ToString(),
            FirstName = string.Format("First Name {0}", i),
            LastName = string.Format("Last Name {0}", i),
            Age = i
        });
    }
    return personList;
}
 
Here we are just creating test data on demand. In an actual production application, you will be calling database and paging, perhaps using Linq Skip and Take functionality or Top and RowNumber features in SQL (see the sample application included in download for paging, sorting and filter functionality).

Now add a service reference to the the SilverlightApplication client and call it PeopleServiceReference. (use Discover button to see PeopleService). Next add System.Windows.Control.Data reference to the SilverlightApplication project.

Add the 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" >
            <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:DataGrid.Columns>
        </data:DataGrid>
    </Grid>
</UserControl>

And following code to page.xaml.cs code behind (note: error handling omitted for readability)

public partial class Page : UserControl {
    public Page() {
        InitializeComponent();
        this.Loaded += new RoutedEventHandler(Page_Loaded);
    }
    void Page_Loaded(object sender, RoutedEventArgs e) {
        GetData();
    }
    private void GetData() {
        PeopleServiceClient proxy = new PeopleServiceClient();
        proxy.GetDataCompleted += new EventHandler<GetDataCompletedEventArgs>(proxy_GetDataCompleted);
        proxy.GetDataAsync();
    }
    void proxy_GetDataCompleted(object sender, GetDataCompletedEventArgs e) {
        this.peopleDataGrid.ItemsSource = e.Result;
    }
}

imageCompile and test the application.

We now have a baseline application that gets data from server and show that to user. Next we will use ObjectDataSource to enable change, add, delete, undelete and save functionality along with custom validation.

Application using ObjectDataSource

Add Neon.Windows.Data reference to client SilverlightApplication. This will bring Neon.Window.* namespace into the project and make available ObjectDataSource and related artifacts.

In order for ObjectDataSource to manage underlying data (entity), you have to implement IDataItemData interface. This allows DataItem to make copy of data to save original data and find data using Key field value. Add a partial class Person.cs as shown to the client SilverlightApplication (Note: Use the same namespace declaration as the Person class generated by WCF Add Service Reference)

namespace SilverlightApplication.PeopleServiceReference {
    public partial class Person : IDataItemData<Person> {
        public Person Copy() {
            return (Person)this.MemberwiseClone();
        }
        public bool ReInit(Person data) {
            return false;
        }
        public object KeyValue {
            get { return this.PersonId; }
        }
        public bool Equals(Person other) {
            return this.PersonId.Equals(other.PersonId, StringComparison.InvariantCultureIgnoreCase);
        }
    }
}

ReInit method provides you opportunity to copy data saved in database to the client cached data. This would be useful for fields like version and auto generated field values. Now just modify proxy_GetDataCompleted in Page.xaml.cs as shown

void proxy_GetDataCompleted(object sender, GetDataCompletedEventArgs e) {
    ObjectDataSource<Person> ds = new ObjectDataSource<Person>();
    ds.LoadData(e.Result);
    peopleDataGrid.ItemsSource = ds.DataList;
}

Also modify DataGrid.Columns entry in Page.xaml as follows

<data:DataGrid.Columns>
    <data:DataGridTextColumn Header="First Name" Binding="{Binding Data.FirstName,Mode=TwoWay}" />
    <data:DataGridTextColumn Header="Last Name" Binding="{Binding Data.LastName,Mode=TwoWay}" />
    <data:DataGridTextColumn Header="Age" Binding="{Binding Data.Age,Mode=TwoWay}" />
</data:DataGrid.Columns>

DataItemNote that binding for for DataGridTextColumns have been changed from PropertyName to Data.PropertyName. This is because ObjectDataSource wraps each  Person entity into a DataItem object and exposes it as a Data property. This allows DataItem to carry out automatic state management and provide IEditableObject interface implementation.

imageIObjectDataProvider

You can always manually load data on demand, but it is easier to offload that functionality to ObjectDataSource. ObjectDataSource knows how to retrieve data using any provider that implements IObjectDataProvider. Provider based pattern allows ObjectDataSource to work with any data store, all you have to do is to implement IObjectDataProvider and interact with your custom data store.

Add a new class called PeopleProvider as shown:

namespace SilverlightApplication.Providers {
    public class PeeopleProvider : IObjectDataProvider<Person> {
        public void GetDataAsync(string objectInfo, System.Collections.Generic.Dictionary<string, string> sortExpression
            , int startRow, int endRow, ObservableCollection<Neon.Web.Data.WhereParameterData> whereParameters) {
            PeopleServiceClient proxy = new PeopleServiceClient();
            proxy.GetDataCompleted += new EventHandler<GetDataCompletedEventArgs>(proxy_GetDataCompleted);
            proxy.GetDataAsync();
        }
        public event EventHandler<ObjectDataEventArgs<Person>> ObjectDataAvaliable;
        //
        void proxy_GetDataCompleted(object sender, GetDataCompletedEventArgs e) {
            OnObjectDataAvaliable(e.Result);
        }
        protected virtual void OnObjectDataAvaliable(ObservableCollection<Person> dataCollection) {
            if (null != ObjectDataAvaliable) {
                Application.Current.RootVisual.Dispatcher.BeginInvoke(() =>
                ObjectDataAvaliable(this, new ObjectDataEventArgs<Person>() { DataCollection = dataCollection })
                );
            }
        }
        //
        public void SaveDataAsync(string objectInfo, ObservableCollection<Neon.Web.Data.ISavableData<Person>> dataList) {
            throw new NotImplementedException();
        }
        public event EventHandler<SaveDataCompletedEventArgs<Person>> SaveDataCompleted;
    }
}

For now we will just concentrate on data retrieval part, we will see how to Save data later.

Comment out proxy_GetDataCompleted in Page.xaml.cs and change GetData as shown

private void GetData() {
    ObjectDataSource<Person> ds = new ObjectDataSource<Person>();
    peopleDataGrid.ItemsSource = ds.DataList;
    ds.AutoFetchData = true;
    ds.ObjectDataProvider = new PeopleProvider();
}

 

 

 

 

Here we are providing ObjectDataSource with PeopleProvider to fetch data on demand. Also with AutoFetchData set to true, ObjectDataSource will automatically fetch data on load.

TypedDataSource

Setting up a data source in code works, but in the sprit of declarative code design, it will be great if we can define the data source in XAML. We can easily do that by setting up a typed data source. Create a new class call PeopleDataSource as shown (optionally you can override methods as needed)

namespace SilverlightApplication.Controls {
    public class PeopleDataSource : ObjectDataSource<Person> {
    }
}

Now comment out GetData code in page.xaml.cs and add following xaml markup 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"
    xmlns:ctrl="clr-namespace:SilverlightApplication.Controls"
    xmlns:prov="clr-namespace:SilverlightApplication.Providers"
    >
    <UserControl.DataContext>
        <ctrl:PeopleDataSource AutoFetchData="True">
            <ctrl:PeopleDataSource.ObjectDataProvider>
                <prov:PeopleProvider/>
            </ctrl:PeopleDataSource.ObjectDataProvider>
        </ctrl:PeopleDataSource>
    </UserControl.DataContext>
    <Grid x:Name="LayoutRoot" Background="White">
        <data:DataGrid x:Name="peopleDataGrid" AutoGenerateColumns="False" 
            Margin="10" RowHeight="22" ItemsSource="{Binding DataList}" >

Note that DataGrid ItemsSource is data bound to DataList property of PeopleDataSource. Compile and test the application.

ItemState

ObjectDataSource automatically keeps track of state of each item. An item can be in any one of the following states: NoChange, Changed, New, Deleted or Saved. We can display state of the item using built in RowStateViewModel property on the DataItem.

Add a new DataGridTemplateColumn as the first column in DataGrid.

<data:DataGridTemplateColumn Width="6" MinWidth="6" MaxWidth="6" 
                             CanUserResize="False" CanUserReorder="False" CanUserSort="False" IsReadOnly="True">
    <data:DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <Border Width="6" DataContext="{Binding RowStateViewModel}" Background="{Binding StateBackground}" 
                    ToolTipService.ToolTip="{Binding StateToolTip}"/>
        </DataTemplate>
    </data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>

Make some changes to data, note that row state changes to Changed and that new state is also reflected on UI.

image After edit to FirstName =>image

Add

You can display a custom UI and add data to the DataList. Alternatively, you can also take advantage of inline add functionality for DataGrid by setting AutoAddRow property on the ObjectDataSource to true.

Modify PeopleDataSource declaration in Page.xaml and set AutoAddRow property to true.

<ctrl:PeopleDataSource AutoFetchData="True" AutoAddRow="True">

Now DataGrid allows addition of new Person.

  image Add New Person =>image

Delete and Undelete

Add following code to page.xaml.cs to enable users to delete data

public Page() {
    InitializeComponent();
    this.Loaded += new RoutedEventHandler(Page_Loaded);
    peopleDataGrid.KeyDown += new KeyEventHandler(peopleDataGrid_KeyDown);
    peopleDataGrid.BeginningEdit += new EventHandler<DataGridBeginningEditEventArgs>(peopleDataGrid_BeginningEdit);
}
void peopleDataGrid_KeyDown(object sender, KeyEventArgs e) {
    if (e.Key == Key.Delete) {
        ((PeopleDataSource)this.DataContext).Delete(new DataItem<Person>[] { (DataItem<Person>)peopleDataGrid.SelectedItem });
    }
}
void peopleDataGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e) {
    DataItem<Person> dataItem = e.Row.DataContext as DataItem<Person>;
    if (DataItemState.Deleted == dataItem.State) {
        MessageBoxResult result = MessageBox.Show("Can not change Deleted item, do you want to Undelete and make the item editable?"
            , "Error", MessageBoxButton.OKCancel);
        if (result == MessageBoxResult.OK) {
            dataItem.State = dataItem.PreviousState;
            e.Cancel = false;
        } else {
            e.Cancel = true;
        }
    }
}

In the KeyDown handler, we are listening for the delete key and call the Delete method of ObjectDataSource. When user deletes a row, its state is changed to Deleted and row state color it is changed to DarkGray.

In the BeginginEdit, we are checking for the delete state to prevent user for deleting data. If selected item has been deleted, we alert the user and provide an option to undelete. This is possible because ObjectDataSource never removes deleted item from collection, it is just marked as deleted, till data is actually saved to backend data store.

image  =>image=>image

imageValidation

You can hook up  your own validation code by implementing in IDataItemFactory. If provided, ObjectDataSource will use IDataItemFactory to build new DataItem. This allows you to supply your own DataItem derivative, and/or customize setup by providing a validator.

Lets first create a PersonValidator class as shown (implementing last name required check)

namespace SilverlightApplication.Data {
    public class PersonValidator : DataItemValidator<Person> {
        public PersonValidator(DataItem<Person> dataItem)
            : base(dataItem) {
        }
        public override void Validate(string propertyName) {
            if ("LastName" == propertyName) {
                if (string.IsNullOrEmpty(this.DataItem.Data.LastName)) {
                    RegisterError(propertyName, "LastName is required");
                } else {
                    ClearError(propertyName);
                }
            }
        }
        public override void Validate() {
            Validate("LastName");
        }
    }
}

Now create the PersonDataItemFactory as shown

namespace SilverlightApplication.Providers {
    public class PersonDataItemFactory : DataItemFactory<Person> {
        public override DataItem<Person> GetEmtpyDataItem() {
            DataItem<Person> personDataItem = base.GetEmtpyDataItem(
                new Person() { PersonId = Guid.NewGuid().ToString() });
            personDataItem.Validator = new PersonValidator(personDataItem);
            personDataItem.State = DataItemState.New;
            return personDataItem;
        }
        public override DataItem<Person> GetDataItem(Person data) {
            DataItem<Person> personDataItem = base.GetDataItem(data);
            personDataItem.Validator = new PersonValidator(personDataItem);
            return personDataItem;
        }
    }
}

Modify Page.xaml to set PeopleDataSource.DataItemFactory to our custom PersonDataItemFactory as shown:

<UserControl.DataContext>
    <ctrl:PeopleDataSource AutoFetchData="True" AutoAddRow="True">
        <ctrl:PeopleDataSource.ObjectDataProvider>
            <prov:PeopleProvider/>
        </ctrl:PeopleDataSource.ObjectDataProvider>
        <ctrl:PeopleDataSource.DataItemFactory>
            <prov:PersonDataItemFactory/>
        </ctrl:PeopleDataSource.DataItemFactory>
    </ctrl:PeopleDataSource>
</UserControl.DataContext>

imageNow when you add new item or change existing item and do not provide last name, user is displayed an error. Note above is just one way of setting up validation. You can setup your own custom validation framework (perheps using meta data to automate routine validation.image)

Save

ObjectDataSource and DataItem keeps track of all the changes. It also provides access to both original and changed data. In order to save data, you can either manually inspect DataItem state and write code or better yet, just write your save routine in SaveDataAsync method of IObjectDataProvider. This allows you to take advantage of Batched saved functionality and also frees you from state management when data is saved.

First we need to modify PeopleService to provide SaveData functionality. Add reference to Neon.Web.Data to SilverlightApplication.Web project. Next add class PersonSavableData.cs as shown. (This is completely optional, you are free to move data as per your own custom DTO schemes, this just one way of moving data.)

[System.Runtime.Serialization.DataContractAttribute(
    Name = "PersonSavableData",
    Namespace = "http://schemas.datacontract.org/2004/07/Neon.Web.Data")]
public class PersonSavableData : ISavableData<Person> {
    [DataMember]
    public Person Data { get; set; }
    [DataMember]
    public Person OriginalData { get; set; }
    [DataMember]
    public int ActionType { get; set; }
    [DataMember]
    public bool ActionTaken { get; set; }
    [DataMember]
    public DateTime ActionDate { get; set; }
    [DataMember]
    public Dictionary<string, string> Errors { get; set; }
}

PersonSavableData implements ISavableData and provides access to original and modified data as well as action to take on that particular instance. Next add SaveData method to PeopleService as shown:

[OperationContract]
public List<PersonSavableData> SaveData(string objectInfo, List<PersonSavableData> dataList) {
    List<PersonSavableData> savedData = dataList;
    List<Person> dataItems = dataList.Select<PersonSavableData, Person>(d => d.Data).ToList();
    Func<Person, Person, int> saveOperation;
    //
    foreach (PersonSavableData savableData in dataList) {
        savableData.ActionDate = DateTime.Now;
        savableData.ActionTaken = false;
        // validate data
        // if validation fails continue
        switch (savableData.ActionType) {
            default:
            case (int)ActionType.Update:
                saveOperation = UpdateData;
                break;
            case (int)ActionType.Insert:
                saveOperation = InsertData;
                break;
            case (int)ActionType.Delete:
                saveOperation = DeleteData;
                break;
        }
        try {
            if (0 < saveOperation(savableData.Data, savableData.OriginalData)) {
                savableData.ActionTaken = true;
            }
        } catch (Exception ex) {
            if (null == savableData.Errors) {
                savableData.Errors = new Dictionary<string, string>();
            }
            savableData.Errors.Add("Exception", ex.Message);
        }
    }
    return savedData;
}
int UpdateData(Person person, Person originalPerson) {
    return 1;
}
int InsertData(Person person, Person originalPerson) {
    return 1;
}
int DeleteData(Person person, Person originalPerson) {
    return 1;
}

In the SaveData method, we iterate over data to save and based on action specified, call appropriate Update, Delete or Insert function. (Here are we just using stub methods for demo.) Now update the service reference in SilverlightApplication, this will bring over PersonSavableData and update PeopleServiceClient with SaveData method. Add partial class PersonSavableData to SilverlightApplication as shown

namespace SilverlightApplication.PeopleServiceReference {
    public partial class PersonSavableData : ISavableData<Person> {
    }
}

This allows us to cast from generic ISavableData<Person> to PersonSavableData. Also add following method to PersonDataItemFactory to provide typed PersonSavableData item for data saving operation.

public override ISavableData<Person> GetSavableData(DataItem<Person> dataItem) {
    return new PersonSavableData() { Data = dataItem.Data, OriginalData = dataItem.OriginalData };
}

And finally modify PeopleProvider to call backend SaveData method

public void SaveDataAsync(string objectInfo, ObservableCollection<Neon.Web.Data.ISavableData<Person>> dataList) {
    PeopleServiceClient proxy = new PeopleServiceClient();
    proxy.SaveDataCompleted += new EventHandler<SaveDataCompletedEventArgs>(proxy_SaveDataCompleted);
    ObservableCollection<PersonSavableData> dataList2 = new ObservableCollection<PersonSavableData>();
    foreach (ISavableData<Person> savableData in dataList) {
        dataList2.Add(savableData as PersonSavableData);
    }
    proxy.SaveDataAsync(objectInfo, dataList2);
}
public event EventHandler<SaveDataCompletedEventArgs<Person>> SaveDataCompleted;
//
void proxy_SaveDataCompleted(object sender, SaveDataCompletedEventArgs e) {
    ObservableCollection<ISavableData<Person>> dataList = new ObservableCollection<ISavableData<Person>>();
    foreach (PersonSavableData savableData in e.Result) {
        dataList.Add(savableData as ISavableData<Person>);
    }
    OnSaveDataCompleted(dataList);
}
protected virtual void OnSaveDataCompleted(ObservableCollection<ISavableData<Person>> dataList) {
    if (null != SaveDataCompleted) {
        App.Current.RootVisual.Dispatcher.BeginInvoke(() =>
        SaveDataCompleted(this, new SaveDataCompletedEventArgs<Person>() { SavableDataCollection = dataList })
        );
    }
}

In order to save data, we need to call Save method on ObjectDataSource. ObjectDataSource provides list of all items that have changed to the SaveDataAsync method of PeopleProvider. SaveDataAsync methods in turn called SaveDataAsync method of PeopleServiceClient, resulting in async call to the PeopleService SaveData method.

Modify Page.xaml to add save button

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Button x:Name="SaveButton" Grid.Row="0" Content="Save" Width="100" HorizontalAlignment="Left"/>
    <data:DataGrid x:Name="peopleDataGrid" AutoGenerateColumns="False" Grid.Row="1"
        Margin="10" RowHeight="22" ItemsSource="{Binding DataList}" >

In Page.xaml.cs wire up Click event and call Save method

void SaveButton_Click(object sender, RoutedEventArgs e) {
    ((PeopleDataSource)this.DataContext).Save();
}

Compile and test application. Modify a row and Save changes. Note that row status color changes from yellow (changed) to green (saved).

image=>image =>image

At this point we have a complete application that can handle add, update, delete and save data back to server. It is easily modifiable by plugging in your own providers and allows for faster development. In summary, ObjectDataSource provides automated state management and lets you choose how to implement backend data store.
 
Please see sample application in download for automatic paging, custom sorting and additional functionality.

Steps to get started

1. Create a SilverlightApplication.

2. Use WCF or alternate means to implement service.

3. For client entity, implement IDataItemData interface

4. Implement IObjectDataProvider to automate fetch and data load process or manually fetch and load data into ObjectDataSource instance. Optionally implement IDataItemFactory to customize DataItem and/or setup custom validation.

5. Create a typed data source for declarative XAML useNeon.ObjectDataSource

Source Code: Neon, a toolkit for developing business applications with Silverlight.

Technorati Tags:
Silverlight ServiceReferences.ClientConfig Alternatives

Silverlight uses the ServiceReferences.ClientConfig file to store and lookup WCF related configuration. Since the ServiceReferences.ClientConfig file is packaged and deployed along with the application in a XAP file, you have to change it for QA, Production, and other deployment scenarios. One option to automate the process is to use the ConfigSwitcher utility that switches proper configuration file based on the selected solution configuration, to perform compile time static linking.

As long as you are not doing any cross domain service access, a simpler alternative is to completely bypass the ServiceReferences.ClientConfig and to do dynamic or run time configuration. It is really easy with the help of SilverlightHost class that provides details about the Silverlight-based application's instantiation settings, and exposes some of the HTML DOM values for the hosted Silverlight plug-in instance. In particular, SilverlightHost.Source property gets the URI of the XAP package and can be used to provide Uri for the service EndpointAddress.

Dynamic Configuration

In a dynamic configuration, we make use of the SilverlightHost properties to dynamically configure and return a proper service client. Lets first start with the traditional ServiceReferences.ClientConfig approach and we will modify it to implement the runtime configuration.  Create a new SilverlightApplication and the corresponding Web Application to host and test the SilverlightApplication. Add a new class, Person.cs to the Silverlight Web Application as shown:

[DataContract]
public class Person {
 
    private string _firstName;
    [DataMember]
    public string FirstName {
        get { return _firstName; }
        set { _firstName = value; }
    }
 
    private string _lastName;
    [DataMember]
    public string LastName {
        get { return _lastName; }
        set { _lastName = value; }
    }
 
    private int _age;
    [DataMember]
    public int Age {
        get { return _age; }
        set { _age = value; }
    }    
}

Next add a new Silverlight enabled WCF service, PeopleService:

[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class PeopleService {
    [OperationContract]
    public List<Person> GetData() {
        List<Person> personList = new List<Person>();
        for (int i = 0; i < 10; i++) {
            personList.Add(new Person() {
                FirstName = string.Format("First Name {0}", i),
                LastName = string.Format("Last Name {0}", i),
                Age = i
            });
        }
        //
        return personList;
    }
}

Add a service reference to the Silverlight client and call it PeopleServiceReference. (use Discover button to find PeopleService). Also add System.Windows.Control.Data reference to the SilverlightApplication project.

 

 

 

Add 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="True" Margin="10"/>
    </Grid>
</UserControl>

Add following code to page.xaml.cs code behind:

public partial class Page : UserControl {
    public Page() {
        InitializeComponent();
        this.Loaded += new RoutedEventHandler(Page_Loaded);
    }
 
    void Page_Loaded(object sender, RoutedEventArgs e) {
        GetData();
    }
 
    private void GetData() {
        //
        PeopleServiceClient proxy = new PeopleServiceClient();
        proxy.GetDataCompleted += new EventHandler<GetDataCompletedEventArgs>(proxy_GetDataCompleted);
        this.Cursor = Cursors.Wait;
        proxy.GetDataAsync();
    }
 
    void proxy_GetDataCompleted(object sender, GetDataCompletedEventArgs e) {
        this.Cursor = Cursors.Arrow;
        if (null != e.Error) {
            MessageBox.Show(e.Error.Message, "Error Getting Data", MessageBoxButton.OK);
            return;
        }
        //
        peopleDataGrid.ItemsSource = e.Result;
    }
 
}

Build and run the application and make sure it is working properly. Note that we are making use of the ServiceReferences.ClientConfig file to configure the PeopleClient. Now just exclude or rename the ServiceReferences.ClientConfig file from the project. If you now build and run the application, you should get the following error:

image

Add a new file called ServiceUtil.cs as shown; to the Silverlight client application

public class ServiceUtil {
    public static PeopleServiceClient GetPeopleServiceClient() {
        BasicHttpBinding binding = new BasicHttpBinding(
            Application.Current.Host.Source.Scheme.Equals("https", StringComparison.InvariantCultureIgnoreCase) 
            ? BasicHttpSecurityMode.Transport : BasicHttpSecurityMode.None);
        binding.MaxReceivedMessageSize = int.MaxValue;
        binding.MaxBufferSize = int.MaxValue;
        return new PeopleServiceClient(binding, new EndpointAddress(
            new Uri(Application.Current.Host.Source, "../PeopleService.svc")));
    }
}

Here we are using Host.Source.Scheme to determine the security mode(https) and Host.Source to setup the proper EndpointAddress Uri. Application.Current.Host.Source provides a Uri, that points to the location of the XAP file, which normally is ClientBin folder under  the web site root. We use that as the base Uri and provide the address of PeopleSerivce as a relative Uri to construct the service endpoint address.

Next modify GetData method in page.xaml.cs to use above method

private void GetData() {
    //
    //PeopleServiceClient proxy = new PeopleServiceClient();
    PeopleServiceClient proxy = ServiceUtil.GetPeopleServiceClient();
    proxy.GetDataCompleted += new EventHandler<GetDataCompletedEventArgs>(proxy_GetDataCompleted);
    this.Cursor = Cursors.Wait;
    proxy.GetDataAsync();
}

Build and run the application and verify that it is working properly. Because we are determining the host Uri and scheme at the runtime, this code can be deployed to various target sites without any modification.

Dynamic Switch

Another alternative is to combine the static ServiceReferences.ClientConfig file with a dynamic lookup. Modify ServiceReferences.ClientConfig  to include multiple named entries, each for the desired target site. Then pass in the proper site information as part of the init parameters. Now when creating the service client, just append proper site information to the service name.

PeopleServiceClient proxy = new PeopleServiceClient("BasicHttpBinding_PeopleService_"+ ((App)Application.Current).InitParams["SiteName"]);

 
ServiceReferences.ClientConfig file:
 
<client>
    <endpoint address="http://localhost:1896/PeopleService.svc" binding="basicHttpBinding"
        bindingConfiguration="BasicHttpBinding_PeopleService" contract="PeopleServiceReference.PeopleService"
        name="BasicHttpBinding_PeopleService_DEV" />
  <endpoint address="http://somesite.com/QA/PeopleService.svc" binding="basicHttpBinding"
      bindingConfiguration="BasicHttpBinding_PeopleService" contract="PeopleServiceReference.PeopleService"
      name="BasicHttpBinding_PeopleService_QA" />
  <endpoint address="http://somesite.com/Test/PeopleService.svc" binding="basicHttpBinding"
      bindingConfiguration="BasicHttpBinding_PeopleService" contract="PeopleServiceReference.PeopleService"
      name="BasicHttpBinding_PeopleService_Test" />
</client>

Approach Comparison

Following is the comparison of various approaches:

Approach Pro Con
ConfigSwitcher - Static compile time linking Works in all cases Requires multiple ServiceReferences.ClientConfig files, each per target site
Dynamic/Runtime configuration Same package can be deployed to any site No Cross domain support
Dynamic Switch - Static compile time linking with Dynamic runtime selection Works in all cases Hosting in ASPX only

Source Code: ServiceUtil.zip (173 KB)

 

 

 

 

 

 

 

 

 

Technorati Tags:
Silverlight DataGrid Custom Sorting

Silverlight DataGrid provides automatic sorting functionality for any data source that implements IList. It does this by internally wrapping the data source in a ListCollectionView. However there are many situations where it is desirable to alter the default sort implementation. This is particularly true when the DataGrid is bound to a data source that is paging data from the server. Fortunately, the DataGrid knows to delegate sorting to a data source that implements ICollectionView. This blog post examines what it takes to implement ICollectionView and provides a sample implementation in the form of a reusable SortableCollectionView.

Default Sorting

default sorting I will start with the paging application that I had previously developed as part of Stealth Paging blog post.

The sample application displays a (paged) list of Persons to user. It implements on demand auto paging, where the data is retrieved one page or 20 rows at a time. As user scrolls and nears the end of available data to display, another call is made to the backend service to get more data.

Run the sample application. Now try sorting on the Age field. Sort again to get data in descending order.

 

 

Pre default sort after default sorting 

Notice that the top Person after sorting in descending order has Age of 19. This is because DataGrid is sorting only the data that is available on the client side. However a user would expect to see 100, not 19, assuming that base data consisted of 101 Persons with Age from 0 to 100. Since we are paging data from the server, we need to alter the default sort behavior, so as to carry out server side sorting.

Custom Sorting

We will implement ICollectionView interface on the base ObservableCollection to take control of sorting. ICollectionView interface defines functions for current record management, custom sorting, filtering, and grouping. Current implementation of DataGrid (Dec 2008 release) only uses current record management and custom sorting functionality. Sorting functionality encompasses CanSort and SortDescriptions properties. For custom sorting, we need to listen for changes to SortDescriptions collection, and in turn refresh the data for new sort options.

Create a new SilverlightApplication and the corresponding Web Application to host and test the SilverlightApplication. I will reuse code from Stealth Paging application. Copy over Person.cs, PeopleService.svc and PeopleService.svc.cs files to the web application. Modify Person.cs and add a new field CountyCode as shown:

private int _countyCode;
[DataMember]
public int CountyCode {
    get { return _countyCode; }
    set { _countyCode = value; }
}

This will help us test sub sorting behavior.

Next modify PeopleService.svc.cs class as shown:

[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class PeopleService {
    [OperationContract]
    public List<Person> GetData(int startRow, int endRow, Dictionary<string, string> sortBy) {
 
        List<Person> personList = new List<Person>();
        for (int i = 0; i < 101; i++) {
            personList.Add(new Person() {
                FirstName = string.Format("First Name {0}", i),
                LastName = string.Format("Last Name {0}", i),
                Age = i,
                City = string.Format("City {0}", i),
                CountyCode = i / 10
            });
        }
        //
        if (null == sortBy || 0 >= sortBy.Keys.Count) {
            return personList.Skip(startRow).Take(endRow - startRow).ToList<Person>();
        }
 
        IEnumerable<Person> source = personList;
        
        foreach (string propertyName in sortBy.Keys) {
            IOrderedEnumerable<Person> orderedEnumerable = source as IOrderedEnumerable<Person>;
            string propName = propertyName;
            if (null == orderedEnumerable) {
                if ("ASC" == sortBy[propertyName]) {
                    source = source.OrderBy<Person, object>(p => GetKeySelector(p, propName));
                } else {
                    source = source.OrderByDescending<Person, object>(p => GetKeySelector(p, propName));
                }
            } else {
                if ("ASC" == sortBy[propertyName]) {
                    source = orderedEnumerable.ThenBy<Person, object>(p => GetKeySelector(p, propName));
                } else {
                    source = orderedEnumerable.ThenByDescending<Person, object>(p => GetKeySelector(p, propName));
                }
            }
        }
        //
        return source.Skip(startRow).Take(endRow - startRow).ToList<Person>();
    }
 
    private object GetKeySelector(Person p, string propertyName) {
        object target = p;
        target = target.GetType().InvokeMember(propertyName, BindingFlags.GetProperty, null, target, null);
        return target;
    }
 
}

Note that we have changed the GetData method to take an additional parameter, Dictionary<string, string> sortBy. Client will request sorting by including one or more columns to sort on and the corresponding sort order, “ASC” for ascending and “DESC” for descending.

In the GetData method, we are first building a list of 101 Person. This is just for demo purpose, in a real application you will not need this step. Next we check to see if the user has requested sorting. If there is no need to sort, we just return the paged data.

If user has provided sortBy data, we loop over sortBy.Keys collection to get properties to sort over. Here we are using LINQ IOrderedEnumerable interface to build composite sort query. For each sort, we also check key’s value to determine the sort order. Finally we add paging functions and execute the composed LINQ query by calling ToList and return the result.

Now lets modify the Silverlight client application to support custom sorting. Add a service reference to the Silverlight client and call is PeopleServiceReference. (use Discover button to find PeopleService). Also add System.Windows.Control.Data reference to the SilverlightApplication project.

 

 

 

Add 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">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <data:DataGrid x:Name="peopleDataGrid" AutoGenerateColumns="True" Grid.Row="0" Margin="10"/>
    </Grid>
</UserControl>

 

Add following code to page.xaml.cs code behind:

public partial class Page : UserControl {
    int _startRow;
    int _pageSize = 20;
    ObservableCollection<Person> _people;
    bool _loading;
    Dictionary<string, string> _sortBy;
 
    public Page() {
        InitializeComponent();
        this.Loaded += new RoutedEventHandler(Page_Loaded);
        this.peopleDataGrid.LoadingRow += new EventHandler<DataGridRowEventArgs>(peopleDataGrid_LoadingRow);
    }
 
    void Page_Loaded(object sender, RoutedEventArgs e) {
        _startRow = 0;
        _people = new ObservableCollection<Person>(); 
        peopleDataGrid.ItemsSource = _people;
        _sortBy = new Dictionary<string, string>();
        GetData(_startRow, _pageSize);
    }
 
    private void GetData(int startRow, int endRow) {
        PeopleServiceClient proxy = new PeopleServiceClient();
        proxy.GetDataCompleted += new EventHandler<GetDataCompletedEventArgs>(proxy_GetDataCompleted);
        _loading = true;
        this.Cursor = Cursors.Wait;
        proxy.GetDataAsync(startRow, endRow, _sortBy);
    }
 
    void proxy_GetDataCompleted(object sender, GetDataCompletedEventArgs e) {
        this.Cursor = Cursors.Arrow;
        _loading = false;
        if (null != e.Error) {
            MessageBox.Show(e.Error.Message, "Error Getting Data", MessageBoxButton.OK);
            return;
        }
        if (0 == _startRow) {
            peopleDataGrid.SelectedItem = null;
            _people.Clear();
        }
        _startRow += _pageSize;
        foreach (Person person in e.Result) {
            _people.Add(person);
        }
    }
 
    void peopleDataGrid_LoadingRow(object sender, DataGridRowEventArgs e) {
        if (_loading || _people.Count < _pageSize) {
            return;
        }
        if (_people.Count - 5 < e.Row.GetIndex()) {
            GetData(_startRow, _startRow + _pageSize);
        }
    }
}

Build and run the application and test out the default sorting functionality.

Add a new class CustomSortDescriptionCollection.cs as shown:

public class CustomSortDescriptionCollection : SortDescriptionCollection {

    public event NotifyCollectionChangedEventHandler MyCollectionChanged {
        add {
            this.CollectionChanged += value;
        }
        remove {
            this.CollectionChanged -= value;
        }
    }
}
 
We need CustomSortDescriptionCollection because the base class SortDescriptionCollection has CollectionChanged declared as protected. And we need access to that event so as to know when user has changed Sort options.

Add a new class SortableCollectionView as shown:

public class SortableCollectionView<T> : ObservableCollection<T>, ICollectionView  {
 
    public SortableCollectionView() {
        this._currentItem = null;
        this._currentPosition = -1;
    }
 
    protected override void InsertItem(int index, T item) {
        base.InsertItem(index, item);
        if (0 == index || null == this._currentItem ) {
            _currentItem = item;
            _currentPosition = index;
        }
    }
 
    public virtual object GetItemAt(int index) {
        if ((index >= 0) && (index < this.Count)) {
            return this[index];
        }
        return null;
    }
 
    #region ICollectionView Members
 
    public bool CanFilter {
        get { return false; }
    }
 
    public bool CanGroup {
        get { return false; }
    }
 
    public bool CanSort {
        get { return true; }
    }
 
    public bool Contains(object item) {
        if (!IsValidType(item)) {
            return false;
        }
        return this.Contains((T)item);
    }
 
    private bool IsValidType(object item) {
        return item is T;
    }
 
    private CultureInfo _culture;
 
    public System.Globalization.CultureInfo Culture {
        get {
            return this._culture;
        }
        set {
            if (this._culture != value) {
                this._culture = value;
                this.OnPropertyChanged(new PropertyChangedEventArgs("Culture"));
            }
        }
    }
 
    public event EventHandler CurrentChanged;
 
    public event CurrentChangingEventHandler CurrentChanging;
 
    private object _currentItem;
 
    public object CurrentItem {
        get { return this._currentItem; }
    }
 
    private int _currentPosition;
 
    public int CurrentPosition {
        get {
            return this._currentPosition;
        }
    }
 
    public IDisposable DeferRefresh() {
        throw new NotImplementedException();
    }
 
    private Predicate<object> _filter;
 
    public Predicate<object> Filter {
        get {
            return _filter;
        }
        set {
            _filter = value;
        }
    }
 
    public ObservableCollection<GroupDescription> GroupDescriptions {
        get { throw new NotImplementedException(); }
    }
 
    public ReadOnlyObservableCollection<object> Groups {
        get { throw new NotImplementedException(); }
    }
 
    public bool IsCurrentAfterLast {
        get {
            if (!this.IsEmpty) {
                return (this.CurrentPosition >= this.Count);
            }
            return true;
        }
    }
 
    public bool IsCurrentBeforeFirst {
        get {
            if (!this.IsEmpty) {
                return (this.CurrentPosition < 0);
            }
            return true;
        }
    }
 
    protected bool IsCurrentInSync {
        get {
            if (this.IsCurrentInView) {
                return (this.GetItemAt(this.CurrentPosition) == this.CurrentItem);
            }
            return (this.CurrentItem == null);
        }
    }
 
    private bool IsCurrentInView {
        get {
            return ((0 <= this.CurrentPosition) && (this.CurrentPosition < this.Count));
        }
    }
 
    public bool IsEmpty {
        get {
            return (this.Count == 0);
        }
    }
 
    public bool MoveCurrentTo(object item) {
        if (!IsValidType(item)) {
            return false;
        } 
        if (object.Equals(this.CurrentItem, item) && ((item != null) || this.IsCurrentInView)) {
            return this.IsCurrentInView;
        }           
        int index = this.IndexOf((T)item);
        return this.MoveCurrentToPosition(index);
    }
 
    public bool MoveCurrentToFirst() {
        return this.MoveCurrentToPosition(0);
    }
 
    public bool MoveCurrentToLast() {
        return this.MoveCurrentToPosition(this.Count - 1);
    }
 
    public bool MoveCurrentToNext() {
        return ((this.CurrentPosition < this.Count) && this.MoveCurrentToPosition(this.CurrentPosition + 1));
    }
 
    public bool MoveCurrentToPrevious() {
        return ((this.CurrentPosition >= 0) && this.MoveCurrentToPosition(this.CurrentPosition - 1));
    }
 
    public bool MoveCurrentToPosition(int position) {
        if ((position < -1) || (position > this.Count)) {
            throw new ArgumentOutOfRangeException("position");
        }
        if (((position != this.CurrentPosition) || !this.IsCurrentInSync) && this.OKToChangeCurrent()) {
            bool isCurrentAfterLast = this.IsCurrentAfterLast;
            bool isCurrentBeforeFirst = this.IsCurrentBeforeFirst;
            ChangeCurrentToPosition(position);
            OnCurrentChanged();
            if (this.IsCurrentAfterLast != isCurrentAfterLast) {
                this.OnPropertyChanged("IsCurrentAfterLast");
            }
            if (this.IsCurrentBeforeFirst != isCurrentBeforeFirst) {
                this.OnPropertyChanged("IsCurrentBeforeFirst");
            }
            this.OnPropertyChanged("CurrentPosition");
            this.OnPropertyChanged("CurrentItem");
        }
        return this.IsCurrentInView;
    }
 
    private void ChangeCurrentToPosition(int position) {
        if (position < 0) {
            this._currentItem = null;
            this._currentPosition = -1;
        } else if (position >= this.Count) {
            this._currentItem = null;
            this._currentPosition = this.Count;
        } else {
            this._currentItem = this[position];
            this._currentPosition = position;
        }
    }
 
    protected bool OKToChangeCurrent() {
        CurrentChangingEventArgs args = new CurrentChangingEventArgs();
        this.OnCurrentChanging(args);
        return !args.Cancel;
    }
 
    protected virtual void OnCurrentChanged() {
        if (this.CurrentChanged != null) {
            this.CurrentChanged(this, EventArgs.Empty);
        }
    }
 
    protected virtual void OnCurrentChanging(CurrentChangingEventArgs args) {
        if (args == null) {
            throw new ArgumentNullException("args");
        }
        if (this.CurrentChanging != null) {
            this.CurrentChanging(this, args);
        }
    }
 
    protected void OnCurrentChanging() {
        this._currentPosition = -1;
        this.OnCurrentChanging(new CurrentChangingEventArgs(false));
    }
 
    protected override void ClearItems() {
        OnCurrentChanging();
        base.ClearItems();
    }
 
    private void OnPropertyChanged(string propertyName) {
        this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }        
 
    public event EventHandler<RefreshEventArgs> OnRefresh;
    public void Refresh() {
        // sort and refersh
        if (null != OnRefresh) {
            OnRefresh(this, new RefreshEventArgs() { SortDescriptions = SortDescriptions });
        }
        //this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
 
    private CustomSortDescriptionCollection _sort;
 
    public SortDescriptionCollection SortDescriptions {
        get {
            if (this._sort == null) {
                this.SetSortDescriptions(new CustomSortDescriptionCollection());
            }
            return this._sort;
        }
    }
 
    private void SetSortDescriptions(CustomSortDescriptionCollection descriptions) {
        if (this._sort != null) {
            this._sort.MyCollectionChanged -= new NotifyCollectionChangedEventHandler(this.SortDescriptionsChanged);
        }
        this._sort = descriptions;
        if (this._sort != null) {
            this._sort.MyCollectionChanged += new NotifyCollectionChangedEventHandler(this.SortDescriptionsChanged);
        }
    }
 
    private void SortDescriptionsChanged(object sender, NotifyCollectionChangedEventArgs e) {
        if (e.Action == NotifyCollectionChangedAction.Remove && e.NewStartingIndex == -1 && SortDescriptions.Count > 0) {
            return;
        }
        if (
            ((e.Action != NotifyCollectionChangedAction.Reset) || (e.NewItems != null)) 
            || (((e.NewStartingIndex != -1) || (e.OldItems != null)) || (e.OldStartingIndex != -1))
            ) {
            this.Refresh();
        }
    }
 
    public System.Collections.IEnumerable SourceCollection {
        get { 
            return this; 
        }
    }
 
    #endregion
}

SortableCollectionView SortableCollectionView inherits from ObservableCollection and also implements ICollectionView. Majority of code is just the default implementation of ICollectionView. For Sorting functionality, have a look at the SortDescriptions property. DataGrid uses this collection to add or remove SortDescription based on user actions. A SortDescription defines the direction and the property name to be used as the criteria for sorting a collection.

We subscribe to the CollectionChanged event of SortDescriptions collection and handle it in the method SortDescriptionsChanged. Here we determine if we need to refresh the data in response to changes to the sort order and if so, we fire the Refresh event. This event is indication to consumers of SortableCollectionView that data needs to be refreshed.

Now modify Page.xaml.cs as shown:

SortableCollectionView<Person> _people;
//ObservableCollection<Person> _people;

void Page_Loaded(object sender, RoutedEventArgs e) {
    _startRow = 0;
    //_people = new ObservableCollection<Person>(); 
    _people = new SortableCollectionView<Person>();
    // default sort order
    _people.SortDescriptions.Add(new SortDescription("Age", ListSortDirection.Ascending));
    _people.OnRefresh += new EventHandler<RefreshEventArgs>(people_OnRefresh);
    peopleDataGrid.ItemsSource = _people;
    _sortBy = new Dictionary<string, string>();
    //nextPageButton.IsEnabled = false;
    GetData(_startRow, _pageSize);
}

void people_OnRefresh(object sender, RefreshEventArgs e) {
    _sortBy.Clear();
    foreach (SortDescription sortDesc in _people.SortDescriptions) {
        _sortBy.Add(sortDesc.PropertyName, (sortDesc.Direction == ListSortDirection.Ascending ? "ASC" : "DESC"));
    }
    _startRow = 0;
    GetData(_startRow, _pageSize);
}

custom sortingWhenever user changes sort options in DataGrid, DataGrid adds or removes SortDescription object in SortDescriptions collection, which causes CollectionChanged event; that SortableCollectionView is listening for. In turn, SortableCollectionView fires OnRefresh event, indicating need to refresh the data as per new sort conditions.

In the people_OnRefresh method, we cycle through the SortDescriptions collection and build the sortBy dictionary that is eventually passed to the backend web service for custom sorting.

Build and run the application. Note that since data is pre sorted by Age field, we have added that to SortDescriptionCollection and it shows on the UI, without any user actions.

Next click on Age field. Data is now sorted and first Person has Age of 100. Try sorting by CountyCode, and add sort by Age descending (hold down shift key to add sub sorts / sort within).

 sorting age sorting countycode age

Custom sorting can be used in other scenarios as well. If data is presorted, you can add new SortDescription to show that on UI. Similarly, it is possible to remove all sorting by clearing SortDescriptions collection. It also enables you to position a fixed row, say an empty row for adding new person, as the first row or the last row. It is also possible to use arbitrary property names in SortMemberPath since we have full control of sorting and can interpret property names as per application requirements.

Additionally filter functionality can also be implemented by setting up the Filter predicate to filter out the item and evaluating the filter when adding an item to the collection.

However there are also certain side effects. If you change any data, it will not be automatically repositioned. Also if you add a new data row, it will not be positioned properly as well. Based on your needs, you may need to re-sort in each of the above cases or use alternate sorting implementation.

Hopefully this will help you incorporate custom sorting functionality in DataGrid, till the Silverlight framework provides an easier alternative (similar to WPF).

Source Code: CustomSorting.zip (185 KB)

Technorati Tags:
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:
Cascading ComboBox

In the post ComboBox in DataGrid, we examined the usage of a simple ComboBox, displaying a fixed list of values. One of the scenario that is quite common in business applications is that of a dependent data selection, where data from one item is used to narrow down selection choices for the next item; also referred as cascading or hierarchical selection. In a cascading scenario, list of values is variable, changing based on some other data.

image

We will use a Car model to showcase Cascading ComboBox behavior. For our example, Car will be classified as consisting of Make, Model and Color. Make determines Model and Model determines Color.

When user selects a Car’s Make, it will be used to narrow down available Models. Once a model is selected, it will be used it to filter out available colors to select from. CarMake - CarModel

Create a new SilverlightApplication and add the corresponding Web Application to host and test the Silverlight application.

We will use CarMake and CarModel classes to represent Make and Model respectively. To keep things simple we will just use two properties, Id to identify (primary key) and Name for description.

Each of the class also overrides Equals to customize equality comparison.

Next add the class Car with MakeId, ModelId and Color properties. Also implement INotifyPropertyChanged for property change notification.

public class Car : INotifyPropertyChanged {

    private int _makeId;
    public int MakeId {
        get { return _makeId; }
        set {
            if (_makeId == value) return;
            _makeId = value;
            RaisePropertyChanged("MakeId");
        }
    }

    private int _modelId;
    public int ModelId {
        get { return _modelId; }
        set {
            if (_modelId == value) return;
            _modelId = value;
            RaisePropertyChanged("ModelId");
        }
    }

    private string _color;
    public string Color {
        get { return _color; }
        set {
            if (_color == value) return;
            _color = value;
            if (value == null) {
                _color = EmptyFactory.GetEmptyColor(_modelInfo);
            }
            RaisePropertyChanged("Color");
        }
    }

    private CarMake _makeInfo;
    public CarMake MakeInfo {
        get { 
            return _makeInfo; 
        }
        set {
            _makeInfo = value;
            if (null != _makeInfo) {
                MakeId = _makeInfo.MakeId;
            } else {
                _makeInfo = EmptyFactory.GetEmptyCarMake();
                MakeId = _makeInfo.MakeId;
            }
            RaisePropertyChanged("MakeInfo");
            ModelInfo = EmptyFactory.GetEmptyCarModel(_makeInfo);

        }
    }


    private CarModel _modelInfo;
    public CarModel ModelInfo {
        get {
            return _modelInfo; 
        }
        set {
            if (value == _modelInfo) { return; }
            _modelInfo = value;
            if (null != _modelInfo) {
                ModelId = _modelInfo.ModelId;
            } else {
                _modelInfo = EmptyFactory.GetEmptyCarModel(_makeInfo);
                ModelId = _modelInfo.ModelId;
            }
            RaisePropertyChanged("ModelInfo");
            Color = EmptyFactory.GetEmptyColor(_modelInfo);
        }
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName) {
        if (null != PropertyChanged) {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public void RaisePropertyChanged(string propertyName) {
        //OnPropertyChanged(propertyName);
        if (null != Application.Current.RootVisual) {
            Application.Current.RootVisual.Dispatcher.BeginInvoke(() =>
            OnPropertyChanged(propertyName));
        }
    }
    #endregion
}

Note that we have also added MakeInfo and ModelInfo helper methods for data binding to ComboBox’s SelectedItem property. This allows us to show user friendly name and save Id, as I have already outlined in the previous ComboBox post. image

We will use a helper factory to create the empty car and other related classes. EmptyFactory provides methods to create empty instances of Car, CarMake, CarModel, and Color. Alternatively you can always have each class provide corresponding static create methods, but keeping it in external factory class allows us to reduce interdependency and complexity in the model classes.

imageNext we add the class to provide data for various lists, CarListProvider. It includes methods that provide various lists for car, namely MakeList, ModelList and ColorList.

In order to provide the proper model list, we need to know which car make user has selected. Since Car is already providing change notification for MakeInfo, we just subscribe to Car’s PropertyChanged event and look for changes to MakeInfo. When that occurs, we use MakeId to provide proper list of Models.

Similarly we listen for changes to ModelInfo. When user selects a Model from dropdown, it triggers Car’s ModelInfo change notification, which enables us to provide appropriate Color list for the selected Model.

Add the class to provide list data, CarListProvider.cs as shown:

public class CarListProvider : INotifyPropertyChanged {

    Car _car;
    public CarListProvider(Car car) {
        _car = car;
        _car.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_car_PropertyChanged);
    }

    void _car_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {
        switch (e.PropertyName) {
            case "MakeInfo":
                RaisePropertyChanged("ModelList");
                break;
            case "ModelInfo":
                RaisePropertyChanged("ColorList");
                break;
            default:
                break;
        }
    }

    public List<CarMake> MakeList {
        get {
            return new List<CarMake> { EmptyFactory.GetEmptyCarMake(),
                                    new CarMake() { MakeId=100, MakeName="Toyota (100)"},  
                                    new CarMake() { MakeId=200, MakeName="Honda (200)"},   
                                    new CarMake() { MakeId=300, MakeName="Nissan (300)"}};
        }
    }

    public ObservableCollection<CarModel> ModelList {
        get {
            RaisePropertyChanged("IsModelListEnabled");

            int makeId = _car.MakeId;
            if (-1 == makeId) {
                return new ObservableCollection<CarModel>() { EmptyFactory.GetEmptyCarModel()};
            }

            return new ObservableCollection<CarModel> { 
                                EmptyFactory.GetEmptyCarModel(_car.MakeInfo),   
                                new CarModel() { ModelId=makeId + 1, ModelName=string.Format("Model{0}",makeId + 1)},  
                                new CarModel() { ModelId=makeId + 2, ModelName=string.Format("Model{0}",makeId + 2)},   
                                new CarModel() { ModelId=makeId + 3, ModelName=string.Format("Model{0}",makeId + 3)},  
                                new CarModel() { ModelId=makeId + 4, ModelName=string.Format("Model{0}",makeId + 4)}};

        }
    }

    public ObservableCollection<string> ColorList {
        get {
            RaisePropertyChanged("IsColorListEnabled");

            int modelId = _car.ModelId;
            if (-1 == modelId) {
                return new ObservableCollection<string>() { EmptyFactory.GetEmptyColor() };
            }
            return new ObservableCollection<string> { EmptyFactory.GetEmptyColor(_car.ModelInfo), 
                                                        string.Format("Red{0}", modelId), 
                                                       string.Format("White{0}", modelId), 
                                                       string.Format("Blue{0}", modelId), 
                                                       string.Format("Green{0}", modelId), 
                                                    };
        }
    }

    public bool IsColorListEnabled {
        get {
            return (-1 != _car.ModelId);
        }
    }

    public bool IsModelListEnabled {
        get {
            return (-1 != _car.MakeId);
        }
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName) {
        if (null != PropertyChanged) {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public void RaisePropertyChanged(string propertyName) {
        //OnPropertyChanged(propertyName);
        if (null != Application.Current.RootVisual) {
            Application.Current.RootVisual.Dispatcher.BeginInvoke(() =>
            OnPropertyChanged(propertyName));
        }

    }
    #endregion
}

Note that we are using a dictionary to cache list of models for a make. If no model list is found, we create new model list, add it to cache and return the model list. In a production application,  you will make service call to get data from the server.

In addition to providing lists for make, model, and color, CarListProvider also exposes two properties, IsModelListEnabled and IsColorListEnabled. These properties are used to enable/disable ComboBox when user has pending parent selection. Alternatively you can just default selection to the first item in the list and always keep ComboBox enabled.

We will also add CarListProvider to Car and expose it for data binding. Add following code to class Car:

public Car() {
    _carListProvider = new CarListProvider(this);
}

public Car(CarMake makeInfo, CarModel modelInfo, string color) : this(){
    _makeInfo = makeInfo;
    _makeId = _makeInfo.MakeId;
    _modelInfo = modelInfo;
    _modelId = _modelInfo.ModelId;
    _color = color;
}

private CarListProvider _carListProvider;
public CarListProvider CarListProvider {
    get { return _carListProvider; }
    set { _carListProvider = value; }
}

Encapsulation of CarListProvider in the Car for this single record scenario is not a requirement, you can also create CarListProvider in resource (as show here) and later set Car as property on it. However it will be a requirement when we go for the multi record scenario later.

To test the working of Cascading ComboBox, add following code 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:src="clr-namespace:SilverlightApplication"
    Width="200" Height="200">
    <Grid x:Name="LayoutRoot" Background="White" Margin="5">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBlock Text="Make:"/>
        <ComboBox ItemsSource="{Binding Path=CarListProvider.MakeList}"
                  DisplayMemberPath="MakeName"
                  Grid.Column="1"
                  SelectedItem="{Binding Path=MakeInfo, Mode=TwoWay}"
                  x:Name="makeComboBox"
                  />
        <TextBlock Grid.Row="1" Text="Model:"/>
        <ComboBox ItemsSource="{Binding Path=CarListProvider.ModelList}"
                  DisplayMemberPath="ModelName"
                  Grid.Column="1"
                  Grid.Row="1"
                  SelectedItem="{Binding Path=ModelInfo, Mode=TwoWay}"
                  IsEnabled="{Binding Path=CarListProvider.IsModelListEnabled}"
                  />
        <TextBlock Grid.Row="2" Text="Color:"/>
        <ComboBox ItemsSource="{Binding Path=CarListProvider.ColorList}"
                  Grid.Column="1"
                  Grid.Row="2"
                  SelectedItem="{Binding Path=Color, Mode=TwoWay}"
                  IsEnabled="{Binding Path=CarListProvider.IsColorListEnabled}"
                  />
    </Grid>
</UserControl>

We are data binding ItemsSource of each ComboBox to corresponding list properties of the CarListProvider. This allows us to update ItemsSource and provide new list data whenever user makes changes to parent selection. Notice that for Make and Model ComboBox, we are also setting DisplayMemberPath to underlying classes Name methods. SelectedItem is bound to Car’s helper properties, that behind the scene/internally set Id properties. Finally IsEnabled property allows us to disable ComboBox when there is pending parent selection.

Add following code to Page.xaml.cs code behind

public partial class Page : UserControl {
    public Page() {
        InitializeComponent();
        //
        this.DataContext = EmptyFactory.GetEmptyCar();
        //this.DataContext = new Car(CarMake.GetEmpty(),CarModel.GetEmpty(),"Please select a color");
    }
}

F5 and test the application.

image  image   image   image

When use selects a Make, Model list only show appropriate Models for the selected Make. Change Make and notice that Model list is also changed. Whenever Model changes, Color list is also changed.

Cascading ComboBox in DataGrid

So far we have seen how to model Cascading ComboBox behavior in a simple one record FormView kind of scenario. However when you want to show Cascading ComboBox in a DataGrid, we have to deal with multiple rows.

imageEach row represents a Car and user can select different make, model and color for each Car. Traditionally CarListProvider will be separate from Car, perhaps created in a control resource. However this does not work for the multi row scenarios. Each row needs to maintain its own state, so that changes in one do not affect other.

This can be achieved by scoping CarListProvider at the Car level. This is the reason we created and exposed CarListProvider from the Car class earlier. It is optional in single record scenarios, but a requirement for the multi record mode.

In a production application, to prevent multiple calls back to server, CarListProvider should be backed up by a client side service wrapper class, that all CarListProviders get data from. This class should cache the list data and reduce redundant roundtrips to the server.

Change Page.xaml as shown.

<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"
    xmlns:src="clr-namespace:SilverlightApplication"
    >
    <Grid x:Name="LayoutRoot" Background="White">
        <data:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding}" Margin="10" ColumnWidth="100">
            <data:DataGrid.Columns>
                <data:DataGridTemplateColumn Header="Make" SortMemberPath="MakeInfo.MakeName" >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding MakeInfo.MakeName}" />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <ComboBox ItemsSource="{Binding CarListProvider.MakeList}" 
                                      SelectedItem="{Binding MakeInfo, Mode=TwoWay}" 
                                      DisplayMemberPath="MakeName"
                                      src:ComboBoxService.ForceOpen="True"/>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn Header="Model"  SortMemberPath="ModelInfo.ModelName">
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding ModelInfo.ModelName}" />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <ComboBox ItemsSource="{Binding CarListProvider.ModelList}" 
                                      DisplayMemberPath="ModelName"
                                      src:ComboBoxService.ForceOpen="true"
                                      SelectedItem="{Binding ModelInfo, Mode=TwoWay}" 
                                      IsEnabled="{Binding Path=CarListProvider.IsModelListEnabled}"/>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn Header="Color" SortMemberPath="Color" >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Color}"/>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <ComboBox ItemsSource="{Binding CarListProvider.ColorList}" 
                                      src:ComboBoxService.ForceOpen="true"
                                      SelectedItem="{Binding Color, Mode=TwoWay}" 
                                      IsEnabled="{Binding Path=CarListProvider.IsColorListEnabled}"/>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
            </data:DataGrid.Columns>
        </data:DataGrid>
    </Grid>
</UserControl>

Change page.xaml.cs

ObservableCollection<Car> _cars;
public Page() {
    InitializeComponent();
    //
    _cars = new ObservableCollection<Car>() {
        new Car(
            new CarMake { MakeId = 100, MakeName = "Toyota (100)" },
            new CarModel { ModelId = 101, ModelName = "Model101" },
            "Red101"
        ),
        new Car(
            new CarMake { MakeId = 300, MakeName = "Nissan (300)" },
            new CarModel { ModelId = 302, ModelName = "Model302" },
            "White302"
        ),
        EmptyFactory.GetEmptyCar()
    };
    this.DataContext = _cars;
}   

F5 and test the application:

image image   image99

Change a Car’s Make, note that model list for that make is shown on that row. If you check model list on another row, it will be for that row’s make.

Note that we have a slight quirk in the behavior, when there is a pending parent selection, child still show a disabled dropdown. It would be best not to show dropdown at all, but that will require change to template at run time and increase complexity quite a bit. But it is still workable, as user is not able to select. Alternatively you can always write a custom DataGrid column and achieve the proper UI behavior. Also note that even though we used data from one ComboBox to drive other ComboBox, any other field can be used to drive the ComboBox.

Source Code:CascadingComboBox.zip

Technorati Tags:
Updated Samples for Silverlight 2

Now that Silverlight 2 has been released, I have updated all the samples. Conversion was mostly straight forward, with very few changes to code.

Building Business Applications with Silverlight 2

Introduces the basic building blocks of a data centric application (primarily using DataGrid control), which can be used to develop full fledge business applications.

Part 1: Adding new item

Part 2: Deleting item

Part 3: Validation (sync)

Part 4: Validation (async, across the wire)

Part 5: Validation (refactored)

Part 6: IEditableObject and Add new item (Take 3!)

Part 7: Beyond Validation, Prevention

All code ported properly except for EndEdit method on DataGrid. DataGrid no longer provides EndEdit method. Instead of calling DataGrid.EndEdit(), we just call EndEdit on the underlying data item that implements IEditableObject interface.

Old Code (Beta 2)

void peopleDataGrid_KeyUp(object sender, KeyEventArgs e) {
    if (Key.Down == e.Key || Key.Enter == e.Key) {
        peopleDataGrid.EndEdit(true, true);
    }
}

New Code (RTW)

void peopleDataGrid_KeyUp(object sender, KeyEventArgs e) {
    if (Key.Down == e.Key || Key.Enter == e.Key) {
        ((Person)peopleDataGrid.SelectedItem).EndEdit();
    }
}

Another change was from DisplayMemberBinding to Binding for data binding.

Source Code for the Business Application:BusinessApp1.zip

Restricting User Input using Attached Property

Highlights use of Attached property to implement text filtering to restrict user input to selected type, such as numeric, decimal and others. Also introduces basics of Attached property and showcases various ways of using them.

Only code change was rename of Source property on KeyEventArgs. Source was renamed to OriginalSource

Old Code (Beta 2)

private static void textBox_KeyDown(object sender, KeyEventArgs e) {
    // bypass other keys!
    if (IsValidOtherKey(e.Key)) {
        return;
    }
    //
    TextBoxFilterType filterType = GetFilter((DependencyObject)sender);
    TextBox textBox = sender as TextBox;
    if (null == textBox) {
        textBox = e.Source as TextBox;
    }

New Code (RTW)

private static void textBox_KeyDown(object sender, KeyEventArgs e) {
    // bypass other keys!
    if (IsValidOtherKey(e.Key)) {
        return;
    }
    //
    TextBoxFilterType filterType = GetFilter((DependencyObject)sender);
    TextBox textBox = sender as TextBox;
    if (null == textBox) {
        textBox = e.OriginalSource as TextBox;
    }

Source Code: FilterService.zip

ComboBox In DataGrid

Examines the usage of ComboBox in DataGrid. In particular, it shows how to implement foreign key scenarios in lieu of SelectedValue/SelectedValuePath property. It also highlights workaround for a bug in that causes ComboBox dropdown to close immediately in DataGrid.

No code change was required to the original RC0 code, just a recompile. But that also means ComboBox in DataGrid still has display bug where it immediately closes. You can use following attached property to force it to open. Please see the original post for full explanation.

public class ComboBoxService {
    public static readonly DependencyProperty ForceOpenProperty =
        DependencyProperty.RegisterAttached("ForceOpen", typeof(bool), typeof(ComboBoxService),
                                            new PropertyMetadata(OnForceOpenChanged));

    public static bool GetForceOpen(DependencyObject d) {
        return (bool)d.GetValue(ForceOpenProperty);
    }

    public static void SetForceOpen(DependencyObject d, bool value) {
        d.SetValue(ForceOpenProperty, value);
    }

    private static void OnForceOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
        ComboBox comboBox = d as ComboBox;
        if ((bool)e.OldValue) {
            comboBox.Loaded -= new RoutedEventHandler(comboBox_Loaded);
        }
        if ((bool)e.NewValue) {
            comboBox.Loaded +=new RoutedEventHandler(comboBox_Loaded);
        }
    }

    static void comboBox_Loaded(object sender, RoutedEventArgs e) {
        ComboBox comboBox = sender as ComboBox;
        if (null == comboBox) {
            comboBox = e.OriginalSource as ComboBox;
        }
        //
        comboBox.IsDropDownOpen = true;
    }
}

Usage

<ComboBox ItemsSource="{Binding CityList, Source={StaticResource cityProvider}}" 
          SelectedItem="{Binding CityInfo, Mode=TwoWay}" 
          DisplayMemberPath="CityName"
          src:ComboBoxService.ForceOpen="true"
      />

Tip: There is a quirk in current implementation of ComboBox. If you set SelectedItem before ItemSource, item will not be highlighted in dropdown. Always first set ItemSource and then SelectedItem.

Source Code: ComboBoxUsage.zip

Stealth Paging : DataGrid

Shows how to combine efficient chunked data retrieval of the web, with continuous scroll experience of the desktop to implement stealth or on demand background paging.

No code change was required to the original RC0 code, just a recompile.

Source Code: StealthPaging.zip

Technorati Tags:
More Posts Next page »