Tag Archives: Control Development

Detailní ASP.NET Request + WebForm Page LifeCycle diagram

Pokud se zabýváte technologií ASP.NET do hloubky, může se Vám hodit můj „ASP.NET 2.0/3.5 Request + Page LifeCycle Diagram“:

ASP.NET LifeCycle 2

Jedná se o první verzi mého diagramu, který hodlám dále graficky vylepšovat a zpřehledňovat. Až mi zbyde chvilka, dám sem i PDF verzi k tisku. Stejnětak je možné, že jsem v něm někde udělal chybu. Pokud tedy nějakou nesrovnalost objevíte, dejte mi prosím vědět.

Červeně jsou označena místa, kde se lze zapojit s vlastní invencí, jde buď o události, virtuální metody, nebo adapter. Modře jsou vyznačeny interface pro implementaci dané funkčnosti a šedě jsou interní implementace ASP.NET.

Viz též

ViewState vs. fáze Init, aneb jak jsem se chytil

Na svém posledním ASP.NET kurzu jsem se báječně chytil na jednom primitivním demu na fungování ViewState. Krásně jsem předváděl, jak „ve fázi Init není ještě ViewState trackován, po fázi Init nastává LoadViewState a před tím se zapne jeho tracking – TrackViewState()“, nu což, vyrobil jsem si krásné demo a nestíhal jsem pak chvíli zírat.

(Základy fungování ViewState viz článek Co by měl každý vědět o ViewState.)

„Nefunkční“ demo

Vyrobil jsem primitivní stránku, s jedním Labelem a jedním tlačítkem.

<asp:Label ID="MyLabel" Text="Výchozí text" runat="server" />
<asp:Button ID="MyButton" Text="Postback" runat="server" />

a jak jsem se demonstrovat, jak přiřazení vlastnosti Text ve fázi Init nepřežije postback:

void Page_Init(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        MyLabel.Text = "Nová hodnota z fáze Init!";
    }
}

Uff, jaké bylo mé překvapení, když Label i po kliknutí na tlačítko Postback přežíval s hodnotou „Nová hodnota z fáze Init!“. Mnohým je hned jasné, kde jsem udělal chybu, ostatním přiblížím:

ViewState v životním cyklu stránek/controlů

Mé překvapení naštěstí netrvalo dlouho, rychle jsem si ověřil, kde přesně se volá v životním cyklu controlů TrackViewState() a rozuzlení bylo na světě:

  1. TrackViewState() není první operace po fázi Init, nýbrž poslední operace fáze Init samotné,
  2. fáze Init (jako jediná ze základních fází životního cyklu) probíhá zdola nahoru, procházením stromu control-tree do hloubky – fáze Init nadřazeného controlu je završena až po dokončení fází Init všech jeho child-controls.

Výsledek je tedy nasnadě, v obsluze události Page_Init jsme úplně na vrcholu control-tree a fáze Init jednotlivých controlů již proběhla. Jejich TrackViewState() již proběhl a ViewState controlů tak již sleduje změny jednotlivých hodnot. Nastavení MyLabel.Text ve fází Page_Init tedy již je operací, kdy změny ViewState labelu jsou sledovány, narozdíl od ViewState stránky (Page) samotné, kde ještě TrackViewState() neproběhl.

„Funkční“ demo

Demo jsem rychle opravil a vše krásně fungovalo, jak má:

void MyLabel_Init(object sender, EventArgs e)
{
    if (!Page.IsPostback)
    {
        this.Text = "Text nastavený ve fázi Init labelu, nepřežije Postback.";
    }
}

void Page_Init(object sender, EventArgs e)
{
    if (!IsPostback)
    {
        this.Title = "Title nastavený ve fázi Init page, nepřežije Postback.";
    }
}

Ve fázi Init samotného Labelu ještě jeho TrackViewState() neproběhl, tedy se Text nezachová.

Ve fázi Init stránky už proběhl TrackViewState() controlů, ale stránky samotné ještě ne, tedy třeba Title se nezachová.

InsertingGridView – grid s řádkem pro přidávání nových položek

Motivace

GridView je šikovný control ASP.NET 2.0, který řeší spoustu nedostatků a neduhů starého dobrého DataGridu. Umí toho hodně, jednu věc však stále neumí – přidávání nových položek (INSERT):

image

Tudy ne

Na internetu je spousta pokusů o implementaci insertingu do GridView, nicméně drtivá většina z nich se omezuje jen na více či méně intenzivní znásilnění řádku Footer a umístění insertingových controlů do něj. Pokud pomineme hlavní nedostatek, že tím vznikne prapodivný hybrid, který má editaci v řádku typu DataControlRowType.Footer, namísto DataControlRowType.DataRow, a že nemůžeme mít Grid s footerem, pak i samotné použití těchto insert-gridů je zoufalé – obsah šablony EditTemplate je tupě kopírován do FooterTemplate, atp. Takovéto násilné řešení se mi nelíbí, tudy ne:

