Představme si situaci, kdy pomocí Repeateru vypisujeme určitá data, přičemž tato data chceme podle určitého kritéria seskupit, např. tedy chceme seznam zakázek seskupovat podle měsíce vytvoření, tj. mít v místě přelomu měsíce určitou vloženou položku s předělovou informací:
V zásadě toho lze pro určité scénáře dosáhnout poměrně snadno, i když ne zcela čistě. Níže uvedený postup berte spíše jako jednu z nejjednodušších omezených možností a inspiraci pro tvorbu složitějšího controlu stejné funkčnosti. Omezení viz níže!
Každá položka standardního Repeateru se při vytvoření dostává do jeho kolekce Items, přičemž je vyvolána událost ItemCreated. Stáčí nám tedy, pokud si budeme v obsluze této události ukládat data příslušného řádku a porovnávat je s daty řádku předchozího. Pokud se data ve vlastnosti, podle které chceme groupovat, liší, pak do kolekce Items repeateru přidáme novou položku – nadpis příslušného seskupení.
GroupingRepeater
Celý výše uvedený postup lze poměrně elegantně ztvárnit do controlu GroupingRepeater, který bude pro groupovací řádek používat šablonu <GroupingTemplate> a pro rozlišení datových položek k seskupení IComparer (pokud jsou položky shodné, jdeme dál, pokud se liší, vkládáme seskupovací řádek).
GroupingRepeater.cs
using System; using System.Web.UI; using System.Web.UI.WebControls; using System.Collections; using System.ComponentModel; namespace MyNamespace { public class GroupingRepeater : System.Web.UI.WebControls.Repeater { private static object lastValue = null; public IComparer Comparer { get { return _comparer; } set { _comparer = value; } } private IComparer _comparer = null; [TemplateContainer(typeof(GroupHeader))] public ITemplate GroupTemplate { get { return _groupTemplate; } set {_groupTemplate = value; } } private ITemplate _groupTemplate = null; protected override void CreateChildControls() { lastValue = null; // na začátku je předchozí hodnota null base.CreateChildControls (); } protected override void OnItemCreated(RepeaterItemEventArgs e) { if(e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem) { if(e.Item.DataItem != null) { if(_comparer.Compare(lastValue, e.Item.DataItem) != 0) { // Comparer označil položky jako různé, přidáme groupovací položku GroupHeader item = new GroupHeader(); _groupTemplate.InstantiateIn(item); item.DataItem = e.Item.DataItem; this.Controls.Add(item); item.DataBind(); } } lastvalue = e.Item.DataItem; } base.OnItemCrated(e); } public class GroupHeader : Control, INamingContainer { private object _dataItem; public virtual object DataItem { get { return _dataItem; } set { _dataItem = value; } } } } }
Demo.aspx
<my:GroupingRepeater id="MyGroupingRepeater" EnableViewState="false" runat="server"> <GroupTemplate> <h2><%# DataBinder.Eval(Container.DataItem, "Datum", "{0:y}")) %></h2> </GroupTemplate> <ItemTemplate> - <%# DataBinder.Eval(Container.DataItem, "Datum") %><br/> </ItemTemplate> </my:GroupingRepeater>
Demo.aspx.cs
... private void Page_Init(object sender, System.EventArgs e) { DataSet ds = MyDataAccess.LoadData(...); MyGroupingRepeater.DataSource = ds.Tables[0]; MyGroupingRepeater.Comparer = new MyComparer(); MyGroupingRepeater.DataBind(); } ... private class MyComparer : System.Collections.IComparer { public int Compare(object x, object y) { if (x == null || y == null) return -1; DataRowView row1 = x as DataRowView; DataRowView row2 = y as DataRowView; DateTime date1 = Convert.ToDateTime(row1.Row[0]); DateTime date2 = Convert.ToDateTime(row2.Row[0]); if ((datum1.Year == datum2.Year) && (datum1.Month == datum2.Month)) return 0; else return -1; } }
Shrnutí
- Celé je to o porovnávání příslušné hodnoty ve dvou po sobě následujících řádcích, k čemuž můžeme využít IComparer.
- Toto porovnávání děláme po vytvoření každého řádku, v události ItemCreated. Událost ItemCrated repeateru je volána před vložením právě vytvořené položky do kolekce Controls, proto můžeme snadno vložit náš groupovací nadpis před tuto položku pouhým Controls.Add(groupHeader);
- Výše uvedený postup není úplně 100% čistý a je určen pouze pro jednoduché scénáře. Zasahujeme totiž do control-tree repeateru v průběhu jeho výstavby, a spoléháme se na vlastnost item.DataItem, která je naplněna pouze po data-bindingu. Nemůžeme tak využít ViewState repeateru, ale musíme ho plnit daty znovu při každém requestu – a to pokud možno dokonce už ve fázi Init (čím později budeme plnit, tím více problémů nás může potkat – může se nám totiž lišit control-tree jednotlivých requestů).
- Pokud bychom chtěli vytvořit opravdový univerzální korektní repeater s groupováním a funkčním ViewState, pak bychom potřebovali lépe pracovat s control-tree vzhledem k control-lifecycle. Nové řádky bychom museli zařazovat do kolekce Items, nikoliv přímo Controls, atp. Samotný Repeater není pro odvození takového controlu úplně ideální a lepší bude vytvořit úplně nový control. Pro nasměrování viz též článek Vlastní primitivní Repeater – templated data-bound control.
- Obdobné principy bychom mohli použít i pro groupování jiných seznamových controlů – DataGrid, GridView, CheckBoxList, RadioButtonList, DataList, atp.