XAML.cz Magazín moderních technologií založených na XAML

DataGrid – grupování, řazení a filtrování

Napsáno pro WPF od Jarda Jirava  [07.09.2010]

Při představování novinek ve WPF 4.0 jsem také ukazoval zobrazovací prvek, který byl velice očekáván a u kterého by se dalo říct, že jakmile je přítomen, můžeme považovat technologii za použitelnou. Vždyť kdo z vás neslyšel od zákazníka požadavek, že by chtěl tu samou tabulku jakou má v Excelu. A právě k tomu slouží prvek DataGrid a dneska bych chtěl ukázat, jak v tomto gridu uskutečnit grupování, řazení a filtrování.

Připrava datového zdroje

První věc, kterou musíme zajistit je vhodný zdroj, ze kterého budeme zobrazovat data. Nejčastěji se asi bude jednat o data z databáze, ale můžeme použít i jakýkoliv jiný zdroj, třeba webovou službu. Důležitějši pro nás je, že pro bindování tohoto zdroje dat využijeme třídy CollectionViewSource, která bude zdrojem pro vlastnost ItemsSource našeho DataGridu. A vlastně v tom spočívá celé kouzlo, které postupně odhalím.

Pro ukázku jsem vybral vzorovou databázi Chinook, kterou si můžete sami stáhnout, a která je dostupná pro různé databázové servery, případně i ve formě xml souboru. Já jsem si vybral jen malou část a pomocí Entity frameworku jsem si vybral potřebná data. Jedna entita – řádek – je tak prezentován touto třídou.

public partial class TrackItem
{

    public string TrackName { get; set; }
    public string TrackComposer { get; set; }
    public int TrackLength { get; set; }
    public string TrackGenre { get; set; }
    public string TrackAlbum { get; set; }
    public string TrackArtist { get; set; }
}

Pro lepší práci jsem si pak ještě vytvořil potomka po třídě ObservableCollection<T>

public partial class TrackItems : ObservableCollection<TrackItem>
{
}

Jak vidíte, jedná se o jednoduché třídy, které naplním při vytváření formuláře a to dotazem pomocí Entity frameworku do Chinook databáze.

TrackItems items = this.Resources["tracks"] as TrackItems;
if (items != null)
{
    using (ChinookEntities db = new ChinookEntities())
    {
        var data = (from a in db.Tracks
             select new TrackItem()
             {
             TrackName = a.Name,
             TrackComposer = a.Composer,
             TrackLength = a.Milliseconds,
             TrackGenre = a.Genre.Name,
             TrackAlbum = a.Album.Title,
             TrackArtist = a.Album.Artist.Name
             }).Take(500);
        foreach (var item in data)
        {
            items.Add(item);
        }
    }
}

Zde je dobré si povšimnout hned prvního řádku, kdy se dotazujeme do kolekce Resources, aby nám to bylo jasné, co vlasně chceme, pojďme se podívat na zajímavější část a to samotný XAML kód, jak je deklarován a jenž vytváří veškerou logiku.

Jak vypadá XAML pro naše účely

Začnu tam, kde jsem v posledním odstavci skončil a to je vytvořením zdrojů pro formulář. Jak již víte z předchozích článků, tak každý element může mít svoje zdroje, které jsou platné pro daný scope, jelikož v tomto okamžiku budu chtít mít přístupný datový zdroj pro celý formulář, definuji si tento zdroj na úrovni formuláře.

<Window.Resources>
        <local:TrackItems x:Key="tracks" />
        <CollectionViewSource x:Key="TracksSource" Source="{StaticResource tracks}">
        </CollectionViewSource>
<local:Mili2TimeConverter x:Key="mili2time" />
</Window.Resources>

Jak vidíte, definoval jsem si vlastní namespace nazvaný local a odkázal jsem se na vytvořenou třídu TrackItems, zároveň jsem si tuto instanci pojmenoval jako tracks, což mi umožňuje, abych se na tuto instanci mohl dále odkazovat a to jak v deklarativní části, tak také v code behind. Toho jsem využil a tuto kolekci jsem si při nahrávání formuláře “vytáhl” a naplnil daty.

Dále je pak uveden zápis a provázání kolekce dat na CollectionViewSource a opět pojmenování tohoto zdroje, abych se na něj mohl dále odkazovat. Tuto část budu postupně rozvíjet jak se budu zmiňovat o jednotlivých možnostech, které dokáže DataGrid ve spolupráci s CollectionViewSource zobrazit.

Ještě než se pustím do samotné ukázky řazení, osvětlím předposlední řádek s uvedeným konvertorem a zároveň ukáži, jak vypadá samotná deklarace DataGridu. Já jsem využil možnosti definovat si sloupce sám, samozřejmě pro rychlé prototypování bude možné nechat vygenerovat sloupce samotných gridem, ale tuto definici jsem využil k tomu, abych lépe zobrazil čas skladby.