Tudy ano

Na internetu je vidět i několik málo nedotažených implementací, které přistupují k nové položce trochu jiným způsobem. Další možností vytvoření insertovacího řádku je totiž rozšíření datové sady zpracovávané pomocí GridView o „prázdnou“ položku a její editace obdobně jako každého jiného řádku.

Tento způsob se mi stal inspirací pro napsání vlastního InsertingGridView, které je samostatným uceleným controlem, potomkem GridView s možností přidávání položek insertovacím řádkem.

Základní princip

Můj InsertingGridView funguje tak, že si pomocí delegáta GetInsertRowDataItem pro insert-řádek vyžádá položku (prázdnou nebo předvyplněnou, objekt, DataRow, nebo cokoliv stejného typu jako ostatní položky gridu), kterou si během data-bindingu vloží na správné místo zpracovávané datové množiny a pracuje s ní v režimu DataControlRowType.DataRow a DataControlRowState.Insert.

Použití

Mým základním pravidlem pro programování reusable záležitostí je maximální důraz na vnější rozhraní a příjemný způsob použití, vnitřní implementace ač by měla být „hezká“, je až druhotnou záležitostí. Začněme tedy tím, čeho chceme dosáhnout, jak má použití takového InsertingGridView vypadat.

Samotnému controlu jsem přidal property AllowInserting=“true|false“, který funkčnost přidávání povoluje a property InsertRowPosition=“Top|Bottom“, která určuje, zda-li má být insertovací řádek v gridu nahoře nebo dole. Control pak v ASPX stránce může vypadat třeba takto:

<havit:InsertingGridView
    ID="MyGridView"
    AllowInserting="true"
    InsertRowPosition="Bottom"
    AutoGenerateColumns="false"
    runat="server"
>
    <Columns>
        <havit:GridViewCommandField
            ShowEditButton="true"
            ShowDeleteButton="true"
            ShowInsertButton="true"
            ValidationGroup="grid"
            CausesValidation="true"
        />
        <asp:BoundField HeaderText="Sloupec bez editace" DataField="Nazev" ReadOnly="true" />
        <asp:TemplateField HeaderText="Sloupec s editací">
            <ItemTemplate>
                <%# ((Objednavka)Container.DataItem).Cislo %>
            </ItemTemplate>
            <EditItemTemplate>
                <asp:TextBox ID="CisloTB" Text="<%# ((Objednavka)Container.DataItem).Cislo %>" runat="server" />
                <asp:RequiredFieldValidator ControlToValidate="CisloTB" ErrorMessage="xxx" ValidationGroup="grid" runat="server" />
            </EditItemTemplate>
        </asp:TemplateField>
    </Columns>
</havit:InsertingGridView>

…oproti standardnímu GridView jsem opravdu přidal jen property AllowInserting a InsertRowPosition, vlastního GridViewCommandField si zatím nevšímejte, zajišťuje jen zobrazení správných příkazů dle stavu řádku a dostaneme se k němu později.

Dále už jsem přidal jen dvě události klasického vzoru – RowInserting a RowInserted, prakticky stejného významu a funkčnosti jako RowUpdating a RowUpdated. Teoreticky bude i jejich obsluha do značné míry stejná a pokud si budete chtít zjednodušit život, můžete se ve své implementaci pro začátek bez nich i obejít.

Poslední, co InsertingGridView pro svou funkčnost potřebuje, je mít nějaký způsob získávání datové položky pro insert-řádek. Prostě mít nějaký způsob, jak získat prázdný/předvyplněný objekt typu Objednavka, pokud grid zobrazuje objednávky, nebo typu DataRowView, pokud pracuje s „neobjektovými“ daty z databáze. Prostě novou položku typu stejného jako jsou ostatní položky v datové sadě bindované na grid. Jako způsob získávání této položky jsem zvolil delegáta vtěleného v propertyGetInsertRowDataItem a příklad kódu stránky tak může vypadat takto:

protected override OnInit(EventArgs e)
{
  MyGridView.GetInsertRowDataItem += MyGridView_GetInsertRowDataItem;
  MyGridView.RowInserting += new new GridViewInsertEventHandler(MyGridView_RowInserting);
  MyGridView.RowUpdating += ...
  MyGridView.RowDeleting += ...
}

