Kolekcje w WPF – sortowanie, grupowanie, filtrowanie oraz nawigacja

2012-12-27

Poprzez mechanizm wiązania danych WPF pozwala w łatwy sposób prezentować zawartość kolekcji obiektów. Jeżeli jednak oprócz samego wyświetlania elementów zależy nam na ich sortowaniu, grupowaniu, filtrowaniu czy nawigacji po nich, standardowe możliwości kolekcji okażą się niewystarczające. W takiej sytuacji idealnym rozwiązaniem będzie użycie widoku kolekcji. CollectionView jest swojego rodzaju wraperem na kolekcję umożliwiającym wykonanie powyższych operacji. Co więcej wszystkie te operacje nie mają wpływu na zbiór źródłowy, co z kolei pozwala na utworzenie kilku niezależnych widoków dla danej kolekcji.

Tworzenie widoku kolekcji

Załóżmy, że mamy następującą klasę:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

Poniższy kod tworzy kolekcję obiektów Person oraz widok dla tej kolekcji:

ObservableCollection<Person> Persons = new ObservableCollection<Person>();

//Pobranie domyślnego widoku kolekcji (każda kolekcja posiada widok domyślny)
ICollectionView PersonsView = CollectionViewSource.GetDefaultView(Persons);

//Utworzenie nowego widoku kolekcji
ICollectionView NewPersonsView = new ListCollectionView(Persons);

Widok możemy zastosować dla dowolnej kolekcji implementującej interfejs IEnumerable (kolekcja źródłowa dostępna jest poprzez właściwość SourceCollection widoku). Obiekt będący widokiem może być następnie użyty jako źródło danych w kontrolkach WPF. Jeżeli kolekcja implementuje interfejs INotifyCollectionChanged wszystkie dokonywane w niej zmiany (dodanie, usunięcie, zmiana elementu) będą automatycznie uwzględniane zarówno w widoku jak i powiązanych z nim kontrolkach. Jeżeli w danym momencie chcemy wymusić odświeżenie widoku możemy użyć metody Refresh.

W zależności od rodzaju kolekcji źródłowej metoda CollectionViewSource.GetDefaultView zwraca obiekt:

  • CollectionView – dla kolekcji implementującej jedynie IEnumerable
  • ListCollectionView – dla kolekcji implementującej IList
  • BindingListCollectionView – dla kolekcji implementującej IBindingListView lub IBindingList

Powyższe klasy mogą być obsługiwane poprzez interfejs ICollectionView. Interfejs ten implementuje klasa CollectionView, po której z kolei dziedziczą klasy ListCollectionView oraz BindingListCollectionView.

Widok kolekcji możemy również stworzyć bezpośrednio w kodzie XAML. Do tego celu służy klasa CollectionViewSource będąca w XAML-u odpowiednikiem klasy CollectionView:

<Window.Resources>
    <local:Persons x:Key="persons" />
    <CollectionViewSource x:Key="personsView" Source="{StaticResource persons}" />
</Window.Resources>

<DataGrid x:Name="dataGrid1"
          ItemsSource="{Binding Source={StaticResource personsView}}">

Nawigacja

Widok kolekcji pozwala na śledzenie aktualnie wybranego elementu oraz jego zmianę przy użyciu szeregu metod. Bieżący element dostępny jest poprzez właściwość CurrentItem, natomiast jego indeks poprzez CurrentPosition:

Person currentPerson = PersonsView.CurrentItem as Person;
int currentPosition = PersonsView.CurrentPosition;

W celu synchronizacji aktualnie wybranego elementu pomiędzy widokiem kolekcji a kontrolką, w kontrolce należy ustawić właściwość IsSynchronizedWithCurrentItem:

<DataGrid ItemsSource="{Binding PersonsView}" IsSynchronizedWithCurrentItem="True">

W przypadku zmiany aktualnie wybranego elementu, na widoku kolekcji wywoływane jest zdarzenie CurrentChanged:

PersonsView.CurrentChanged += PersonsViewCurrentChanged;

private void PersonsViewCurrentChanged(object sender, EventArgs e)
{
    //Kod wykonywany po zmianie bieżącego elementu
}

Zmiany bieżącego elementu możemy dokonać za pomocą metod: MoveCurrentToFirst, MoveCurrentToPrevious, MoveCurrentToNext, MoveCurrentToLast, MoveCurrentToPosition, MoveCurrentTo. Oto ich użycie:

public void MoveToFirst()
{
    PersonsView.MoveCurrentToFirst();
}

public void MoveToNext()
{
    if (PersonsView.CurrentPosition < (PersonsView.Cast<Person>().Count() - 1))
        PersonsView.MoveCurrentToNext();
}

public void MoveToPrev()
{
    if (PersonsView.CurrentPosition > 0)
        PersonsView.MoveCurrentToPrevious();
}

public void MoveToLast()
{
    PersonsView.MoveCurrentToLast();
}

public void MoveToPosition(int position)
{
    PersonsView.MoveCurrentToPosition(position);
}

public void MoveTo(Person person)
{
    PersonsView.MoveCurrentTo(person);
}

Warto zaznaczyć, że w przypadku pobierania liczby wszystkich elementów widoku należy posłużyć się metodą Cast (konwertuje ona widok na obiekt IEnumerable), a następnie metodą Count. Odczyt liczby elementów z kolekcji źródłowej nie zawsze jest właściwy ponieważ widok może posiadać filtr ograniczający liczbę elementów.

Sortowanie

