Příčetné renderování sitemap podruhé aneb komponenta pro hierarchická data

4. díl - Příčetné renderování sitemap podruhé aneb komponenta pro hierarchická data

Tomáš Herceg       26.01.2010       ASP.NET WebForms, ASP.NET/IIS, .NET       11961 zobrazení

V minulém dílu jsme si ukázali, jak přepsat HTML výstup komponenty TreeView pro docílení vykreslení jednoduchého menu. V tomto dílu si napíšeme komponentu HierarchyRepeater, která umí zobrazit hierarchická data z komponenty SiteMapDataSource přesně podle definovaných šablon.

V minulém dílu jsme si ukázali, jak přepsat HTML výstup již hotových komponent v ASP.NET pomocí Control Adapters. Pokud jste dumali nad tím, jak vyřešit obecně škaredé renderování komponent (mnoho z nich renderuje vnořené tabulky a podobné šílenosti), možná uvítáte sice již starší, ale přesto celkem použitelnou knihovnu CSS Friendly Control Adapters. Ta přepisuje většinu základních komponent ASP.NET tak, aby se renderovaly alespoň trochu hezky. Má sice své mouchy, ale obecně ji doporučuji.

Dnes si však ukážeme o něco lepší řešení, které nám umožní renderovanou sitemapu lépe customizovat a bude umět i něco víc – zobrazit jakákoliv hierarchická data. Napíšeme si komponentu HierarchyRepeater, která bude spolupracovat s datovými zdroji poskytujícími hierarchická data (což jsou třeba SiteMapDataSource a XmlDataSource) a umožní tuto hierarchii vyrenderovat.

Tato komponenta je vhodná pro renderování menu a stromů, které jen zobrazují data. Naše komponenta bude provádět databinding při každém požadavku na stránku, i při postbacku. Nechová se tedy jako ostatní datové komponenty, např. Repeater, které lze databindovat jednou a při postbacku si svůj obsah kompletně obnoví z ViewState. Implementace chytřejší verze HierarchyRepeateru by byla o dost složitější, nechtěl jsem to už zbytečně komplikovat. Jak to udělat lépe si třeba vysvětlíme v některém z příštích dílů.

Jak začít?

Třídu HierarchyRepeater budeme chtít používat zhruba nějak takto:

         <my:HierarchyRepeater ID="HierarchyRepeater1" runat="server" DataSourceID="SiteMapDataSource1"
MainContainerID="MainPlaceholder" ItemContainerID="ItemPlaceholder" SubItemsContainerID="SubItemsPlaceholder">
<LayoutTemplate>
<ul>
<asp:PlaceHolder ID="MainPlaceholder" runat="server"></asp:PlaceHolder>
</ul>
</LayoutTemplate>
<LeafItemLayoutTemplate>
<li>
<asp:PlaceHolder ID="ItemPlaceholder" runat="server"></asp:PlaceHolder>
</li>
</LeafItemLayoutTemplate>
<ItemLayoutTemplate>
<li>
<asp:PlaceHolder ID="ItemPlaceholder" runat="server"></asp:PlaceHolder>
<ul>
<asp:PlaceHolder ID="SubItemsPlaceholder" runat="server"></asp:PlaceHolder>
</ul>
</li>
</ItemLayoutTemplate>
<ItemTemplate>
<asp:HyperLink ID="HyperLink1" runat="server" NavigateUrl='<%# Eval("Url") %>'><%# Eval("Title") %></asp:HyperLink>
</ItemTemplate>
</my:HierarchyRepeater>

LayoutTemplate definuje po vzoru komponenty ListView šablonu celé komponenty – dovnitř placeholderu s ID uvedeným ve vlastnosti MainContainerID se pak vloží celý obsah komponenty.

Šablona LeafItemLayoutTemplate definuje vzhled položky, která nemá žádné potomky (obecně se pro vrcholy ve stromové struktuře, které nemají žádné potomky, používá název listy, proto to Leaf). Obsah samotné položky se vloží do komponenty s ID udaným ve vlastnosti ItemContainerID.

Šablona ItemLayoutTemplate definuje vzhled položky, která potomky má. Opět obsahuje jeden placeholder s ID stejným jako v LeafItemLayoutTemplate, a druhý placeholder s ID definovaným vlastností SubItemsContainerID, který obsahuje kolekci podřízených položek.