private object MyGridView_GetInsertRowDataItem()
{
  Objednavka obj = new Objednavka();
  obj.Cislo = "předvyplněná hodnota nového řádku";
  return obj;
}

private void MyGridView_RowInserting(object sender, GridViewInsertEventArgs e)
{
  GridViewRow row = MyGridView.Rows[e.RowIndex];
  TextBox cisloTB = row.FindControl("CisloTB") as TextBox;
  ...
}

…toť vše.

Pro použití insertingu tedy v podstatě musím jen nastavit AllowInserting=“true“, nastavit delegáta GetInsertRowDataItem vracejícího hodnotu pro nový řádek a obsloužit událost RowInserting.

Na tomto místě je důležité zdůraznit, že můj InsertingGridView nepodporuje deklaratorní data-binding pomocí DataSourceID, protože tuto metodiku obecně považuji za zhovadilost a pro produkční projekty prakticky nepoužitelnou. Plnou podporu DataSourceID by nebyl problém do InsertingGridView implementovat, ostatně je to celé jen o Copy&Paste z Reflectoru.

Základní schéma implementace controlu

Control InsertingGridView je implementován jako potomek klasického GridView, přičemž:

  1. V metodě override void PerformDataBinding(IEnumerable data), která zajišťuje data-binding, se na správné místo výchozí datové sady vloží datová položka pro nový řádek (její pozici si uložíme do property InsertRowDataSourceIndex) a zavolá se s touto rozšířenou datovou sadou base.PerformDataBinding(extendedData), který samotný data-binding provede.
    Správné místo pro vložení datové položky nového řádku musíme určit na základě property InsertingRowPosition a v případě povoleného stránkování i na základě čísla stránky. V případě stránkování musíme mimo datové položky pro nový řádek vkládat do datové sady i další dummy-položky, na začátek sady jednu pro každou stránku před aktuální stránkou a na konec sady jednu pro každou stránku za aktuální stránkou. To vše proto, aby nám běžné položky po stránkách neposkakovaly podle toho, jestli zrovna editujeme nebo insertujeme a abychom měli stále stejný počet stránek.
    protected override void PerformDataBinding(IEnumerable data)
    {
        if (AllowInserting)
        {
            if (GetInsertRowDataItem == null)
            {
                throw new InvalidOperationException("Při AllowInserting musíte nastavit GetInsertRowData");
            }
            ArrayList newData = new ArrayList();
    
            object insertRowDataItem = GetInsertRowDataItem();
            foreach (object item in data)
            {
                newData.Add(item);
            }
            if (AllowPaging)
            {
                int pageCount = (newData.Count + this.PageSize) - 1;
                if (pageCount < 0)
                {
                    pageCount = 1;
                }
                pageCount = pageCount / this.PageSize;
    
                for (int i = 0; i < this.PageIndex; i++)
                {
                    newData.Insert(0, insertRowDataItem);
                }
                for (int i = this.PageIndex + 1; i < pageCount; i++)
                {
                    newData.Add(insertRowDataItem);
                }
            }
            if (EditIndex < 0)
            {
                switch (InsertRowPosition)
                {
                    case GridViewInsertRowPosition.Top:
                        this.InsertRowDataSourceIndex = (this.PageSize * this.PageIndex);
                        break;
                    case GridViewInsertRowPosition.Bottom:
                        if (AllowPaging)
                        {
                            this.InsertRowDataSourceIndex = Math.Min((((this.PageIndex + 1) * this.PageSize) - 1), newData.Count);
                        }
                        else
                        {
                            this.InsertRowDataSourceIndex = newData.Count;
                        }
                        break;
                }
                newData.Insert(InsertRowDataSourceIndex, insertRowDataItem);
            }
            data = newData;
        }
        base.PerformDataBinding(data);
    
  2. V metodě override GridViewRow CreateRow(…), která zajišťuje vytvoření nového controlu GridViewRow představujícího jeden řádek gridu, zajistíme, aby se při vytváření řádku pro insertovací položku nastavit správný stav tohoto řádku na DataControlRowState.Insert – což zajistí nejenom použití edit-režimu pro řádek, ale i správné chování CommandFieldů, atp.
    protected override GridViewRow CreateRow(int rowIndex, int dataSourceIndex, DataControlRowType rowType, DataControlRowState rowState)
    {
            GridViewRow row = base.CreateRow(rowIndex, dataSourceIndex, rowType, rowState);
            // Řádek s novým objektem přepínáme do stavu Insert, což zajistí zvolení EditItemTemplate a správné chování CommandFieldu.
            if ((rowType == DataControlRowType.DataRow)
                && (AllowInserting)
                && (dataSourceIndex == InsertRowDataSourceIndex))
            {
                _insertRowIndex = rowIndex;
                row.RowState = DataControlRowState.Insert;
            }
            if ((_insertRowIndex < 0) && (rowIndex == (this.PageSize - 1)))
            {
                row.Visible = false;
            }
                return row;
    }
    
  3. Dále potřebujeme vytvořit zachytávání příkazu Insert a jeho správné zpracování s příslušným vyvoláním událostí RowInserting a RowInserted. To zajistíme v metodě override void OnRowCommand(GridViewCommandEventArgs e) a následně v nové metodě HandleInsert(…), která je klonem metody HandleUpdate(…) klasického GridView. Já používám HandleInsert() ve zjednodušené podobě bez podpory DataSourceID, pokud by po této podpoře někdo toužil, nechť si zkopíruje z Reflectoru GridView.HandleUpdate() a upraví ho na insert, logika obsluhy je totožná.
    protected override void OnRowCommand(GridViewCommandEventArgs e)
    {
        base.OnRowCommand(e);
    
        bool causesValidation = false;
        string validationGroup = String.Empty;
        if (e != null)
        {
            IButtonControl control = e.CommandSource as IButtonControl;
            if (control != null)
            {
                causesValidation = control.CausesValidation;
                validationGroup = control.ValidationGroup;
            }
        }
    
        switch (e.CommandName)
        {
            case DataControlCommands.InsertCommandName:
                this.HandleInsert(Convert.ToInt32(e.CommandArgument, CultureInfo.InvariantCulture), causesValidation);
                break;
        }
    }
    protected virtual void HandleInsert(int rowIndex, bool causesValidation)
    {
        if ((!causesValidation || (this.Page == null)) || this.Page.IsValid)
        {
            GridViewInsertEventArgs argsInserting = new GridViewInsertEventArgs(rowIndex);
            this.OnRowInserting(argsInserting);
            if (!argsInserting.Cancel)
            {
                GridViewInsertedEventArgs argsInserted = new GridViewInsertedEventArgs();
                this.OnRowInserted(argsInserted);
                if (!argsInserted.KeepInEditMode)
                {
                    this.EditIndex = -1;
                    this.InsertRowDataSourceIndex = -1;
                    base.RequiresDataBinding = true;
                }
            }
        }
    }
    
  4. Poslední, co stojí implementačně za zmínku, je úprava obsluhy události RowEditing. Potřebujeme, aby editace a inserting byly vzájemně výlučné, aby tedy při zahájení editace byl vypnut inserting a při ukončení editace naopak reaktivován:
    protected override void OnRowEditing(GridViewEditEventArgs e)
    {
        base.OnRowEditing(e);
    
        if (!e.Cancel)
        {
            this.EditIndex = e.NewEditIndex;
            if ((AllowInserting) && (this.InsertRowDataSourceIndex >= 0) && (this._insertRowIndex < e.NewEditIndex))
            {
                this.EditIndex = this.EditIndex - 1 ;
                this.RequiresDatabinding = true; 
            }
            this.InsertRowDataSourceIndex = -1;
            _insertRowIndex = -1;
        }
    }
    
  5. Ostatní části kódu jsou jen běžné implementační záležitosti. Jsou vytvořeny události RowInserting a RowInserted, k ním příslušné metody OnRowInserting a OnRowInserted. Pro události jsou vytvořeny třídy argumentů GridViewInsertEventArgs a GridViewInsertedEventArgs. Pro InsertRowPosition je enum GridViewInsertRowPostion. Potřeba je taky delegát GetInsertRowDataItemDelegate.
  6. Poslední záležitostí, která stojí za zmínku, je již zmiňovaný GridViewCommandField. Je to potomek klasického CommandFieldu. Ten sice má podporu Insertu, ale korektně se chová jen ve FormsView, nikoliv v GridView. V GridView totiž při nastavení ShowInsertButton=“true“ zobrazuje na každém ne-insertovém řádku i tlačítko „New“ a na insert-řádku tlačítko naopak „Cancel“. GridViewCommandField tedy není nic jiného, než modifikace CommandFieldu, která tyto dvě nežádoucí tlačítka nezobrazuje.
    Klasický CommandField je bohužel dost zapouzdřen a neumožňuje své chování příliš overridovat, takže GridViewCommandField a další nutné třídy DataControlButton, DataControlImageButton, DataControlLinkButton, atp., jsou jen spousty Copy&Paste z Reflectoru.

InsertingGridView Known Issues

  • Nepodporuje data-binding pomocí DataSourceID (neřeším, protože DataSourceID nesnáším).
  • Pravděpodobně selže v případě custom-pagingu dat, protože PerformDataBind() nyní předpokládá na vstupu úplnou datovou sadu všech stránek, nikoliv částečnou (Toto doladím, až to budu potřebovat, není to zas tak obvyklý scénář – ono už běžný paging+inserting v jednom gridu je v praxi neobvyklá kombinace).

Download

Ke článku jsou přiloženy úplné zdrojové kódy controlu InsertingGridView a souvisejících tříd, z ranných fází jeho vývoje. Článek i zdrojáky jsou zamýšleny  jen jako inspirace do bojů s Vaším vlastním gridem a přiložené zdrojáky nejsou přímo kompilovatelené.  InsertingGridView v nich dědí z našeho už dříve rozšířeného EnterpriseGridView, i když z jeho funkčnosti mnoho nevyužívá a pro nikoho by neměl být problém adaptace přímo na potomka GridView.

Feedback welcome

Netroufám si tvrdit, že výše uvedené řešení či dokonce jeho implementace jsou dokonalé. Proto uvítám jakékoliv náměty, které Vás napadnou při vlastní implementaci či použití…

Update 10/2013

GridView s touto funkčností do dnes velmi intenzivně používáme, jestli si však dobře uvědomuji, opravili jsme v kódu za tu dobu jeden nebo dva bugy. Pokud by Vám tedy tento kód nestačil jako inspirace a zasekli byste se na nějakém bugu, ozvěte se mi.

GroupingRepeater – Groupování dát v Repeateru

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í
  1. 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.
  2. 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);
  3. 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ů).
  4. 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.
  5. Obdobné principy bychom mohli použít i pro groupování jiných seznamových controlů – DataGrid, GridView, CheckBoxList, RadioButtonList, DataList, atp.

