Manish Dalal's blog

Exploring .net!

October 2008 - Posts

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:
Stealth Paging : DataGrid

It is quite common to see paging of data in today’s web application. It allows developer to show only a fixed set of rows at a time and pulling next set of rows on demand, improving overall user experience.

In a desktop application, user normally does not have to deal with explicit paging. User experience is that of a continuous data stream, user just scrolls to see additional data.

It is certainly possible to replicate explicit paging functionality in Silverlight. However Silverlight is supposed to be a RIA platform, combining best features of web and desktop. So let see how we can combine these two concepts, efficient chunked data retrieval of the web, with continuous scroll experience of the desktop. Enter Stealth Paging.

Stealth Paging

Stealth paging refers to the concept of retrieving data in background just before it is needed. You start by loading DataGrid with only one or two pages worth of data, just enough to cover the screen and some more. Now when user starts scrolling, and nears the end of available rows to show, we fetch additional data and add it to the data collection. User continues scrolling, without ever realizing that data was fetched in the background.

We will start with a simple cumulative paging and then enhance it to implement the stealth mode.

Simple Cumulative Paging

In traditional web paging scenario, user is only shown one page of data at a time. Each new page replaces previous page, forcing user to page back to see the previous data. Instead of replacing one page of data with next page of data, we will just add to the existing data. This way user has access to all the current and previous data, and only needs to page to get next set of data.

Create a new Silverlight application project. Also create the corresponding web application to host and test the Silverlight application

Step 1 – PeopleService

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

using System.Runtime.Serialization;

namespace Silverlight.Web {
    [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; }
        }

        private string _city;
        [DataMember]
        public string City {
            get { return _city; }
            set { _city = value; }
        }
    }
}

Note that Person class is marked with DataContract attribute and individual fields with DataMemeber attribute. Next add a simple service to provide data for the client. Add 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(int startRow, int endRow) {
    List<Person> personList = new List<Person>();
    for (int i = startRow; i < endRow; 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)
        });
    }
    //
    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.

Step 2 - Client

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

Add following xaml to page.xaml.

<UserControl x:Class="Silverlight.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"/>
        <Button x:Name="nextPageButton" Content="Next Page" Grid.Row="1" HorizontalAlignment="Right" Margin="2,2,20,2"/>
    </Grid>
</UserControl>

Here we are adding DataGrid and also button to fetch the next set of data. Note that unlike traditional web application, we do not have previous button as we are accumulating all the data.

Add following code to page.xaml.cs code behind

 
public partial class Page : UserControl {
    
    int _startRow;
    int _pageSize = 20;
    ObservableCollection<Person> _people;

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

    void Page_Loaded(object sender, RoutedEventArgs e) {
        _startRow = 0;
        _people = new ObservableCollection<Person>();
        peopleDataGrid.ItemsSource = _people;
        nextPageButton.IsEnabled = false;
        GetData(_startRow, _pageSize);
    }

    void nextPageButton_Click(object sender, RoutedEventArgs e) {
        GetData(_startRow, _startRow + _pageSize);
    }
    
    private void GetData(int startRow, int endRow) {
        PeopleServiceClient proxy = new PeopleServiceClient();
        proxy.GetDataCompleted += new EventHandler<GetDataCompletedEventArgs>(proxy_GetDataCompleted);
        nextPageButton.IsEnabled = false;
        this.Cursor = Cursors.Wait;
        proxy.GetDataAsync(startRow, endRow);
    }

    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;
        }
        _startRow += _pageSize;
        foreach (Person person in e.Result) {
            _people.Add(person);
        }
        nextPageButton.IsEnabled = true;
    }
}

Here we are loading 20 rows at startup. When user clicks on next page button, we call backend service and get additional 20 rows of data.

image

F5 and test the application.

image

Stealth Paging

Now let make the paging automatic. Instead of asking user to click on the next button to fetch next set of data, we will get next set of data automatically when required, providing perception of one continuous scrollable page, similar to what user is already accustomed in a desktop application.

Comment out nextPageButton in xaml and code behind. Add an event handler for DataGrid.LoadingRow event. Also create class level variable _loading to track when data is being loaded.

 
public Page() {
    InitializeComponent();
    this.Loaded += new RoutedEventHandler(Page_Loaded);
    //this.nextPageButton.Click += new RoutedEventHandler(nextPageButton_Click);
    this.peopleDataGrid.LoadingRow += new EventHandler<DataGridRowEventArgs>(peopleDataGrid_LoadingRow);
}

void peopleDataGrid_LoadingRow(object sender, DataGridRowEventArgs e) {
    if (_loading || _people.Count < _pageSize) {
        return;
    }
    if (_people.Count - 5 < e.Row.GetIndex()) {
        GetData(_startRow, _startRow + _pageSize);
    }
}

What enables stealth paging is the detection of need for more data. It would be great if there was some way to let DataGrid know of total number of rows and event that will trigger when DataGrid needs next set of data, but current implementation does not have that feature. But we can engineer our own functionality using LoadingRow event.

image

LoadingRow event is fired just before the DataGrid is about to display the row. This event enable us to be stealthy by providing us with the crucial row number (using e.Row.GetIndex()). We are constantly looking for the row being displayed. When the last from the 5th row is about to be display, we know that we are near the end (only 4 rows are left to be shown) and make call to backend service to get additional data. Newly fetched data is added to the existing data collection, forcing DataGrid to add new rows and in turn increasing the scrollable area. As user continues scrolling down and every time we are near the end of available rows to show, we fetch additional data.

If you run the application, and start scrolling down, you will see that size of scroll thumb reduces progressively as we add more data.

image image

One caveat is a little jerkiness in the scrollbar position and obvious change to height of scroll thumb. Other caveat is that we have removed random access to data that traditional explicit paging provides.

Hopefully DataGrid will evolve to provide features to enable stealth paging in future, but till then you can use LoadingRow technique to provide user with a stealth paging experience.

Source Code: StealthPaging.zip

Technorati Tags:
More Posts