Poslední šablonou je ItemTemplate, kterážto definuje vzhled konkrétní položky a může pochopitelně obsahovat i databindingy na hodnoty. Pokud chcete, aby položka měla obrázek, jednoduše ho dopíšete do ItemTemplate. Budeme moci používat i binding expressions, které z aktuální položky zjistí například úroveň zanoření – <%# Container.DataItemLevel %>.

Výhody řešení

Můžeme si jednoduše nadefinovat pro každé menu vlastní layout, jednoduše používat CSS a naše menu bude umět přesně to, co od něj chceme. Není těžké si navíc HierarchyRepeater podědit a šablony mu nainicializovat po svém v kódu – můžeme tak mít mnoho předdefinovaných vzhledů komponenty.

Možná rozšíření

Určitě by se dalo udělat mnoho vylepšení – třeba nějak rozlišit šablony položek první úrovně, druhé úrovně atd.

Přemýšlel jsem i o tom, že bych místo HierarchyRepeater udělal komponentu HierarchyListView, která by uměla i operace Insert, Update a Delete, bohužel ASP.NET nějak nepočítá s tím, že hierarchická data bude někdo chtít měnit a hierarchické komponenty DataSource tyto operace vůbec neumí. Můžeme je tam doimplementovat sami, ale hrozí riziko, že pokud si v produktovém týmu ASP.NET vývojáři vzpomenou a do daných rozhraní příslušné metody doplní, museli bychom svůj kód přepsat, protože hlavičky metod určitě vymyslí jinak než my.

Naše komponenta ale pro jednoduchost bude umět data jen zobrazovat, díky čemuž to nemusíme řešit.

Základní kostra třídy

Naše třída s nadeklarovanými základními vlastnostmi může vypadat například takto (vše již známe z minulých dílů):

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Collections.ObjectModel;

namespace MyComponents
{

/// <summary>
/// Hierarchická komponenta Repeater
/// </summary>
[
ParseChildren(true)]
public class HierarchyRepeater : HierarchicalDataBoundControl
{
/// <summary>
/// Šablona celé komponenty
/// </summary>
[
Browsable(false), PersistenceMode(PersistenceMode.InnerProperty)]
public ITemplate LayoutTemplate { get; set; }

/// <summary>
/// Šablona položky s podřízenými položkami
/// </summary>
[
Browsable(false), PersistenceMode(PersistenceMode.InnerProperty)]
public ITemplate ItemLayoutTemplate { get; set; }

/// <summary>
/// Šablona položky bez podřízených položek
/// </summary>
[
Browsable(false), PersistenceMode(PersistenceMode.InnerProperty)]
public ITemplate LeafItemLayoutTemplate { get; set; }

/// <summary>
/// Šablona obsahu položky
/// </summary>
[
Browsable(false), PersistenceMode(PersistenceMode.InnerProperty), TemplateContainer(typeof(HierarchyRepeaterDataItem))]
public ITemplate ItemTemplate { get; set; }

/// <summary>
/// ID komponenty v šabloně LayoutTemplate, do níž se vloží celý obsah komponenty
/// </summary>
[
Category("Layout"), DefaultValue("MainContainer"), Bindable(true)]
public string MainContainerID
{
get { return (string)ViewState["MainContainerID"] ?? "MainContainer"; }
set { ViewState["MainContainerID"] = value; }
}

/// <summary>
/// ID komponenty v šabloně ItemLayoutTemplate nebo LeafItemLayoutTemplate, do níž se vloží obsah položky
/// </summary>
[
Category("Layout"), DefaultValue("ItemContainer"), Bindable(true)]
public string ItemContainerID
{
get { return (string)ViewState["ItemContainerID"] ?? "ItemContainer"; }
set { ViewState["ItemContainerID"] = value; }
}

/// <summary>
/// ID komponenty v šabloně ItemLayoutTemplate, do níž se vloží podřízené položky
/// </summary>
[
Category("Layout"), DefaultValue("SubItemsContainer"), Bindable(true)]
public string SubItemsContainerID
{
get { return (string)ViewState["SubItemsContainerID"] ?? "SubItemsContainer"; }
set { ViewState["SubItemsContainerID"] = value; }
}


/// <summary>
/// Vrací HTML tag hlavního elementu komponenty
/// </summary>
protected override HtmlTextWriterTag TagKey
{
get { return HtmlTextWriterTag.Div; }
}




}

}