Dynamicky přidávané controly musí mít nastaveno ID, jinak jim blbne postback

Pokud do control-tree přidáváme dynamicky nějaké controly, které mají obsluhu postbacku (data nebo event), pak pokud těmto controlům explicitně nanastavíme nějaké ID, může se nám snadno stát, že postback nebude korektně vyhodnocen, např.:

  • Button, LinkButton, ImageButton nebudou emitovat události Click, Command, …,
  • inputové controly (TextBox, DropDownList, …) zapomenou přes roundtrip data,

…v zábavnějším případě se nám může stát, že postback není korektní jen u některých dynamický controlů a u některých se vyhodnotí správně (třeba pokud přidáváme řádky objednávky, tak nám data zapomíná jen poslední řádek).

Nevím, jestli je to bug, ale dělalo to už v .NET 1.1 a dělá to i v .NET 2.0.

Každopádně nastavením ID u dynamicky přidávaných controlů se těchto potíží zbavíme!

private void CreateControlsHiearchy()
{
   LinkButton lb = new LinkButton();
   lb.ID = "MyLB"; // <--- bez toho není jisté, že se nám bude Click korektně volat !!!
   lb.Click += ...
   ...
   Controls.Add(lb);
}

Control pro zadávání předem neznámého počtu položek

Ukažme si základní schema jednoduchého controlu, který umožní uživateli zadat data předem neurčeného počtu položek – umožní tedy uživateli dynamické přidávání dalších položek – např. řádky objednávky, několik telefonních čísel, několik e-mail e-mailů – v našem případě několik zásilek k přepravě.

 

