Category Archives: Development

Programátorská hádanka – Výjimka ve výjimkách

Jaký je rozdíl v zachytávání výjimek při použití typu výjimky Exception

try
{
...
}
catch (Exception e)
{
...
}

a bez použití tohoto typu, tedy

try
{
...
}
catch
{
...
}

Zdůrazňuji, že tento rozdíl existuje jen v .NET Frameworku 1.x, ve verzi 2.0 jsou způsoby funkčně rovnocenné.

Odpověď najdete o řádku níže napsanou bílým písmem (pro odtajnění třeba označte):

>>> Konstrukce catch (Exception e) zachytává jen CLS-compliant výjimky, catch zachytává všechny chyby. Praktický rozdíl je při zachytávání chyb z COM objektů, jejichž chyby nejsou CLS-compliant výjimkami. .NET Framework 2.0 tyto chyby z COM objektů zabalí do RuntimeWrappedException, které jsou CLS-compliant, takže je chyba zachycena i při použití catch (Exception e).  <<<

…a jako posledně: teď se přiznejte, kdo jste to znal!

Lokalizace skinů (Themes)

Nevím jak vás, ale mně už delší dobu rozčiluje, že skiny nejsou lokalizovatelné. Pánové z Microsoftu na to buď zapomněli, nebo nevím, každopádně se tak skiny staly do značné míry nepoužitelnými pro lokalizované aplikace.

Štvalo mě to tak dlouho, až jsem napsal vlastní lokalizaci skinů. Není to nic překrásného, protože jen máloco je v ASP.NET tak zapouzdřeno jako témata a skiny a tak se kód hemží reflexí – nicméně to výborně funguje k tomu účelu, na který to sám potřebuji – do lokalizovaných webových aplikací s běžným provozem bez masové zátěže.

Optimalizace opět ponechám na každém z vás, stejnětak případný příhodnější způsob integrace do ASP.NET. Níže popisuji jednoduché řešení, jehož účelem je pochopení použitého způsobu lokalizace skinů. Taktéž předesílám, že se jedná o explicitní metodu lokalizace, protože implicitní lokalizaci nepovažuji za obecně vhodnou.

Jak lokalizace skinů funguje

První, co každý asi zkusí, je použít ve skinu běžnou expression <%$ Resources: MyResources, MyValueName %>. ASP.NET Vás odmění krásnou chybovou hláčkou parseru: „Expressions are not allowed in skin files.“ Tudy cesta nevede…

Inspiroval jsem se tedy v syntaxi lokalizace, kterou používá XmlSiteMapProvider pro web.sitemap soubory, tedy např.:

&lt;siteMapNode url=&quot;~/default.aspx&quot; title=&quot;$resources:MyResources,MyResourceKey,My default value&quot; /&gt;

a řekl jsem si, že stejnou syntaxi bych mohl použít i pro lokalizaci skinů:

&lt;asp:Button Text=&quot;$resources: MyResource, MyValue, Default Value&quot; runat=&quot;server&quot; /&gt;
&lt;asp:Button SkinID=&quot;SkinWithDefault&quot; Text=&quot;$resources: MyResource, InvalidResource, Default Value&quot; runat=&quot;server&quot; /&gt;

…samozřejmě se tím odepře možnost použití textů začínajících znaky „$resource:“, ale to mi nevadí.

Teď ještě vymyslet, kde a jak v životním cyklu stránky zajistit odchytávání a vyhodnocování těchto lokalizačních výrazů. Po chvilce bádání s Reflectorem lze dospět k následujícím poznatkům:

  1. téma stránky je uložena v private fieldu Page._theme typu PageTheme
  2. stylesheet-téma stránky je uloženo v private fieldu Page._styleSheet typu PageTheme
  3. témata se inicializují (InitializeThemes) po ve fází PreInit životního cyklu stránky, avšak až po události PreInit (v obsluze události PreInit tedy zřejmě není možné témata modifikovat),
  4. témata se aplikují ve fázi Init životního cyklu stránky, avšak před událostí Init (v obsluze události Init je tedy pozdě je modifikovat),
  5. třída PageTheme má metodu ApplyControlSkin(Control control), která najde ve slovníku skinů skin (typ ControlSkin) příslušející danému controlu a aplikuje ho pomocí skin.ApplySkin(Control control)
  6. metoda ControlSkin.ApplySkin(Control control) nedělá nic jiného, než že zavolá vygenerovaného delegáta.