Aby komponenta mohla nějak rozumně fungovat, pro každou položku v hierarchii si vytvoří instanci třídy HierarchyRepeaterItem, kterážto bude obsahovat pár vlastností typu DataItemIndex, DataItemParentIndex, DataItemLevel atd., pomocí nichž budeme schopni zrekonstruovat hierarchii položek a rychle se ptát na rodiče. Položky si totiž pro snazší přístup zlinearizujeme do kolekce Items, kterou dáme ven uživateli jen pro čtení.

Jak udělat kolekci, která je navenek jen pro čtení? Můžeme použít generickou třídu ReadOnlyCollection<T>, kde T je datový typ vnitřních položek, v našem případě HierarchyRepeaterItem. Pojďme na věc.

Vnitřní položky

Přidejte do třídy HierarchyRepeater tyto deklarace a konstruktor:

         private List<HierarchyRepeaterDataItem> items = new List<HierarchyRepeaterDataItem>();
/// <summary>
/// Kolekce vnitřních položek
/// </summary>
public ReadOnlyCollection<HierarchyRepeaterDataItem> Items { get; private set; }

/// <summary>
/// Inicializuje novou instanci třídy
/// </summary>
public HierarchyRepeater()
{
Items =
new ReadOnlyCollection<HierarchyRepeaterDataItem>(items);
}

Tím se vytvořila privátní kolekce items (klasický List, který můžeme upravovat) a vlastnost Items typu ReadOnlyCollection, která je veřejná. Kód zvenčí tedy uvidí jen tuto kolekci, kterou může procházet, zjistit počet položek, ale to je tak všechno. Nemůže do ní nic přidat, nahradit, smazat atd. V konstruktoru pak ReadOnlyCollection zinicializujeme a jako zdroj dat jí dáme seznam items – kolekce jej navenek zpřístupní pro čtení, ale modifikovat ho můžeme pouze přes seznam items, ke které se dostane jen kód v naší třídě. Změna v seznamu items se automaticky promítne do kolekce Items.

Nyní pojďme vytvořit třídu vnitřní položky, bude poměrně jednoduchá:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI;

namespace MyComponents
{
/// <summary>
/// Položka komponenty HierarchyRepeater
/// </summary>
public class HierarchyRepeaterDataItem : Control, INamingContainer, IDataItemContainer
{
/// <summary>
/// Vrátí datový objekt, proti kterému se provádí databinding
/// </summary>
public object DataItem { get; internal set; }

/// <summary>
/// Vrací index položky v kolekci položek rodičovské komponenty
/// </summary>
public int DataItemIndex { get; internal set; }

/// <summary>
/// Vrací úroveň (číslováno od 0) položky v hierarchii
/// </summary>
public int DataItemLevel { get; internal set; }

/// <summary>
/// Vrací index rodičovské položky v hierarchii
/// </summary>
public int DataItemParentIndex { get; internal set; }

/// <summary>
/// Vrací pozici položky zobrazené v komponentě
/// </summary>
public int DisplayIndex { get; internal set; }

/// <summary>
/// Vrací cestu položky v hierarchii
/// </summary>
public string DataItemPath { get; internal set; }
}
}

Jak vidíme, má nadefinováno pár vlastností – některé jsou vynucené rozhraním IDataItemContainer. Vidíme též, že třída dědí z Control, je to klasická komponenta ve stránce.

Aby vše správně fungovalo, dědí tato třída ještě z INamingContainer. Důvod je prostý – do každé HierarchyRepeaterDataItem budeme instanciovat šablonu ItemTemplate, která může obsahovat komponenty. Chceme, aby ID komponent v každé instanci šablony bylo unikátní – každá položka našeho HierarchyRepeateru tedy musí být naming container. To dáme najevo právě implementováním rozhraní INamingContainer. Podotýkám, že toto rozhraní nedeklaruje žádné metody ani vlastnosti, ASP.NET runtime se jen dívá, jestli ho třída implementuje nebo ne.