<DataGrid x:Name="grid" ItemsSource="{Binding Source={StaticResource TracksSource}}" CanUserAddRows="False" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Name" Binding="{Binding TrackName}" />
        <DataGridTextColumn Header="Composer" Binding="{Binding TrackComposer}" />
        <DataGridTextColumn Header="Length" Binding="{Binding TrackLength, Converter={StaticResource mili2time}}" />
        <DataGridTextColumn Header="Album" Binding="{Binding TrackAlbum}" />
        <DataGridTextColumn Header="Artist" Binding="{Binding TrackArtist}" />
        <DataGridTextColumn Header="Genre" Binding="{Binding TrackGenre}" />                
    </DataGrid.Columns>
</DataGrid>

Jak vidíte u sloupce Length je použito jak bindování, tak je také použit jednoduchý, v tomto případě jednosměrný, Convertor.

[ValueConversion(typeof(int), typeof(string))]
public class Mili2TimeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        int miliseconds = (int)value;
        TimeSpan ts = new TimeSpan(0, 0, 0, 0, miliseconds);
        return string.Format("{0:00}:{1:00}", ts.Minutes, ts.Seconds);            
    }
     public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Řazení v DataGridu

Konečně se dostávám k tomu nejzajímavějšímu, po nutných přípravách, které jak sami vidíte nejsou nikterak zdlouhavé, a to je řazení v DataGridu. Pokud byste si nyní spustili aplikaci, tak budou jednotlivé řádky zobrazeny tak, jak jsou vráceny databází, což samozřejmě můžete ovlivnit dotazem do databáze. Představte si však situaci, kdy toto ovlivnit nemůžete, například voláte webovou službu, která toto neumožňuje. Přesto chcete nabídnout uživateli prvotní seřazení podle některého sloupce, nebo hned podle několika sloupců.

Opět tedy využijeme již známý CollectionViewSource a doplníme do jeho deklarace informaci o tom, jakým způsobem se mají data řadit.

<CollectionViewSource.SortDescriptions>
        <base:SortDescription PropertyName="TrackAlbum" />
        <base:SortDescription PropertyName="TrackLength" />
</CollectionViewSource.SortDescriptions>

jediné co je třeba je doplnit namespace, abychom mohli použít SortDescription

xmlns:base="clr-namespace:System.ComponentModel;assembly=WindowsBase"

Od tohoto okamžiku se nám tak data budou řadit podle hodnot ve vlastnosti TrackAlbum a následně dle TrackLength. Samozřejmě máme možnost to samé provést i programově

ICollectionView tracks = CollectionViewSource.GetDefaultView(grid.ItemsSource);
if (tracks != null && tracks.CanSort == true)
{
    tracks.SortDescriptions.Clear();
    tracks.SortDescriptions.Add(new SortDescription("TrackAlbum", ListSortDirection.Ascending));
}
Všimněte si toho, že se dotazuji na DefaultView pro přiřazený datasource, což mi vrátí aktuální instanci CollectionView.

Filtrování v DataGridu

Pokud by vám přišlo, že řazení je vcelku jednoduché, pak vám filtrování v DataGridu musí přijít ještě jednodušší. CollectionViewSource nabízí událost Filter, do které můžeme zapsat naši filtrovací podmínku. Tato událost je vyvolána na každém řádku datového zdroje a to v okamžiku, kdy dojde k aktualizaci dat v DataGridu.

Pro demonstraci jsem tak doplnil do okna CheckBox, a nechal jsem si zobrazovat pouze skladby delší jak 4 minuty při jeho zatržení. Nejdříve jsem tedy musel reagovat na samotnou změnu stavu CheckBoxu a následně jsem se v obsluze události ptal, zda data zobrazit nebo nezobrazovat.

Jak jsem již zmínil, aby se aplikoval filter, je třeba zajistit obnovu dat, k tomu dobře poslouží metoda Refresh.

private void showLong_Checked(object sender, RoutedEventArgs e)
{
    CollectionViewSource.GetDefaultView(grid.ItemsSource).Refresh();
}

Pak už nic nebrání v tom, obsloužit událost Filter

private void CollectionViewSource_Filter(object sender, FilterEventArgs e)
{
    TrackItem item = e.Item as TrackItem;
    if (item != null)
    {
        if ((showLong.IsChecked == true) && (item.TrackLength < 240000))
        {
                e.Accepted = false;
        }
        else
        {
                e.Accepted = true;
        }
    }
}

Grupování v DataGridu

Pomalu se dostávám k cíly dnešního povídání a to je představení grupování v DataGridu. Opět k tomu využijeme možností, které nám nabízí zdroj dat, ale tentokrát budeme muset maličko uzpůsobit také DataGrid samotný a maličko mu napovědět, jak má zobrazovat zgrupovaná data.

