Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVVM paging & sorting

I am struggling with finding an adequate solution to impletmenting Sorting and Paging for a WPF DataGrid that conforms to the MVVM P&P.

The following example illustrates an effective way to implement paging which follows MVVM practices, but the custom implementation of sorting (which is required once you implement paging) does not follow MVVM:

http://www.eggheadcafe.com/tutorials/aspnet/8a2ea78b-f1e3-45b4-93ef-32b2d802ae17/wpf-datagrid-custom-pagin.aspx

I currently have a DataGrid bound to a CollectionViewSource (defined in XAML with GroupDescriptions and SortDescritptions) bound to an ObservableCollection in my ViewModel. As soon as you implement Paging by limiting the number of items your DataGrid gets per page, it breaks the sorting defined in the CollectionViewSource because it is only sorting the subset of items. What is the best approach under MVVM to implement Paging and Sorting?

Thanks,

Aaron

like image 736
Aaron Avatar asked Mar 14 '11 23:03

Aaron


2 Answers

The other day I wrote a PagingController class to help with paging, so here you go:

  • PagingController.cs
  • CurrentPageChangedEventArgs.cs

You will have to clean up the sources a bit because the make some use of MS Code Contracts, they reference some (really basic) utility stuff from Prism, etc.

Usage sample (codebehind - ViewModel.cs):

private const int PageSize = 20;

private static readonly SortDescription DefaultSortOrder = new SortDescription("Id", ListSortDirection.Ascending);

private readonly ObservableCollection<Reservation> reservations = new ObservableCollection<Reservation>();

private readonly CollectionViewSource reservationsViewSource = new CollectionViewSource();

public ViewModel()
{
    this.reservationsViewSource.Source = this.reservations;

    var sortDescriptions = (INotifyCollectionChanged)this.reservationsViewSource.View.SortDescriptions;
    sortDescriptions.CollectionChanged += this.OnSortOrderChanged;

    // The 5000 here is the total number of reservations
    this.Pager = new PagingController(5000, PageSize);
    this.Pager.CurrentPageChanged += (s, e) => this.UpdateData();

    this.UpdateData();

}

public PagingController Pager { get; private set; }

public ICollectionView Reservations
{
    get { return this.reservationsViewSource.View; }
}

private void UpdateData()
{
    var currentSort = this.reservationsViewSource.View.SortDescriptions.DefaultIfEmpty(DefaultSortOrder).ToArray();

    // This is the "fetch the data" method, the implementation of which
    // does not directly interest us for this example.
    var data = this.crsService.GetReservations(this.Pager.CurrentPageStartIndex, this.Pager.PageSize, currentSort);
    this.reservations.Clear();
    this.reservations.AddRange(data);
}

private void OnSortOrderChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.Action == NotifyCollectionChangedAction.Add) {
        this.UpdateData();
    }
}

Usage sample (XAML - View.xaml):

<DataGrid ... ItemSource="{Binding Reservations}" />

<!-- all the rest is UI to interact with the pager -->
<StackPanel>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="4">
        <StackPanel.Resources>
            <Style TargetType="{x:Type Button}">
                <Setter Property="FontFamily" Value="Webdings" />
                <Setter Property="Width" Value="60" />
                <Setter Property="Margin" Value="4,0,4,0" />
            </Style>
            <Style TargetType="{x:Type TextBlock}">
                <Setter Property="Margin" Value="4,0,4,0" />
                <Setter Property="VerticalAlignment" Value="Center" />
            </Style>
            <Style TargetType="{x:Type TextBox}">
                <Setter Property="Margin" Value="4,0,4,0" />
                <Setter Property="Width" Value="40" />
            </Style>
        </StackPanel.Resources>
        <Button Content="9" Command="{Binding Path=Pager.GotoFirstPageCommand}" />
        <Button Content="3" Command="{Binding Path=Pager.GotoPreviousPageCommand}" />
        <TextBlock Text="Page" />
        <TextBox Text="{Binding Path=Pager.CurrentPage, ValidatesOnExceptions=True}" />
        <TextBlock Text="{Binding Path=Pager.PageCount, StringFormat=of {0}}" />
        <Button Content="4" Command="{Binding Path=Pager.GotoNextPageCommand}" />
        <Button Content=":" Command="{Binding Path=Pager.GotoLastPageCommand}" />
    </StackPanel>
    <ScrollBar Orientation="Horizontal" Minimum="1" Maximum="{Binding Path=Pager.PageCount}" Value="{Binding Path=Pager.CurrentPage}"/>
</StackPanel>

Short explanation:

As you see, the ViewModel doesn't really do much. It keeps a collection of items representing the current page, and exposes a CollectionView (for data binding) and a PagingController to the View. Then all it does is update the data items in the collection (and consequently in the CollectionView) every time the PagingController indicates that something has changed. Of course this means that you need a method that, given a starting index, a page size, and a SortDescription[] returns the slice of data described by these parameters. This is part of your business logic, and I haven't included code for that here.

On the XAML side all the work is done by binding to the PagingController. I have exposed the full functionality here (buttons bound to First/Prev/Next/Last commands, direct binding of a TextBox to CurrentPage, and binding of a ScrollBar to CurrentPage). Typically you will not use all of this at the same time.

like image 172
Jon Avatar answered Sep 19 '22 13:09

Jon


You should use a collection property of type ListCollectionView in your ViewModel, and bind the Grid to it. That way the CollectionView definition would not sit in the View but instead in the ViewModel (where it belongs) and that would help you do all the manipulations you want easily in the ViewModel (be it paging, sorting or filtering)

like image 20
Elad Katz Avatar answered Sep 21 '22 13:09

Elad Katz