Jak fungují data-bound controls?

Data-bound control je obecně komponenta, která si vezme z nějakého datového zdroje data a nějakým způsobem je prezentuje uživateli. Jak to dělá, to už je její věc. Mezi známé databound komponenty patří GridView, Repeater, ListView, ale třeba i DropDownList či CheckBoxList.

Klasické zdroje dat, které vracejí tabulku nebo pole objektů, poskytují rozhraní IEnumerable. Hierarchické zdroje dat poskytují rozhraní IHierarchicalEnumerable. Toto rozhraní dědí z IEnumerable, ale obsahuje metodu, která umožňuje pracovat s hierarchií.

Pokud píšeme klasickou data-bound control, dědíme ze třídy DataBoundControl, kterážto dědí z BaseDataBoundControl. My píšeme hierarchickou data-bound control, dědíme ze třídy HierarchicalDataboundControl, která dědí také ze třídy BaseDataBoundControl.

Třída BaseDataBoundControl definuje vlastnosti DataSource, DataSourceID a mnoho dalších členů, dále také metodu GetData, která vrací tzv. view, neboli pohled na data. View je v našem případě objekt HierarchicalDataSourceView a deklaruje metodu Select.

V případě klasických data-bound controls nám metoda GetData vrátí objekt DataSourceView (resp. nějaký, který z něj dědí), který má kromě metody Select též metody Insert, Update, Delete a další. Lineární datové struktury můžeme pomocí objektu view i modifikovat, hierarchické bohužel ne. To je příčinou problému, o němž jsem psal v sekci s možnými rozířeními.

V případě hierarchických datových komponent chce metoda GetData zadat jeden parametr typu string, a to cestu v hierarchii, která se má vracet. To se hodí v případě, že bychom v komponentě chtěli zobrazovat jen určitý podstrom. Příslušný datový zdroj by nám již sám vrátil jen příslušné položky.

Protože naše komponenta bude jednoduchá, předáme vždy prázdný řetězec, čímž naznačíme, že chceme všechno – tedy celou hierarchii.

Výhodou dědění z DataBoundControl pochopitelně je to, že se nemusíme starat o implementaci vlastností DataSourceID, DataSource, nemusíme kontrolovat, jestli daný datový zdroj umí to, co chceme, jestli je ID správné atd. Navíc by to bylo mimořádně hloupé – měli bychom v aplikacích opakující se kód. Dědičnost nám v tomto případě šetří práci a brání tomu, abychom jednu změnu museli dělat na více místech, protože bychom měli na různých místech stejný kód, který dělá to samé.

Zobrazujeme položky

Jak v naší komponentě zobrazit položky? Jednoduše – přepíšeme metodu CreateChildControls, v ní si vytáhneme z datového zdroje příslušný view, na němž zavoláme Select. Projdeme vrácenou kolekci, která implementuje rozhraní IHierarchicalEnumerable a vygenerujeme vnitřní komponenty. Tedy nic světoborného.

         /// <summary>
/// Vytáhne z datového zdroje data a vytvoří vnitřní komponenty
/// </summary>
protected override void CreateChildControls()
{
if (LeafItemLayoutTemplate == null)
throw new ArgumentNullException("LeafItemLayoutTemplate");
if (ItemLayoutTemplate == null)
throw new ArgumentNullException("ItemLayoutTemplate");
if (ItemTemplate == null)
throw new ArgumentNullException("ItemTemplate");
if (string.IsNullOrEmpty(MainContainerID) && LayoutTemplate != null)
throw new ArgumentException("MainContainerID");
if (string.IsNullOrEmpty(ItemContainerID))
throw new ArgumentException("ItemContainerID");
if (string.IsNullOrEmpty(SubItemsContainerID))
throw new ArgumentException("SubItemsContainerID");

// pokud máme LayoutTemplate, instanciovat ji a najít placeholder
if (LayoutTemplate != null)
LayoutTemplate.InstantiateIn(
this);
var mainContainer = this.FindControl(MainContainerID) ?? this;

// instanciovat jednotlivé položky
RecursiveCreateChildControls(mainContainer.Controls, GetData(
"").Select(), 0, -1);
}