Do definiowania sortowania elementów widoku służy kolekcja SortDescriptions składająca się z obiektów SortDescription. Obiekty te określają właściwości po jakich następuje sortowanie oraz jego kierunek (rosnąco/malejąco):

if (PersonsView.CanSort)
{
    using (PersonsView.DeferRefresh())
    {
        PersonsView.SortDescriptions.Clear();
        PersonsView.SortDescriptions.Add(new SortDescription("LastName",
            ListSortDirection.Ascending));
        PersonsView.SortDescriptions.Add(new SortDescription("FirstName",
            ListSortDirection.Ascending));
    }
}

Metoda DeferRefresh opóźnia odświeżenie widoku do czasu zakończenia na nim wszystkich operacji.

Sortowanie zdefiniować możemy także w kodzie XAML:

<!-- xmlns:cm="clr-namespace:System.ComponentModel;assembly=WindowsBase" -->
<CollectionViewSource.SortDescriptions>
    <cm:SortDescription PropertyName="LastName" />
    <cm:SortDescription PropertyName="FirstName" />
</CollectionViewSource.SortDescriptions>

Niestety powyższy sposób sortowania ma jedną wadę – niską wydajność. Przy dużych kolekcjach jest on nieefektywny. Istnieje jednak alternatywa w postaci właściwości CustomSort widoku, do której przypisujemy własny obiekt implementujący interfejs IComparer (właściwość dostępna jest w klasie ListCollectionView):

ListCollectionView personsView = PersonsView as ListCollectionView;
personsView.CustomSort = new PersonSorter();

public class PersonSorter : IComparer
{
    public int Compare(object a, object b)
    {
        int result;
        Person personA = a as Person;
        Person personB = b as Person;
        result = personA.LastName.CompareTo(personB.LastName);
        if (result == 0)
            result = personA.FirstName.CompareTo(personB.FirstName);
        return result;
    }
}

Grupowanie

Grupowanie elementów widoku tworzymy poprzez kolekcję GroupDescriptions. Składa się ona z obiektów PropertyGroupDescription definiujących właściwości, po których wykonywane jest grupowanie:

if (PersonsView.CanGroup)
{
    using (PersonsView.DeferRefresh())
    {
        PersonsView.GroupDescriptions.Clear();
        PersonsView.GroupDescriptions.Add(new PropertyGroupDescription("LastName"));
    }
}

Podczas definiowania grupowania możemy użyć przeciążonego konstruktora PropertyGroupDescription przyjmującego jako drugi argument obiekt IValueConverter. Pozwala on na dowolną konwersję wartości właściwości przed użyciem jej w grupowaniu (możemy na przykład dla właściwości Age klasy Person stworzyć grupowanie po przedziałach wartości).

Grupowanie utworzyć możemy także w kodzie XAML:

<CollectionViewSource.GroupDescriptions>
    <PropertyGroupDescription PropertyName="LastName" />
</CollectionViewSource.GroupDescriptions>

W celu prezentacji zgrupowanych danych musimy zdefiniować odpowiedni styl wykorzystując właściwość GroupStyle kontrolki. Oto przykładowy styl:

<DataGrid.GroupStyle>
    <GroupStyle>
        <GroupStyle.HeaderTemplate>
            <DataTemplate>
                <DockPanel Background="Navy">
                    <TextBlock Text="{Binding Name}" Foreground="White"
                               Margin="30,0,0,0" Width="100"/>
                    <TextBlock Text="{Binding ItemCount}" Foreground="White"/>
                </DockPanel>
            </DataTemplate>
        </GroupStyle.HeaderTemplate>
    </GroupStyle>
</DataGrid.GroupStyle>

Właściwość GroupStyle dostępna jest w każdej kontrolce dziedziczącej po ItemsControl.

Należy pamiętać, że użycie grupowania danych oznacza wyłączenie wirtualizacji, co przy dużych kolekcjach może mieć negatywny wpływ na wydajność.

Filtrowanie

Włączenie filtrowania widoku polega na przypisaniu do jego właściwości Filter metody odpowiedzialnej za selekcję elementów. Metoda w parametrze przyjmuje element kolekcji, w wyniku zwracając wartość logiczną określającą czy dany element spełnia warunki filtrowania:

if (PersonsView.CanFilter)
    PersonsView.Filter = PersonFilter;

private bool PersonFilter(object item)
{
    Person person = item as Person;
    return person.Age >= 18;
}

W przypadku zmiany warunków filtrowania należy odświeżyć widok przy użyciu metody Refresh.

Podczas definiowania filtrowania z poziomu kodu XAML musimy posłużyć się zdarzeniem Filter obiektu CollectionViewSource:

//XAML
<CollectionViewSource x:Key="personsView" Source="{StaticResource persons}"
                      Filter="CollectionViewSource_Filter" />

//C# (Code-behind)
private void CollectionViewSource_Filter(object sender, FilterEventArgs e)
{
    Person person = e.Item as Person;
    e.Accepted = person.Age >= 18;
}

Więcej informacji można znaleźć na MSDN: CollectionViewSource, CollectionViewListCollectionView, BindingListCollectionView, How to: Group, Sort, and Filter Data in the DataGrid Control.

Reklamy

Posted on 2012-12-27, in .NET/C# and tagged , , , . Bookmark the permalink. 1 komentarz.

  1. Super artykul, piekne dzieki!

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj / Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj / Zmień )

Zdjęcie na Google+

Komentujesz korzystając z konta Google+. Wyloguj / Zmień )

Connecting to %s

%d blogerów lubi to: