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):
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ž:
- 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);
- 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; }
- 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; } } } }
- 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; } }
- 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.
- 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.