Netvrdím, že níže uvedený postup je jediný možný a uvítám jakoukoliv diskuzi pod článkem, berte to spíše jako nasměrování pro další snažení…

Neřeším také, jak na control navázat již existující data (možná v příštím článku), a i vytahování dat z controlu není úplně user-friendly. Nicméně cílem je demonstrovat základní schéma dynamického počtu položek a vylepšení kolem už si každý jistě umí udělat.

Základní schema by se dalo shrnout:

  1. Vytvoříme si control představující jednu položku – není to sice nezbytně nutné, ale usnadní to další manipulaci – control samozřejmě může už existovat, pokud se ptáme jen na jména milenek, může nám stačit už TextBox – nicméně většinou budeme potřebovat control, který bude obsahovat nekolik prvků, validátory, DropDownListy naplněné daty, atp. V našem konkrétním případě jsem si teda připravil UserControl Zasilka.ascx, který má za předka třídu Zasilka (code-behind).
  2. Vytvoříme CompositeControl, který bude v CreateChildControls dynamicky přidávat tolik položek, kolik je právě potřeba. Aby nám fungoval postback a viewstate, musí být control-tree vystavěn při každém roundtripu znovu, a to právě ve fázi Init. V CompositeControl je připraveno schéma pomocí metody CreateChildControls(), která je volána právě už ve fázi Init. Zásadním problémem zde je, jak ve fázi Init zjistit, kolik položek máme zrovna vygenerovat, když ještě nemáme načtena post-back data, ani ViewState.
  3. Aktuální počet položek si musíme ukládat tak, abychom ho znali už ve fázi Init, například do hidden-inputu. Lze využít i Session, či jiná uložiště, vlastní hidden-input se však zde nabízí jako ideální.
  4. Přidání další položky realizujeme v event-handleru pomocí Controls.AddAt(), odebírání položky pomocí Controls.RemoveAt(). Klikne-li uživatel na tlačítko „přidat další položku“, dozvíme se o tom v event-handleru až v poměrně pokročilé fázi zpracování postbacku, když už je control-tree vytvořen (jinak by ani event nemohl být korektně obsloužen). V této fázi jsou existující položky i naplněny daty a nebylo by tedy vhodné znovu rebuildovat control-tree, protože bychom tato data ztratili, popř. museli znovu dotahovat. Protože však control-tree díky jeho pevné stavbě známe, nic nám nebrání přidat další položku do existujícího control-tree, popř. položku odebrat.
  5. Data z controlu vytahujeme například přímým přístupem do Controls, jelikož strukturu položek v Controls známe.

