V diskusních fórech zaměřených na .Net technologii se často objevují dotazy, jejichž společným jmenovatelem je snaha o odchycení změnových událostí ovládacích prvků vnořených v šablonách prvků DataGrid, Repeater či Datalist. Pisatelé většinou končí zjištěním, že například událost Click tlačítka odchytí v události ItemCommand DataGridu, ale k události SelectedIndexChanged DropDownListu se přes DataGrid žádným elegantním způsobem nedostanou. Tento článek ukazuje, jak lze toto omezení obejít.

Nutná dávka teorie

Pokud se v serverovém událostním modelu ASP.NET neorientujete, přečtěte si článek, který jej prodrobně popisuje. Používání šablon a kompozitních serverových ovládacích prvků ulehčuje takzvané bublání událostí z vnořených ovládacích prvků. Co bublání událostí přesně znamená? Představte si, že do sloupce se šablonou (TemplateColumn) v DataGridu vložíte tlačítko, na jehož událost Click chcete reagovat. Tlačítko je po připojení datového zdroje k DataGridu vytvořeno v každém řádku. Bez bublání událostí bychom si museli manuálně přihlásit odběr události Click každého tlačítka. To konkrétně znamená, že bychom museli projít každou položku DataGridu, v ní vyhledat prvek tlačítko a zaregistrovat k události Click delegáta. Pokud jste podobná řešení vyzkoušeli při registraci změnových událostí, protože jste si mysleli, že jiný způsob neexistuje, jistě budete se mnou souhlasit, že vlastnosti jako čitelnost a spravovatelnost kódu značně utrpěly.

Tvůrci ASP.NET přišli s jednoduchým řešením – události z vnořených ovládacích prvků je možné posílat (bublat) do nadřazených prvků. Právě k tomuto účelu byla vytvořena chráněná metoda RaiseBubbleEvent deklarovaná ve třídě Control. Všechny prvky v ASP.NET dědí přímo nebo nepřímo ze třídy Control, proto mohou metodu RaiseBubbleEvent použít k bublání vlastních událostí. Pro zajímavost si uvedeme kód metody RaiseBubbleEvent:

protected void RaiseBubbleEvent (object source, EventArgs args)
{
  Control curParent = Parent;
  while (curParent != null)
  {
    if (curParent .OnBubbleEvent (source, args))
      return;
  curParent = curParent.Parent;
  }
}

Metoda má dva argumenty. Argument source představuje zdroj události, argument args nese další informace o události. Všechny třídy, které nesou informace o události, musí dědit přímo či nepřímo z třídy EventArgs, a proto může být metoda použita k bublání jakékoli události. V kódu je volána metoda OnBubbleEvent rodiče prvku, který bublá událost. Metoda OnBubbleEvent je deklarována ve třídě Control a její výchozí implementace pouze vrátí hodnotu false.

Ovládací prvky mohou metodu OnBubbleEvent přepsat a reagovat specifickým způsobem na bublanou událost. V případě, že metoda OnBubbleEvent vrátí true, je bublání události ukončeno, protože rodič dal najevo, že událost zpracoval. Když metoda OnBubbleEvent vrátí false, je událost bublána dalšímu rodiči v hierarchii ovládacích prvků (přesněji řečeno jde o rodiče rodiče původního prvku). Celá sekvence příkazů se opakuje, dokud metoda OnBubbleEvent nevrátí true nebo dokud metoda RaiseBubbleEvent nedojde na počátek hierarchie ovládacích prvků, což je indikováno hodnotou null v proměnné curParent. Prvek, který bublanou událost zpracuje, ji většinou zapouzdří do vlastní generické události, jejíž odběr si přihlásí zájemce o události vnořených prvků. Příkladem může být událost ItemCommand DataGridu.

Popsané řešení je pro autory ASP.NET stránek velmi přínosné, ale má jednu nevýhodu. Standardně jsou v ASP.NET bublány pouze události, jejichž třída nesoucí dodatečné informace o události dědí z třídy CommandEventArgs. V praxi to znamená, že je bublána událost Click tlačítek (Button, LinkButton, ImageButton), ale nejsou například bublány změnové události (SelectedIndexChanged DropDownListu, TextChanged TextBoxu…).

Příklad bublání událostí

Na DropDownListu si ukážeme, jak můžeme bublání změnových událostí do ASP.NET sami doplnit (zdrojový kód ke stažení). Náš příklad se pro názornost soustředí na bublání události SelectedIndexChanged potomka DropDownListu (třída DropDownListEx), který je vložen do sloupce se šablonou v prvku odvozeném z DataGridu (třída DataGridEx). V některém dalším článku si ukážeme ovládací prvek, který dokáže bublat události do DataGridu, Repeateru i DataListu.

Nejdříve vytvoříme třídu, která ponese informace o bublané události. Třída bude obsahovat vlastnost s názvem Source, která vrací odkaz na DropDownListEx, jež událost vyvolal, a vlastnost Item, která obsahuje odkaz na položku DataGriduEx (DataGridItem), jejíž součástí je DropDownListEx. Vlastnost Source je typu Object, abychom třídu BubbledIndexChangeEventArgs mohli případně použít i k přenosu informací o bublaných událostech z jiných ovládacích prvků.

public class BubbledIndexChangeEventArgs : EventArgs
{
  private DataGridItem m_item;
  private object m_source;
  public BubbledIndexChangeEventArgs(DataGridItem item, object source) : base()
  {
    if (item == null)
      throw new ArgumentNullException(„item“);
    m_item = item;
    if (source == null)
      throw new ArgumentNullException(„source“);
    m_source = source;
  }
  public DataGridItem Item
  {
    get
    {
      return m_item;
    }
  }
  public object Source
  {
    get
    {
      return m_source;
    }
  }
}