public void ApplySkin(Control control)
{
    this._controlSkinDelegate(control);
}

Zdánlivě nikde není rozumný prostor pro modifikaci skinu, nikde není seznam hodnot skinu, žádná property-value kolekce, kde by se dal resource-odkaz najít a modifikovat. Vygenerovaný delegát prostě natvrdo nastavuje vlastnosti příslušného controlu a hotovo.

…zde však přichází to kouzlo! Můžeme totiž delegáta, který je uložen v private fieldu _controlSkinDelegate vyměnit za delegáta vlastního. A to nejenomže ho můžeme vyměnit, ale můžeme si dokonce odkaz na původního delegáta uchovat a tuto původní metodu z našeho delegáta zavolat. Celý můj fígl tedy spočívá ve výměně tohoto delegáta, za jinou metodu, která však v sobě nejprve volá metodu původní (aplikuje skin) a následně prochází skinovatelné property controlu (pro jednoduchost pouze textové) a hledá v nich lokalizující resource-odkaz, který případně vyhodnotí. Abychom si měli původního delegáta jak uchovat, je nová metoda wrapována do instance třídy SkinLocalizationDelegateWrapper.

Poslední, co zbývá vyřešit, je jak se s vlastní obsluhou dostat do správného místa životního cyklu stránky, tedy za InitializeTheme(), které je po události PreInit a před ApplySkin(), které je na začátku Init fáze, před událostí Init. Nehledal jsem dlouho, a možná najdete vhodnější místo, ale já jsem pro tyto účely znásilnil virtuální metodu Control.ResolveAdapter(), která se volá na začátku InitRecursive(), tedy přesně v místě, kde se nám to hodí.

SkinLocalizationPageBase