Kód takového controlu s dynamickým počtem položky by tedy mohl vypadat nějak následovně:

public class Zasilky : CompositeControl
 {
  // Počet zásilek si mezi postbacky posíláme v input-hidden,
  // z něj ho načítáme přímo pomocí Form, ukládáme až těsně
  // před renderem OnPreRender(), abychom měli poslední stav
  // po případném přidání/odebrání.
  public int PocetZasilek
  {
   get
   {
    if (_pocetZasilek == null)
    {
     _pocetZasilek = 1;
     object tmp = Page.Request.Form[this.ClientID + &quot;_PocetZasilek&quot;];
     if (tmp != null)
     {
      _pocetZasilek = Convert.ToInt32(tmp);
     }
    }
    return (int)_pocetZasilek;
   }
   set
   {
    _pocetZasilek = value;
   }
  }
  private int? _pocetZasilek;
  
  // abychom mohli buttony referencovat (chceme je skrývat),
  // uložíme si odkaz na ně do private fieldu
  private LinkButton pridejZasilkuLB;
  private LinkButton odeberZasilkuLB;
  
  // uloží nám počet položek do input-hidden
  protected override void OnPreRender(EventArgs e)
  {
   Page.ClientScript.RegisterHiddenField(this.ClientID + &quot;_PocetZasilek&quot;, _pocetZasilek.ToString());
   
   base.OnPreRender(e);
  }
  
  // klasické schéma CompositeControlu
  protected override void CreateChildControls()
  {
   Controls.Clear();
   CreateControlHiearchy();
   ClearChildViewState();
  }
  
  // budujeme control-tree
  private void CreateControlHiearchy()
  {
   // položky - jsou naschvál na začátku, což nám usnadňuje jejich snadné referencování pomocí Controls[i],
   // jinak bychom museli přístup k nim mít složitější
   for (int i = 0; i &lt; PocetZasilek; i++)
   {
    CreateZasilkaControl(Controls.Count);
   }
  
   Literal lit1 = new Literal();
   lit1.Text = &quot;&lt;tr&gt;&lt;td colspan=\&quot;3\&quot; class=\&quot;zindent\&quot;&gt;&quot;;
   Controls.Add(lit1);
  
   // tlačítko pro přidání položky
   pridejZasilkuLB = new LinkButton();
   pridejZasilkuLB.ID = &quot;PridejZasilkuLB&quot;; // &lt;-- nutno nastavit ID, jinak nám můžou blbnout postbacky
   pridejZasilkuLB.Text = (string)HttpContext.GetGlobalResourceObject(&quot;Zasilky&quot;, &quot;PridejZasilku&quot;);
   pridejZasilkuLB.CssClass = &quot;arrow&quot;;
   pridejZasilkuLB.Click += new EventHandler(pridejZasilkuLB_Click);
   pridejZasilkuLB.CausesValidation = false;
   Controls.Add(pridejZasilkuLB);
  
   Literal lit3 = new Literal();
   lit3.Text = &quot;&lt;br/&gt;\n&quot;;
   Controls.Add(lit3);
  
   // tlačítko pro odebrání položky
   odeberZasilkuLB = new LinkButton();
   odeberZasilkuLB.ID = &quot;OdeberZasilkuLB&quot;; // &lt;-- nutné nastavit ID, jinak nám můžou blbnout postbacky
   odeberZasilkuLB.Text = (string)HttpContext.GetGlobalResourceObject(&quot;Zasilky&quot;, &quot;OdeberPosledniZasilku&quot;);
   odeberZasilkuLB.CssClass = &quot;arrow&quot;;
   odeberZasilkuLB.Click += new EventHandler(odeberZasilkuLB_Click);
   odeberZasilkuLB.CausesValidation = false;
   odeberZasilkuLB.Visible = (PocetZasilek &gt; 1);
   Controls.Add(odeberZasilkuLB);
  
   Literal lit2 = new Literal();
   lit2.Text = &quot;&lt;/td&gt;&lt;/tr&gt;\n&quot;;
   Controls.Add(lit2);
  }
  
  // vytvoření jedné položky v control-tree
  private void CreateZasilkaControl(int index)
  {
   Zasilka zasilkaControl = (Zasilka)Page.LoadControl(&quot;~/Controls/Zasilka.ascx&quot;);
   zasilkaControl.ID = &quot;ZasilkaUC_&quot; + index.ToString(); // &lt;-- nutné nastavit ID kvůli korektním postback
   Controls.AddAt(index, zasilkaControl);
  }
  
  // obsluha události - přidání položky
  private void pridejZasilkuLB_Click(object sender, EventArgs e)
  {
   PocetZasilek++;
   CreateZasilkaControl(PocetZasilek - 1); // přidáme za poslední položku
   odeberZasilkuLB.Visible = true;
  }
  
  private void odeberZasilkuLB_Click(object sender, EventArgs e)
  {
   if (PocetZasilek &gt; 1)
   {
    PocetZasilek--;
    Controls.RemoveAt(PocetZasilek); // odebereme poslední položku
    if (PocetZasilek == 1)
    {
     odeberZasilkuLB.Visible = false;
    }
   }
  }
 }

