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
The other day I wrote a PagingController
class to help with paging, so here you go:
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.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With