Vytvořil jsem tedy nakonec třídu SkinLocalizationPageBase, která je potomkem System.Web.UI.Page, a která by měla být předkem všech stránek webu, kde lokalizované skiny používáme (nejlépe tedy úplně všech, pomocí <pages pageBaseType=“SkinLocalizationPageBase“ /> ve web.configu.

Výsledný kód vypadá takto:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Web.UI.Adapters;
using System.Reflection;
using System.Collections;
using System.Globalization;
using System.Resources;

namespace Havit.Web.UI
{
    /// &lt;summary&gt;
    /// Bázová třída pro stránky, které obsahují controly s lokalizovatelnými skiny.
    /// &lt;/summary&gt;
    public class SkinLocalizationPageBase : Page
    {
        #region ResolveAdapter (override)
        /// &lt;remarks&gt;
        /// Metoda ResolveAdapter je jediné místo, které jsem našel, že se pomocí něj dá vykonávat nějaký kód
        /// mezi InitializeTheme() (po OnPreInit) a ApplyControlSkin (před OnInit).
        /// &lt;/remarks&gt;
        protected override ControlAdapter ResolveAdapter()
        {
// Theme
            FieldInfo themeFieldInfo = typeof(Page).GetField(&quot;_theme&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
            PageTheme theme = themeFieldInfo.GetValue(this) as PageTheme;

            if (theme != null)
            {
                PropertyInfo controlSkinsPropertyInfo = typeof(PageTheme).GetProperty(&quot;ControlSkins&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
                IDictionary controlSkins = controlSkinsPropertyInfo.GetValue(theme, null) as IDictionary;

                foreach (DictionaryEntry entry in controlSkins)
                {
                    ControlSkin controlSkin = entry.Value as ControlSkin;

                    if (controlSkin != null)
                    {
                        FieldInfo controlSkinDelegateFieldInfo = typeof(ControlSkin).GetField(&quot;_controlSkinDelegate&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
                        ControlSkinDelegate controlSkinDelegate = controlSkinDelegateFieldInfo.GetValue(controlSkin) as ControlSkinDelegate;

                        SkinLocalizationDelegateWrapper sldw = new SkinLocalizationDelegateWrapper(controlSkinDelegate);

                        controlSkinDelegateFieldInfo.SetValue(controlSkin, new ControlSkinDelegate(sldw.DoWork));
                    }

                }
            }

            // StylesheetTheme
            FieldInfo styleSheetFieldInfo = typeof(Page).GetField(&quot;_styleSheet&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
            PageTheme styleSheet = styleSheetFieldInfo.GetValue(this) as PageTheme;

            if (styleSheet != null)
            {
                PropertyInfo controlSkinsPropertyInfo = typeof(PageTheme).GetProperty(&quot;ControlSkins&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
                IDictionary controlSkins = controlSkinsPropertyInfo.GetValue(styleSheet, null) as IDictionary;

                foreach (DictionaryEntry entry in controlSkins)
                {
                    ControlSkin controlSkin = entry.Value as ControlSkin;

                    if (controlSkin != null)
                    {
                        FieldInfo controlSkinDelegateFieldInfo = typeof(ControlSkin).GetField(&quot;_controlSkinDelegate&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
                        ControlSkinDelegate controlSkinDelegate = controlSkinDelegateFieldInfo.GetValue(controlSkin) as ControlSkinDelegate;

                        if (!(controlSkinDelegate.Target is SkinLocalizationDelegateWrapper))
                        {

                            SkinLocalizationDelegateWrapper sldw = new SkinLocalizationDelegateWrapper(controlSkinDelegate);

                            controlSkinDelegateFieldInfo.SetValue(controlSkin, new ControlSkinDelegate(sldw.DoWork));
                        }
                    }

                }
            }

            return base.ResolveAdapter();
        }
        #endregion

        #region SkinLocalizationDelegateWrapper
        /// &lt;summary&gt;
        /// SkinLocalizationDelegateWrapper obalí původní výkonný kód skinování a zajistí vyhodnocení resource-odkazů.
        /// &lt;/summary&gt;
        class SkinLocalizationDelegateWrapper
        {
            private ControlSkinDelegate _oldControlSkinDelegate;

            public SkinLocalizationDelegateWrapper(ControlSkinDelegate oldControlSkinDelegate)
            {
                this._oldControlSkinDelegate = oldControlSkinDelegate;
            }

            public Control DoWork(Control control)
            {
                Control result = null;

                if (this._oldControlSkinDelegate != null)
                {
                    result = this._oldControlSkinDelegate(control);
                }

                PropertyInfo[] properties = result.GetType().GetProperties();
                foreach (PropertyInfo property in properties)
                {
                    if (property.PropertyType == typeof(String))
                    {
                        // zajímají nás jen property typu string
                        ThemeableAttribute themableAttribute = Attribute.GetCustomAttribute(property, typeof(ThemeableAttribute)) as ThemeableAttribute;
                        if ((themableAttribute == null) || themableAttribute.Themeable)
                        {
                            // které nemají atribut themable vůbec, nebo ho nemají s volbou false (výchozí je true)
                            string propertyValue = property.GetValue(result, null) as string;
                            if (((propertyValue != null) &amp;&amp; (propertyValue.Length &gt; 10)) &amp;&amp; propertyValue.ToLower(CultureInfo.InvariantCulture).StartsWith(&quot;$resources:&quot;, StringComparison.Ordinal))
                            {
                                string resourceOdkaz = propertyValue.Substring(11);
                                if (resourceOdkaz.Length == 0)
                                {
                                    throw new ConfigurationErrorsException(&quot;Resource odkaz skinu nesmí být prázdný.&quot;);
                                }
                                string resourceClassKey = null;
                                string resourceKey = null;
                                int length = resourceOdkaz.IndexOf(',');
                                if (length == -1)
                                {
                                    throw new ConfigurationErrorsException(&quot;Resource odkaz skinu není platný&quot;);
                                }
                                resourceClassKey = resourceOdkaz.Substring(0, length);
                                resourceKey = resourceOdkaz.Substring(length + 1);
                                string defaultPropertyValue = null;
                                int index = resourceKey.IndexOf(',');
                                if (index != -1)
                                {
                                    defaultPropertyValue = resourceKey.Substring(index + 1); // default value
                                    resourceKey = resourceKey.Substring(0, index);
                                }
                                else
                                {
                                    propertyValue = null;
                                }

                                try
                                {
                                    propertyValue = (string)HttpContext.GetGlobalResourceObject(resourceClassKey.Trim(), resourceKey.Trim());
                                }
                                catch (MissingManifestResourceException)
                                {
                                    // NOOP
                                }

                                if (propertyValue == null)
                                {
                                    propertyValue = defaultPropertyValue;
                                }

                                property.SetValue(result, propertyValue, null);
                            }
                        }
                    }
                }

                return result;
            }
        }
        #endregion
    }
}

Úplné demo v podobě Web Site je v připojeném ZIP souboru.

Související články

Office SharePoint Designer ničí odkazy v .master souborech

Dvě hodiny jsem právě strávil s obligátní chybou „File Not Found.“, problém se nakonec ukázal v tom, že Microsoft Office SharePoint Designer při otevření default.master souboru změnil v direktivách <%@ Register %> atribut src z

<%@ Register TagPrefix="wssuc" TagName="Welcome" src="~/_controltemplates/Welcome.ascx" %>

na

<%@ Register TagPrefix="wssuc" TagName="Welcome" src="_controltemplates/Welcome.ascx" %>

…zabít málo!!!

Zobrazování podrobností výjimek a chyb

Jestli Vás taky rozčilují chybové hlášky SharePointu ve stylu „Unexpected error has occured.“ nebo „File not found.“ a marně hledáte detaily těchto chyb/výjimek, pak vězte, že stačí ve web.configu nastavit v elementu SharePoint/SafeMode atribut CallStack na true:

<SharePoint>
    <SafeMode MaxControls="200" CallStack="true" ...

Samozřejmě k tomu ještě <customErrors mode=“Off“ /> nebo mode=“RemoteOnly“:

<customErrors mode="Off" />

…a je to.

Samozřejmě na produkční mašině bychom měli mít CallStack=“false“.

HttpException: The Controls collection cannot be modified because the control contains code blocks

Tuto krásnou výjimku můžeme znenadání dostat (mimo jiné), pokud budeme v controlu <head runat=“server“> používat code-bloky, např.

<head runat="server">
    <title>HAVIT Goran</title>
    <link rel="stylesheet" type="text/css" href="~/templates/styles/havit.css" />
    <link rel="stylesheet" type="text/css" href="~/templates/styles/global.css" />
    <script type="text/javascript" src="<%= ResolveUrl("~/templates/scripts/HavitScripts.js") %>"></script>
    <asp:ContentPlaceHolder ID="HeadtailCPH" runat="server" />
</head>

Onen ďábelský blok <%= … %> nemusí zpočátku vůbec vadit, pokud však někdy později do stránky přidáme funkcionalitu, která by chtěla měnit Page.Header, např. umístíme do stránky control, který si bude chtít přilinkovat přes Header vlastní styly a skripty, pak budeme obšťastněni krásnou výjimkou System.Web.HttpException:

The Controls collection cannot be modified because the control contains code blocks (i.e. <% … %>).

…lepší je tedy code-bloky <% %>, resp. <%= %> v <head runat=“server“> nepoužívat (ještě lépe je nepoužívat vůbec) a nahradit je jiným způsobem. V tomto konkrétním případě třeba za <%# ResolveUrl(…) %> data-bindovací konstrukci (samozřejmě pak musíme volat Page.Header.DataBind()).

Programátorská hádanka – neprobádaná zákoutí C#

Víte co znamená @ v následujícím bloku kódu?

ICollection @is = dataSource as ICollection;
if (@is != null)
{
     this.Items.Capacity = @is.Count + this.Items.Count;
}

Odpověď najdete o řádku níže napsanou bílým písmem (pro odtajnění třeba označte):

>>> Zavináč je možné použít na začátku identifikátoru, pokud chceme, aby identifikátorem mohlo být i klíčové slovo (is, null, for, …) <<<

…a teď se přiznejte, kdo jste to znal?

Proč nejde udělat rozumný AjaxValidator?

Po půl dni snažení a bádání jsem dospěl k subjektivnímu závěru, že nelze udělat rozumný ASP.NET validátor založený na AJAXu. Přesněji řečeno bylo mým cílem vytvořit obdobu CustomValidatoru, který by měl běžný ServerValidate a navíc klientskou jscript část, která by pomocí AJAXového HTTP-requestu validovala hodnotu vůči tomuto ServerValidate.

Narazil jsem na následující zásadní překážky, které podle mě nelze snadno překonat:

  1. Současné ASP.NET validátory, resp. všechny jejich Microsoftí obslužné klientské skripty a API jsou nekompromisně synchronní. V okamžiku požadavku validace (obvykle před submitem stránky) jsou volány prosté validační funkce všech povolených validátorů a od těchto se očekává jen true/false dle výsledku validace. V současném konceptu validátorů nikde není rozumný prostor pro asynchronní operace či nějakého zásahu do průběhu validace. 
  2. Je prakticky nemožné rozumným způsobem provést synchronní AJAXové volání vůči serveru a stejnětak je nemožné rozumně toto synchronní volání simulovat nějakým blokováním threadu uvnitř validační funkce. Javascript nezná žádné sleep/wait/pause a veškeré snahy o jeho implementaci vždy končí v tupé smyčce zatěžující CPU ze 100% do okamžiku splnění nějaké podmínky (přijetí AJAXového callbacku, dosažení určitého času, naplnění čítače, atp.)

Pro současný koncept ASP.NET validátorů jsem dospěl k těmto závěrům:

  1. Protože je koncept založen především na jednorázové kontrole hodnot před submitem formuláře a má za účel tento submit povolit/zakázat, je oprávněně založen na synchronních operacích a pro jakékoliv dlouhotrvající operace či asynchronní volání zde není prostor a poměrně logicky se s nimi nepočítá.
  2. Asynchronní AJAXová validace vůči serveru by byla použitelná u konceptu průběžné validace, kdy by byly hodnoty kontrolovány ihned po jejich zadání (onchange) a takováto validace by díky svému zpoždění nemohla blokovat odeslání formuláře, spíše by sloužila jako proaktivní kontrola uživatelského zadání.
  3. Současný koncept validátorů připouští pouze zběsilá řešení se synchronizačním blokováním threadu v různých sleep smyčkách (byť s timeoutem), popř. „chováním nekompatibilní“ on-change implementace dle bodu 2.

Veškeré pokusy o implementace AjaxValidatoru, které jsem na netu viděl, trpěly jedním nebo několika nedostatky z výše uvedených a jejich praktická použitelnost se tak blíží nule (spíše se tak stávají přetěžovači serverů, než účinnými validátory).

Avšak! Pokud by se našel někdo, koho by napadlo, jak výše uvedená omezení účinně obejít a funkční AjaxValidator vytvořit, sem s myšlenkou a pochvala ho nemine… ;-)))

PS: Sám jsem dospěl k implementaci AjaxValidatoru, který měl nastavitelný Timeout a ve validační funkci po zavolání asynchronního AJAX requestu na ServerValidate čekal po dobu tohoto timeoutu na odezvu serveru (čekal = různé hnusné implementace zatěžujících smyček čekajících na nějaký příznak nebo timeout). Pokud do Timeoutu nebyl AJAXem výsledek od serveru získán, propustila klientská část validaci jako IsValid=true, a tak se to v případě submitu zvalidovalo na serveru, nebo v případě onchange validace dovalidovalo později asynchronně (až dorazil callback, tak se aktualizoval validátor). Výsledek mi přišel pro praxi nepoužitelný, už timeout 1s je pro ovládání webového formuláře nepříjemný, stránka nereaguje na odeslání formuláře okamžitě, proto jsem to celé zahodil… Můj poznatek je, že pokud validátorem chápeme to, co ASP.NET, tedy blokaci odeslání formuláře, tak přes AJAX cesta nevede, i kdyby to šlo bez toho cyklického pollingu s timeoutem. Leda by byla odezva v řádu milisekund, což při serverových validacích moc nehrozí (obvykle nějaký dotaz do DB na exists, atp.).

Šířka tlačítek v IE (input type=“button“)

V Internet Exploreru je docela alchymie vyrobit tlačítko, které je široké stejně jako text na něm.

<input type="button" value="Text tlačítka" class="button"/>
<asp:Button ID="MyButton" Text="Text tlačítka" class="button"/>

Co jinde funguje, v IE selhává:

.button {
    margin:0;
    padding:0;
}

Co však kupodivu pomůže, je nastavení:

.button{
    padding:0;
    width:auto;
    overflow:visible;
}

…někdy je lepší nebádat, proč tomu tak je.

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.

AJAX: Nefunkční validátory uvnitř UpdatePanelu

Microsoftu se podařil pozoruhodný kousek, standardní validátory ASP.NET 2.0 nefungují korektně uvnitř ajaxového UpdatePanelu – při změně obsahu panelu se původní validátory neodregistrují. Např. tak nefungují validátory v GridView editaci, ve wizzardech, atp. Obvykle vyskakuje krásná JScript chyba „null is null“.

Předchozí beta verze AJAXu to řešily upravenými verzemi validátorů, které korektně používaly ScriptManager ke své přeregistraci. V RTM verzi AJAXu však již tyto validátory nejsou a Microsoft se rozhodl, že je bude aktualizovat všem přes WindowsUpdate, nezávisle na AJAXu.

Ta horší zpráva je, že tak dosud neudělal, přestože AJAX už dávno releasoval.

…takže kdo nemá nervy čekat na opravené validátory, musí sám ručně (přes <tagMapping>) použít jejich aktualizované verze (nebo odtud), tak jak tomu bylo v betách.