Vytahování dat z controlu by pak v nejjednoduším případě mohlo vypadat nějak takto:

for (int i=0; i &lt; Zasilky.PocetZasilek; i++)
{
   Zasilka zasilka = (Zasilka)Zasilky.Controls[i];
   uloziste[i] = zasilka.Hmotnost;
}
  
// kdyby byl položkou jen TextBox
for (int i=0; i &lt; Zasilky.PocetZasilek; i++)
{
   TextBox polozka = (TextBox)MyControl.Controls[i];
   uloziste[i] = polozka.Text;
}

Attachment: screenshot.gif

Zobrazení controlu jen v určité položce Repeateru

Je poměrně snadný způsob, jak alespoň částečně obměňovat obsah položek repeateru. Chceme-li například zobrazit control jen v první položce (mezi první a druhou položkou), pak  takto:

<asp:Repeater ID="ZpravickyRepeater" Runat="server">
   <ItemTemplate>
      ...
      <xy:CokolivTrebaPlaceHolder Visible="<%# (Container.ItemIndex == 0) %>" Runat="server"/>
   </ItemTemplate>
</asp:Repeater>

Vlastní primitivní Repeater – templated data-bound control

Primitivní Repeater

Začněme velmi jednoduchým příkladem, primitivní napodobeninou Repeateru, na níž si ukážeme základy vytváření složených datově-vázaných controlů (composite data-bound controls) a použití šablon v controlech (templates).

Dejme tomu chceme vytvořit control, který bude podle šablon vypisovat seznam zákazníků. Oproti standardnímu Repeateru z ASP.NET však bude pracovat se strong-typed prvky a přidáme si jednoduchou podporu pro načítání šablon ze souborů. Spokojíme se prozatím bez podpory ViewState (data bude tedy nutno bindovat v každém roundtripu) i bez eventů (IremCreated, ItemDataBound, Command, …).

<havit:MyRepeater ID="cosi" runat="server">
      <ItemTemplate>
         Jméno: <%# Container.Zakaznik.Jmeno %><br/>
         Datum narození: <%# Container.Zakaznik.DatumNarozeni.ToShortDateString() %></br>
      </ItemTemplate>
</havit:MyRepeater>

Základy práce se šablonami (templates)

Princip šablon je velmi jednoduchý. Když si uvědomíme, že controly tvoří hiearchii (kořenem je Page a podřízené controly jsou vždy v kolekci Controls), pak použití šablony není nic jiného než připojení nové větve controlů do této hiearchie.

V controlu se šablona samotná vytvoří pomocí property typu ITemplate.

[PersistenceMode(PersistenceMode.InnerPropety)]
[TemplateContainer(typeof(MyRepeaterItem))]
public ITemplate ItemTemplate
{
      get { return _itemTemplate; }
      set { _itemTemplate = value; }
}

Atribut PersistenceMode nastavený na InnerPropety nám říká, že ItemTemplate bude ve zdrojovém kódu controlu zapsán jako vnořený tag. Atribut TemplateContainerzase parseru říká, jakého typu je objekt Container, který budeme používat uvnitř template.