V této metodě nejdřív zkontrolujeme, jestli máme zadané všechny šablony (LayoutTemplate je nepovinná) a vlastnosti definující ID placeholderů v layout šablonách.

Pokud máme zadanou LayoutTemplate, vyiinstanciujeme ji. Dále pak potřebujeme komponentu, do níž nacpeme jednotlivé položky – najdeme tedy v tom, co bylo v šabloně, komponentu s ID daným vlastností MainContainerID. Pokud bychom ji nenašli (ať už proto, že někdo zadal ID špatně, tady by bylo možná lepší vyhodit chybu; anebo proto, že LayoutTemplate nebyla zadána), určíme jako hlavní kontejner tuto komponentu (to se dělá operátorem ?? – pokud je výraz nalevo od něj null, vrátí výraz napravo, jinak vrátí výraz nalevo).

Jakmile jsme zjistili, kam máme vnitřní položky vkládat, zavoláme metodu RecursiveCreateChildControls a předáme jí cílovou kolekci komponent ControlCollection, kam má položky přidat, a samozřejmě také data, která se budou zobrazovat (ty vytáhneme z datového zdroje; chtělo by to ověřit, zda-li ho vůbec máme nastavený, ale to si kdyžtak dodělejte sami). Nakonec jí předáme index úrovně, na níž položky jsou, a index rodičovské položky (protože položky na nulté nejvyšší úrovni žádného rodiče nemají, nastavíme jim index rodiče –1).

Nyní už stačí samotná metoda RecursiveCreateChildControls, která vezme předanou kolekci a vygeneruje její komponenty. Zde je její kód:

         /// <summary>
/// Vytvoří položky pro danou kolekci hierarchických dat
/// </summary>
private void RecursiveCreateChildControls(ControlCollection container, IHierarchicalEnumerable values, int level, int parentIndex)
{
// projít všechny hodnoty
foreach (var value in values.OfType<object>().Select(d => values.GetHierarchyData(d)))
{
// vytvořit datovou položku
var item = new HierarchyRepeaterDataItem();
item.DataItemIndex = items.Count;
item.DataItemParentIndex = parentIndex;
item.DataItemLevel = level;
item.DataItem = value.Item;
item.DataItemPath = value.Path;
item.DisplayIndex = items.Count;

// instanciovat šablonu podle toho, jestli položka má potomky
if (value.HasChildren)
ItemLayoutTemplate.InstantiateIn(item);
else
LeafItemLayoutTemplate.InstantiateIn(item);

// přidat datovou položku do kolekce
container.Add(item);
items.Add(item);

// nahradit placeholder s ID ve vlastnosti ItemContainerID
var control = item.FindControl(ItemContainerID);
if (control == null) throw new ArgumentException("ItemContainerID");
ItemTemplate.InstantiateIn(control);

// provést databinding na položce
item.DataBind();

// vytvořit vnitřní položky
if (value.HasChildren)
{
control = item.FindControl(SubItemsContainerID);
if (control == null) throw new ArgumentException("SubItemContainerID");
RecursiveCreateChildControls(control.Controls, value.GetChildren(), level +
1, item.DataItemIndex);
}
}
}

Zajímavý je hned vrchní foreach cyklus. Projdeme všechny hodnoty z kolekce values, na níž aplikujeme funkci Select. Pokud neznáte technologii LINQ, pak vězte, že Select je metoda, která se volá na kolekci a vrátí novou kolekci, která obsahuje prvky původní kolekce prohnané nějakou funkcí. Funkce Select bere jeden parametr, a to lambda funkci d => values.GetHierarchyData(d) (představme si to jako zjednodušený zápis funkce – má jeden parametr d a obsahuje jen řádek return values.GetHierarchyData(d)). Takže výraz values.Select(d => values.GetHierarchyData(d)) vrátí kolekci stejně velkou jako je kolekce values, a pokud prvky ve values byly di, pak prvky nové kolekce jsou výsledky volání values.GetHierarchyData(di) na jednotlivé položky původní kolekce.

K čemu takováto šaškárna? Rozhraní IHierarchicalEnumerable dědí z IEnumerable a kromě toho definuje metodu GetHierarchyData, které pro každou položku vrátí objekt implementující rozhraní IHierarchyData. Pomocí něj můžeme přistupovat k podřízeným a nadřízeným položkám. Je to celé trochu zamotané, možná to spíš osvětlí tento obrázek.

 IHierarchicalEnumerable kolekce