Nejde všechno naráz, takže budu postupovat postupně, nejdříve od úpravy zdroje dat, kterým je CollectionViewSource. Zde je třeba doplnit informaci o tom, podle kterých vlastností budeme chtít data grupovat. Já jsem si vybral vlastnosti TrackGenre a TrackArtist, které k tomu téměř vybízely.

<CollectionViewSource.GroupDescriptions>
    <PropertyGroupDescription PropertyName="TrackGenre" />
    <PropertyGroupDescription PropertyName="TrackArtist" />
</CollectionViewSource.GroupDescriptions>

V tento okamžik, když pustíte aplikaci tak uvidíte stále stejný výsledek, neboť DataGrid neví nic o tom, jakým způsobem by měl zobrazit tyto grupované položky. Tuto informaci mu musíme deklarovat a to pomocí stylu.

<DataGrid.GroupStyle>
    <GroupStyle>
        <GroupStyle.HeaderTemplate>
            <DataTemplate>
                <DockPanel Background="Beige">
                    <TextBlock FontWeight="Bold" Text="{Binding Name}" Margin="5, 0, 0, 0" />
                    <TextBlock Text="{Binding ItemCount}" Margin="5, 0, 0, 0"/>
                </DockPanel>
            </DataTemplate>
        </GroupStyle.HeaderTemplate>
    </GroupStyle>
    <GroupStyle>
        <GroupStyle.HeaderTemplate>
            <DataTemplate>
                <DockPanel>
                    <TextBlock Text="{Binding Name}" Margin="5, 0, 0, 0" />
                    <TextBlock Text="{Binding ItemCount}" Margin="5, 0, 0, 0" />                </DockPanel>
            </DataTemplate>
        </GroupStyle.HeaderTemplate>
    </GroupStyle>
</DataGrid.GroupStyle>

Já jsem definoval dva maličko různé styly a to z toho důvodu, že jsem použil grupování podle dvou vlastností. Samozřejmě toto není nutné dělat pro každou grupovanou skupinu a v případě, že počet grupovaných skupin přesáhne počet stylů, použije se na přebývající skupiny poslední deklarovaný styl.

Výsledné zobrazení

Práce s DataGridem a datovým zdrojem v podobě CollectionViewSource je poměrně snadná a nabízí nespočet možností k zobrazení dat uživateli. V předchozích odstavcích jsem pak popsal jednotlivé možnosti, které nám DataGrid nabízí a takto vypadá konečný výsledek

datagrid-groupsort

a pohled na ta samá data, tentokrát však vyfiltrovaná dle uvedené podmínky

datagrid-groupfilter

Závěrem

DataGrid je poměrně robustní zobrazovací prvek, který nabízí různé možnosti zobrazení dat uživateli. Pokud tak chcete nabídnout základní a přesto sofistikovaný pohled na data, určitě tohoto prvku využijete. Nutno ovšem podotknout, že DataGrid nepatří mezi rychlíky v zobrazování dat a tak při větším množství zobrazovaných dat a pak obzvláště v kombinaci s grupováním a řazením je třeba počítat s tím, že bychom měli uživateli nabídnout jen nutné minimum informací, případně použít některý z gridů, které nabízejí společnosti třetích stran a které můžeme jak zakoupit tak volně používat.

Určitě by pak mohlo být zajímavé srovnání nejen výkonnosti, ale také funkčnosti těchto gridů, byl by o takové srovnání zájem?

Komentáře

ukládám komentář, vyčkejte prosím..
  1. Pekny prehled. Zajem o srovnani urcite je ;-)

    07.09.2010 @ 19:18
  2. David

    Diky za clanek, zajem o srovnani tez je :)

    08.09.2010 @ 06:52
  3. Spišo

    Porovnanie možností a rýchlostí rôznych komerčných gridov by bolo veľmi zaujímavé. Mňa hlavne zaujíma, ako v týchto testoch obstojí dxGrid od DevExpressu. Díky

    28.10.2010 @ 12:29

@xamlcz

  • RT @jvanrhyn: XAML, It's a bit like olives. Takes a while to get used to. But once you're used to it. It is actually pretty good. <3 XAML
  • RT @moser_christian: WPF Inspector 0.9.7 is released. It supports .NET 3.5 and 4.0 The project is now open source and available on CodeP ...
  • Jeff Handley oznámil vydání WCF RIA Services v.1.0 SP1 RTM http://bit.ly/gOgckn ke stažení na http://bit.ly/gVAXdK
  • jedna výzva pro Brno. Byl někdo z vás na přednášce o RIA v MS Akvárku? Dejte o sobě vědět. Děkuji
  • také jste uvažovali o tom, že zkusíte na projekt použít Caliburn Micro nebo naopak Prism 4? A co tak obojí, šlo by to nebo ne? Již brzy