Pro začátek je to vše, co potřebujeme pro definici šablony, parser se sám postará o naplnění property ItemTemplate, pokud ji v .aspx stránce použijeme.

Rozhraní ITemplate definuje jedinou metodu InstantiateIn(Control container), která nedělá nic jednoduššího, než že připojí control-hiearchy tvořené šablonou jako větev do containeru (toho containeru, jehož typ jsme udali v atributu TemplateContainer).

Nutno zdůraznit, že container, do kterého template připojujeme, musí implementovat rozhraní INamingContainer, aby nám korektně fungoval objekt Container v kódu šablony.

Základy vytváření data-bound controls

Pro vytváření data-bound control je v ASP.NET 2.0 připraven pattern v podobě abstraktní třídy CompositeDataBoundControl. Composite proto, že nové controly vytváříme skládáním jiných controlů do hiearchie, nikoliv renderováním HTML kódu controlů úplně nových.

Základní implementace se v podstatě celá odehrává v metodě CreateChildControls(IEnumerable dataSource, bool dataBinding), přičemž základní pattern chování by měl být zhruba následující

override int CreateChildControls(IEnumerable dataSource, bool dataBiding)
{
      if (dataBinding)
      {
            // vytahej z dataSource data a ulož si je ve své podobě, obvykle do this.Items
            // this.Items bychom si měli ukládat do ViewState, drží nám data mezi postbacky
      }



      // vytvoř controly pro každou položku this.Items a přidej ji do this.Controls
      // foreach (MyItem item in Items) { ... Controls.Add(..); }
      // return počet vytvořených items
}

Pokud nemáme ViewState, tak lze samozřejmě obě fáze sloučit a rovnou tvořit control-hiearchy z dat v dataSource.

MyRepeater – kombinace CompositeDataBoundControl a ITemplate

No a není nic jednoduššího, než obě možnosti zkombinovat a vytvořit tak control, který bude data-bound a pro reprezentaci dat bude používat šablony:

public class MyRepeater : CompositeDataBoundControl
{
      [PersistenceMode(PersistenceMode.InnerPropety)]
      [TemplateContainer(typeof(MyRepeaterItem))]
      public ITemplate ItemTemplate
      {
            get { return _itemTemplate; }
            set { _itemTemplate = value; }
      }
      private ITemplate _itemTemplate;

      /// <summary>
      /// Soubor .ascx, který představuje případnou externí ItemTemplate.
      /// </summary>
      public string ItemTemplateFile
      {
            get { return _itemTemplateFile; }
            set { _itemTemplateFile = value; }
      }
      private string _itemTemplateFile;
      public Collection<MyRepeaterItem> Items
      {
            get
            {
                  if (_items == null)
                  {
                        _items = new Collection<MyRepeaterItem>();
                  }
                  return _items;
            }
      }
      private Collection<MyRepeaterItem> _items;
      protected override CreateChildControls(IEnumerable dataSource, bool dataBinding)
      {
            // vytaháme data z dataSource
            if (dataBinding)
            {
                  if (dataSource != null)
                  {
                        foreach (object o in dataSource)
                        {
                              if (!(o is Zakaznik))
                              {
                                    throw new ApplicationException("DataSource musí obsahovat prvky typu Zakaznik.");
                              }
                              MyRepeaterItem item = new MyRepeaterItem(o as Zakaznik);
                              Items.Add(item);
                        }
                  }
            }
            // vytvoříme controly
            foreach (MyRepeaterItem in Items)
            {
                  if (!String.IsNullOrEmpty(ItemTemplateFile))
                  {
                        _itemTemplate = Page.LoadTemplate(ItemTemplateFile);
                  }
                  _itemTemplate.InstantiateIn(item);
                  item.DataBind();
                  Controls.Add(item);
            }
            return Items.Count;
      }
}

Kód ukazuje i druhou možnost získávání šablon – jejich načítání z .ASCX souboru pomocí Page.LoadTemplate(). Šablona totiž může úplně stejně, jako ve vlastní .aspx stránce, být uložena v samostatném .ASCX souboru (jen pro Container je tam potřeba použít explicitní přetypování, protože tam parser neví o atributu TemplateContainer).

Pro úplnost ještě MyRepeaterItem:

public class MyRepeaterItem : Control, INamingContainer
{
      public Zakaznik Zakaznik
      {
            get { return _zakaznik; }
            set { _zakaznik = value; }
      }
      public MyRepeaterItem(Zakaznik zakaznik)
      {
            this._zakaznik = zakaznik;
      }
}
Odkazy na související články