Dále vytvoříme potomka třídy DropDownList, který bude bublat událost SelectedIndexChanged.

public class DropDownListEx : DropDownList
{
  public DropDownListEx() : base()
  {
  }
  protected override void OnSelectedIndexChanged(EventArgs e)
  {
    base.OnSelectedIndexChanged (e);
    DataGridItem item = null;
    Control hc = null;
    hc = this.NamingContainer as Control;
    while (hc != null)
    {
      item = hc as DataGridItem;
      if (item != null)
        break;
      hc = hc.NamingContainer as Control;
    }
    if (item != null)
      RaiseBubbleEvent(this, new BubbledIndexChangeEventArgs(item, this));
  }
}

Přepsaná metoda OnSelectedIndexChanged, která je odpovědná za vyvolání události SelectedIndexChanged, nejprve zavolá metodu OnSelectedIndexChanged bázové třídy, aby byli notifikováni klienti, kteří si přihlásili odběr události SelectedIndexChanged přímo u DropDownListuEx. Dále metoda v cyklu while hledá položku DataGriduEx, ve které je DropDownListEx obsažen. Využita je vlastnost NamingContainer, která vrací odkaz na rozhraní INamingContainer. INamingContainer je značkovací rozhraní, které nemá žádné metody a kterým ovládací prvky, jež jsou rozhraním označeny, signalizují běhovému prostředí ASP.NET, že má jejich vnořený prvkům generovat unikátní jména závislá na jejich pozici v hierarchii ovládacích prvků. Rozhraním je označena i třída DataGridItem.

Pokud není položka datagridu nalezena, má proměnná item hodnotu null a DropDownListEx událost nebublá, protože je zřejmé, že nebyl vložen do DataGriduEx. V opačném případě je vytvořena instance třídy BubbledIndexChangeEventArgs, které je předán odkaz na nalezenou položku DataGriduEx a na aktuální instanci prvku DropDownListEx. Poté je událost bublána metodou RaiseBubbleEvent.

Nyní musíme vytvořit potomka DataGridu, který bublanou událost zachytí, zpracuje a zpřístupní ji formou vlastní generické události.

public class DataGridEx : DataGrid
{
  protected static object DropDownSelectedIndexChangedKey = new Object();
  public delegate void DropDownSelectedIndexChangedEventHandler(object source, BubbledIndexChangeEventArgs e);
  public event DropDownSelectedIndexChangedEventHandler DropDownSelectedIndexChanged
  {
    add
    {
      Events.AddHandler(DropDownSelectedIndexChangedKey, value);
    }
    remove
    {
      Events.RemoveHandler(DropDownSelectedIndexChangedKey, value);
    }
  }
  public DataGridEx() : base()
  {
  }
  protected override bool OnBubbleEvent(object source, EventArgs args)
  {
    if (args is BubbledIndexChangeEventArgs)
    {
      OnDropDownSelectedIndexChanged((BubbledIndexChangeEventArgs)args);
      return true;
    }
    else
      return base.OnBubbleEvent (source, args);
  }
  protected virtual void OnDropDownSelectedIndexChanged(BubbledIndexChangeEventArgs e)
  {
    DropDownSelectedIndexChangedEventHandler eh = (DropDownSelectedIndexChangedEventHandler)Events[DropDownSelectedIndexChangedKey];
    if (eh != null)
      eh(this, e);
  }
}

Ve třídě DataGridEx je deklarována nová událost DropDownSelectedIndexChanged a delegát pro tuto událost s názvem DropDownSelectedIndexChangedEventHandler. Událost DropDownSelectedIndexChanged je vyvolána, když je zachycena bublaná událost SelectedIndexChanged vnořeného DropDownListuEx.

Chráněný statický člen s názvem DropDownSelectedIndexChangedKey je unikátním klíčem události. Jestliže jste neslyšeli o optimalizované implementaci událostí, přečtěte si článek, který se tomuto tématu podrobně věnuje.

Třída DataGridEx přepisuje metodu OnBubbleEvent, ve které zkontroluje, zda třída s informacemi o události je typu BubbledIndexChangeEventArgs. V případě, že byla předána instance třídy BubbledIndexChangeEventArgs, je vyvolána událost DropDownSelectedIndexChanged metodou OnDropDownSelectedIndexChanged a metoda OnBubbleEvent vrátí hodnotu true, která ve tříde DropDownListEx zamezí dalšímu bublání události. Pokud nebyla předána instance třídy BubbledIndexChangeEventArgs, metoda deleguje zpracování bublané události na implementaci v bázové třídě.

Uživatel si musí prvky DataGridEx a DropDownListEx na ASP.NET stránce zaregistrovat a po vložení prvku DropDownListEx do sloupce se šablonou si jen přihlásí odběr události DropDownSelectedIndexChanged. Ještě raději připomenu, že musíte nastavit vlastnost AutoPostBack prvku DropDownListEx na true, pokud chcete být na výběr nové položky upozorněni ihned.

Snad tento článek sníží počet v úvodu zmiňovaných příspěvků v diskusních fórech, ve kterých se lamentuje nad absencí bublání změnových událostí, a že si jejich pisatelé uvědomí bezprecedentní míru flexibility, kterou jim .Net Framework poskytuje.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Žádný příspěvek v diskuzi

Odpovědět