Ještě jednou a podle obrázku – z prvního rozhraní IHierarchicalEnumerable v proměnné values si pro každou položku vytáhneme metodou Select objekt IHierarchyData, na němž zavoláme metodu GetChildren, která nám vrátí kolekci objektů IHierarchicalEnumerable s podřízenými položkami. Z každého prvku kolekce IHierarchicalEnumerable tedy můžeme získat opět novou kolekci IHierarchicalEnumerable jeho podřízených položek.

Ve foreach cyklu pro každou položku předané kolekce a transformované metodou Select (položky z prostředního sloupce na obrázku) vytvoříme novou instanci třídy HierarchyRepeaterDataItem a nastavíme jí její vlastnosti – DataItem na konkrétní hodnotu objekt (vlastnost Value z objektu IHierarchyData), dále vlastnosti DataItemIndex a DisplayIndex na aktuální počet položek v kolekci items (do této kolekce tuto položku vzápětí přidáme, takže první bude mít index 0, protože před jejím přidáním měla kolekce 0 prvků; druhá bude mít 1 atd.). Level a index rodiče nastavíme podle parametrů, které jsme dostali při volání metody RecursiveCreateChildControls.

Pokud má položka nějaké potomky, instanciujeme dovnitř šablonu ItemLayoutTemplate, pokud žádné nemá, instanciujeme místo toho LeadItemLayoutTemplate. Uvnitř si v každém případě najdeme komponentu s ID uvedeným ve vlastnosti ItemContainerID a do ní vyinstanciujeme šablonu ItemTemplate, která obsahuje samotnou položku – například odkaz a text.

Pokud měla položka nějaké podřízené položky (HasChildren je true), najdeme si komponentu s ID z SubItemsContainerID a zavoláme se rekurzivně pro kolekci komponent nalezeného kontejneru a kolekci dětí právě procházené položky (která je opět typu IHierarchicalEnumerable), přičemž o jedničku zvýšíme level a jako rodiče předáme index aktuální položky. Celá tato maškaráda pak začíná znovu o úroveň níže, až takto projdeme rekurzivně celý strom položek.

Po přidání položky do stránky ještě zavoláme DataBind, čímž hned provedeme databinding v šabloně ItemTemplate.

A to je vlastně celé. Výsledný repeater ve stránce nad testovací sitemapou z minulého dílu vypadá nějak takto:

Výsledek

Předdefinování šablon

Abychom nemuseli při každém použití specifikovat šablony natvrdo, můžeme se ještě procvičit v programování šablon – zkuste si nyní sami napsat komponentu SimpleMenu dědící z HierarchyRepeater, která v konstruktoru nastaví šablony ItemTemplate, ItemLayoutTemplate, LeafItemTemplate a LayoutTemplate tak, aby se menu vyrenderovalo stejně jako náš HierarchyRepeater ze začátku článku.

Pokud nad něčím váháte, můžete si zobrazit správné řešení.

Závěrem

To je pro tento díl vše. Doufám, že jste se nezamotali v sekci s IHierarchicalEnumerable a že chápete vše, co jsem se zde pokoušel vysvětlit. Pokud máte nějaké náměty či připomínky, pište do komentářů.

 

hodnocení článku

1 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Všechny díly tohoto seriálu

6. Jak je to s cestami a adresami v ASP.NET 05.09.2010
5. Komponenta pro zadávání data a času 01.07.2010
4. Příčetné renderování sitemap podruhé aneb komponenta pro hierarchická data 26.01.2010
3. Příčetné renderování sitemap 19.01.2010
2. Komponenta pro editaci kolekcí pro ASP.NET 01.01.2010
1. Jak napsat CAPTCHA komponentu pro ASP.NET? 20.09.2009

 

 

 

Nový příspěvek

 

Diskuse: Příčetné renderování sitemap podruhé aneb komponenta pro hierarchická data

Zdravím.

Prosím dalo by se to řešit bez toho LINQ výrazu?

foreach (var value in values.OfType<object>().Select(d => values.GetHierarchyData(d)))

Díky

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback