Silverlightでカスタムページングをしたい時

また日記を放置していたので、月一くらいでは小ネタも書いておこうかと(・∀・;)
元ネタはこちらですが、ちょっと使ったのでSilverlightでのカスタムページングについて。
http://weblogs.asp.net/manishdalal/archive/2009/10/01/silverlight-3-custom-sorting-with-paging-support.aspx


SilverlightでDataGrid、DataPagerを使ってデータ管理アプリケーションを作るときの話ですが(・ω・)
Silverlightで作ったクライアントにデータを提供するサービス側も.NETで作って、WCF RIA ServicesでLinqToEntitiesDomainServiceなんかを使えば、ページング処理なんかも楽ちんぽんと作れて、ちゃんとSkip() & Take()した必要なデータだけの取得なんかも出来ちゃうわけですが。
でも、サービス側がJava製だったり既存のRESTなサービスを使いたいときとか、パラメータを自分で組み立ててデータ取得の通信を行いたいときにはどうするか(・ω・)?
もちろん、ページに必要なデータのみをオンデマンドで取得する方法で。*1


っで、そこで登場するのが、ICollectionView、IPagedCollectionViewですね(・∀・)
ICollectionViewの定義はこんなんですが。

public interface ICollectionView : IEnumerable, INotifyCollectionChanged
{
    event EventHandler CurrentChanged;
    event CurrentChangingEventHandler CurrentChanging;

    bool Contains(object item);
    IDisposable DeferRefresh();
    bool MoveCurrentTo(object item);
    bool MoveCurrentToFirst();
    bool MoveCurrentToLast();
    bool MoveCurrentToNext();
    bool MoveCurrentToPosition(int position);
    bool MoveCurrentToPrevious();
    void Refresh();

    bool CanFilter { get; }
    bool CanGroup { get; }
    bool CanSort { get; }
    CultureInfo Culture { get; set; }
    object CurrentItem { get; }
    int CurrentPosition { get; }
    Predicate<object> Filter { get; set; }
    ObservableCollection<GroupDescription> GroupDescriptions { get; }
    ReadOnlyObservableCollection<object> Groups { get; }
    bool IsCurrentAfterLast { get; }
    bool IsCurrentBeforeFirst { get; }
    bool IsEmpty { get; }
    SortDescriptionCollection SortDescriptions { get; }
    IEnumerable SourceCollection { get; }
}

カスタムページングに関連するのは、CanSort、SortDescriptions、Refresh()/DeferRefresh()のあたり(・ω・)
ICollectionViewの実装をDataGridにバインドしてソートをすると、DeferRefresh()が呼び出されます。
DeferRefresh()は更新遅延用のメカニズムなので、IDisposable.Dispose()内でRefresh()メソッドをコールバックするオブジェクトを返してあげればOK。
っで、Refresh()内ではイベントをあげて、ソート条件にあったデータの取得処理を呼び出し、コレクションを再構築してあげるっと。
ソートで使用する列や昇順降順はSortDescriptionsプロパティを参照、っと。


っで、ICollectionViewの実装としてはObservableCollectionを継承したクラスを作る方向で(・ω・)
CurrentItem、CurrentPositionに対応するための現在位置のプロパティを用意して、コレクションの操作時にはその情報も更新して。
CanFilter、CanGroupのあたりは、とりあえずソートに対応するだけであればfalseを返す実装でOK。
あとは、MoveXxx()を実装して、CurrentChanged、CurrentChanging及びOnPropertyChanged()を適切に呼び出してあげれば、オンデマンドでのデータ取得に対応したコレクションの完成っと。


更にページングに対応するためには、IPagedCollectionViewも実装(・ω・)
IPagedCollectionViewの定義はこんなんですが。

public interface IPagedCollectionView
{
    event EventHandler<EventArgs> PageChanged;
    event EventHandler<PageChangingEventArgs> PageChanging;

    bool MoveToFirstPage();
    bool MoveToLastPage();
    bool MoveToNextPage();
    bool MoveToPage(int pageIndex);
    bool MoveToPreviousPage();

    bool CanChangePage { get; }
    bool IsPageChanging { get; }
    int ItemCount { get; }
    int PageIndex { get; }
    int PageSize { get; set; }
    int TotalItemCount { get; }
}

IPagedCollectionViewの実装をDataPagerにバインドすると、ページの変更時にはMoveToXxx()が呼び出されるので(・ω・)
ICollectionViewの実装にIPagedCollectionViewも実装してあげて、MoveToXxx()の中でRefresh()を呼び出すようにすれば、ページングもオンデマンドで行えるようになるっと。


っで、リンク先からはその実装例がダウンロードできますね(・∀・)
ICollectionView及びIPagedCollectionView実装のPagedSortableCollectionViewの使い方はこんなんです。


まずViewModelをこんな感じで用意して。

public class HogeViewModel
{
    public PagedSortableCollectionView<Hoge> DataList { get; private set; }

    public int PageSize
    {
        get { return DataList.PageSize; }
    }

    public HogeViewModel()
    {
        DataList = new PagedSortableCollectionView<Hoge>();
        DataList.OnRefresh += new EventHandler<RefreshEventArgs>(DataList_OnRefresh);
        GetData();  // 初期データ取得
    }

    // DataGrid、DataPagerでの操作からこのハンドラが呼び出される
    private void DataList_OnRefresh(object sender, RefreshEventArgs e)
    {
        GetData();
    }

    private void GetData()
    {
        // 下記の条件を元に検索
        int take = DataList.PageSize;
        int skip = DataList.PageIndex * DataList.PageSize;
        foreach(SortDescription sortDesc in DataList.SortDescriptions) { ...  }

        // 本当はここで非同期通信開始
...
        // 完了ハンドラでデータ更新
        DataList.Clear();
        foreach (Hoge hoge in list) {
            DataList.Add(hoge);
        }
        DataList.TotalItemCount = count;
    }
}

DataGrid、DataPagerとはこんな風にバインディング

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.DataContext>
        <vm:PeopleViewModel />
    </Grid.DataContext>
    <data:DataGrid x:Name="dataGrid"
                   AutoGenerateColumns="True"
                   Grid.Row="0"
                   ItemsSource="{Binding DataList}" />
    <data:DataPager x:Name="dataPager"
                    Grid.Row="1"
                    PageSize="{Binding PageSize}"
                    DisplayMode="FirstLastPreviousNext"
                    IsTotalItemCountFixed="True"
                    Source="{Binding DataList}" />
</Grid>

これで、カスタムソート/ページングしたデータの取得をオンデマンドで行う仕組みができました(・∀・)
将来のWCFはRESTfulということですが、このあたりのサポートも強化されると嬉しいかな〜(・ω・)

*1:サンプルレベルのものだと、画面初期表示に必要なデータを取得するようなものしかなかったりしてあれですが(´д`;)