Komponenta pro zadávání data a času

5. díl - Komponenta pro zadávání data a času

Tomáš Herceg       01.07.2010       C#, VB.NET, ASP.NET WebForms, Komponenty, .NET       17091 zobrazení

V tomto díle se podíváme na to, jak s využitím komponenty CalendarExtender z knihovny AjaxControlToolkit napsat komponentu pro snadné zadávání data, která podporuje validaci a datové zdroje.

Jednou z velmi častých výtek mnoha ASP.NET vývojářů je absence komponenty DateTimePicker, která by umožňovala zadat uživateli datum.

ASP.NET sice obsahuje vestavěnou komponentu Calendar, která ale renderuje ne příliš hezký HTML kód a navíc jejím použitím odříznete uživatele, kteří nemají zapnuté klientské skripty. Navíc se komponenta Calendar pro zadávání data nehodí vždy – jednak si nějak rozumně neporadí s možností, kdy datum není zadáno (tedy s hodnotou null), a navíc kdyby po mě někdo chtěl, abych mu datum narození své babičky klikal v kalendáři a musel tisíckrát přepnout na předchozí měsíc, tak bych ho asi poslal velmi nevybíravými výrazy do Abu Dhábí.

Na druhou stranu když mám vybrat vhodné datum pro konání nějaké akce, je kalendář praktický, protože v něm vidíme, co je to za den v týdnu atd. Ideální je tedy nabídnout uživateli obě možnosti a nechat ho, aby si vybral – pokud datum odněkud opisuje, je příliš v minulosti, anebo ho prostě zná z hlavy, je rychlejší ho napsat ručně. V opačném případě použije kalendář.

Co tedy budeme dělat?

V dnešním díle tohoto seriálu tedy napíšeme komponentu, kterou lze použít pro zadávání data a času. Bude se skládat ze tří částí – textového pole a kalendáře, který se zobrazí nebo schová v případě, že má textové pole focus. Protože tuto funkcionalitu částečně řeší komponenta CalendarExtender ze známého balíku AjaxControlToolkit, použijeme i tu a postavíme komponentu DateTimePicker, která bude podporovat nezadání hodnoty, bude validovat formát zadaného data a bude podporovat obousměrný databinding z datových zdrojů.

V současné době se poměrně dost mluví o HTML 5 a nové verze prohlížečů jej pomalu začínají podporovat, přestože finální verze tohoto bohulibého standardu má být uvedena až za poměrně dlouhou dobu. Některé nápady, které HTML 5 přináší, jsou poměrně pěkné a měly tady být už před 10 lety. HTML 5 mimo jiné nabízí elegantní řešení zadávání data ve formulářích, my ale naši komponentu uděláme tak, aby si vystačila s aktuální verzí HTML 4 či XHTML 1.x, jelikož ne všechny prohlížeče mnoho prohlížeček tyto novinky z HTML 5 podporují.

Začínáme

Pro základní funkcionalitu by samozřejmě stačila komponenta TextBox s validátorem, který kontroluje správnost data. To byla nejčastěji používaná možnost v ASP.NET 2.0 těsně po jeho uvedení, pokud jste si nechtěli vyhrát s javascriptem a napsat si skrývání, zobrazování a výběr data z kalendáře sami. A i ten validátor jste konec konců museli naimplementovat vy, protože ASP.NET v základu žádný pro kontrolu formátu data a času neobsahuje.

Krátce po uvedení ASP.NET 2.0 se začala prudce rozmáhat technologie AJAX a s ní přišel framework Atlas, dnes se mu říká jen ASP.NET AJAX. Jednalo se o separátní knihovnu, která obsahovala mimo jiné komponenty ScriptManager a UpdatePanel (k nim se také dostaneme, pokud je ještě neznáte). Kromě toho vzniknul komunitní projekt Ajax Control Toolkit, který obsahuje nejrůznější komponenty využívající javascriptovou knihovnu Microsoft Ajax Library.

Od .NET Frameworku 3.5 se ASP.NET AJAX stal součástí .NETu, kdežto Ajax Control Toolkit je pořád separátní knihovna (dnes již je celkem odladěná a stabilní, takže se ji rozhodně nemusíte bát používat). Ke stažení je na stránce http://ajaxcontroltoolkit.codeplex.com/. Ukázky komponent, které obsahuje (je jich dost, některé z nich jsou docela praktické, což ale nelze říct o všech), jsou pak na http://www.asp.net/ajax/ajaxcontroltoolkit/samples/.

Mnoho komponent z Ajax Control Toolkitu jsou tzv. extendery, speciální komponenty, které rozšiřují schopnosti komponent ostatních. Kromě toho, že je můžete ovládat ze serverového kódu, máte k nim přístup i přes klientské skripty, což se někdy může hodit a což si také v některém z příštích dílů ukážeme.

Komponentu, kterou budeme potřebovat, je CalendarExtender. Každý extender má jednu hlavní a důležitou vlastnost – TargetControlID, což je ID komponenty, kterou tento extender rozšiřuje.

Pojďme se tedy podívat, jak tuto komponentu ve spojení s textovým polem použít.

Vytvořte novou Web Site a do složky Bin přidejte poslední verzi knihovny AjaxControlToolkit.dll, kterou stáhnete z výše uvedené adresy. Do stránky pak vložte tento kód:

 <%@ Page Language="C#" %>
<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="ajax" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>

<div>

<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<ajax:CalendarExtender ID="CalendarExtender1" runat="server" TargetControlID="TextBox1" />

</div>
</form>
</body>
</html>

První věc, kterou musíme udělat vždy, když chceme použít ASP.NET AJAX a jakoukoliv komponentu, která jej používá, přidáme do stránky komponentu ScriptManager (najdete ji v toolboxu v sekci AJAX Extensions). Tato komponenta se stará o zprostředkování klientských skriptů generované stránce podle toho, jaké komponenty obsahuje (aby skriptů nebylo zbytečně moc), dále zajišťuje například globalizaci, neboli správné formátování data, času, názvů měsíců apod.

Pokud máte větší webovou aplikaci a používáte MasterPages, stačí komponentu ScriptManager dát jen do MasterPage. Musí být hned na začátku a uvnitř hlavního formuláře ve stránce.

Dále máme ve stránce obyčejnou komponentu TextBox a pod ní komponentu CalendarExtender (prefix ajax je zaregistrován nahoře direktivou Register, u větších než velmi malých webů doporučuji zaregistrovat jej v souboru web.config globálně pro celou aplikaci). Komponenta CalendarExtender má nastaveno TargetControlID, což znamená, že rozšiřuje komponentu TextBox1.

Pokud nyní stránku spustíme, uvidíme jen samotný TextBox. V případě, že jej začneme vyplňovat, objeví se pod ním kalendář. Jakmile textové pole opustíme, kalendář zmizí. Všimněte si, že se dají snadno vybírat i jednotlivé roky, stačí kliknout na rok v horním řádku, a dále je zde spousta “úžasných” animací. Funguje to, nicméně rozhodně ještě nejsme hotovi.

Kalendář pro výběr data

Validace správnosti zadání data

Abychom zajistili, že uživatel do pole zadá opravdu jenom datum, anebo nic, je třeba toto nějak ošetřit. Vzhledem k tomu, že chceme psát pěkně a nehodláme vymýšlet kolo, napíšeme si vlastní validátor. Nedávno se jeden vývojář vztekal, že s tím je moc práce a že si raději do stránky přidá Label, který v code behindu nastaví na chybovou hlášku nebo vymaže a že si to raději ošéfuje sám. Musel jsem se mu smát, protože to je daleko složitější a krkolomnější.

Validátory jsou standardním řešením, jak kontrolovat vstup uživatele. Podporují validační skupiny (validuje se jen někdy podle toho, s jakou částí stránky pracujeme), spolupracují s komponentami FormView (databázová operace se neprovede, pokud je obsah uvnitř nevalidní atd.), podporují klientskou validaci (aby se kvůli kontrole nemusel odesílat celý formulář) a mimo jiné můžeme snadno použít komponentu ValidationSummary, která na jednom místě vypíše, co vše je ve formuláři špatně.

Napsat validátor je velmi snadné. Od uživatele chceme, aby datum zadal v přesném formátu, díky čemuž nemůže dojít například k záměně měsíce a dne. Tento formát je nutno nastavit i extenderu, aby při kliknutí do kalendáře datum v tomto formátu vyplnil. Použijeme například formát d, což je výchozí formát pro datum (bez časového údaje) v závislosti na místním nastavení systému.

Validátor bude obyčejná třída dědící z BaseValidator, stačí přepsat metodu EvaluateIsValid. Pokud v textovém poli nebude zadáno nic, validátor chybu neohlásí. Pokud chceme vynutit, aby pole bylo nutně vyplněné, použijeme na něj RequiredFieldValidator, ten si ale každý už přidá sám.

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

namespace MyControls
{

/// <summary>
/// Validátor, který ověřuje správnost zadání data a času
/// </summary>
public class DateTimeValidator : BaseValidator
{

/// <summary>
/// Vrací nebo nastavuje řetězec určující formát data
/// </summary>
[
Category("Behavior"), DefaultValue("d"), Bindable(false)]
public string Format
{
get { return (string)ViewState["Format"] ?? "d"; }
set { ViewState["Format"] = value; }
}

/// <summary>
/// Ověří, zda-li je v poli vyplněno datum v požadovaném formátu
/// </summary>
protected override bool EvaluateIsValid()
{
// získat vyplněnou hodnotu
var value = GetControlValidationValue(ControlToValidate).Trim();

// pokud není vyplněno nic, je to v pořádku
if (string.IsNullOrEmpty(value)) return true;

// zkusit datum naparsovat a vrátit výsledek
DateTime d;
return DateTime.TryParseExact(value, Format, null, System.Globalization.DateTimeStyles.None, out d);
}

}

}

To je celý kód validátoru. Vidíme, že obsahuje pouze vlastnost Format, jejíž výchozí hodnota je d, a dále implementaci metody EvaluateIsValid. Vlastnost Format svou hodnotu ukládá do ViewState, je to běžný postup při vývoji komponent pro ASP.NET WebForms, kterému jsme se věnovali v minulých dílech tohoto seriálu.

Občas se najdou lidé, kteří by správnost data kontrolovali pomocí regulárních výrazů (a že jich je). To je bohužel dost častý a chybný přístup, jelikož správný regulární výraz, kterým by neprošly hodnoty typu 31.6.2010 nebo 37.18.1234 a který by správně uměl i přestupné roky se všemi výjimkami po sto a čtyřech stech letech, pokud se vůbec dá napsat, bude rozhodně velmi dlouhý a určitě se nevejde na obrazovku. Regulární výrazy nejsou samospasitelná univerzální náplast na všechno a rozhodně se nehodí k validaci data, parsování XML či HTML a podobným zrůdnostem, k nimž je mnozí vývojáři rádi zneužívají.

Datum ověřujeme metodou DateTime.TryParse, případně DateTime.TryParseExact, která bere jako parametr i konkrétní formát data. Tato metoda vrací True nebo False a do posledního parametru, pokud se to povede, uloží i rozparsované datum. To je ten správný způsob (a mimochodem kratší než regexpy), jak ověřit, zda-li je datum správně zadáno. Mohli bychom ještě přidat javascriptovou logiku, aby se správnost data dala ověřit přímo na klientovi ještě před odesláním formuláře, ale to v tomto díle řešit nebudeme.

Zaregistrujeme tedy do stránky jmenný prostor MyControls a přidáme k našim dvěma komponentám náš právě napsaný validátor. Mimochodem prohlédněte si vlastnosti, které deklaruje už sama třída BaseValidator a o které se při psaní vlastních validátorů nemusíte starat (např. ControlToValidate, ValidationGroup). Tohle vše nemusíte sledovat a řešíte jen konkrétní validaci. Jestli se má spouštět a kdy, to řeší třída BaseValidator.

 <%@ Page Language="C#" %>
<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="ajax" %>
<%@ Register Namespace="MyControls" TagPrefix="my" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>

<div>

<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<ajax:CalendarExtender ID="CalendarExtender1" runat="server" TargetControlID="TextBox1" />
<my:DateTimeValidator ID="DateTimeValidator1" runat="server" ControlToValidate="TextBox1">*</my:DateTimeValidator>

<asp:Button ID="Button1" runat="server" Text="Test" />
</div>
</form>
</body>
</html>

Pod naše tři komponenty jsem přidal ještě tlačítko, které nemá namapovanou žádnou událost – slouží jen k vyvolání postbacku. Pokud validace selže, vedle textového pole pro datum se objeví hvězdička.

Zapouzdření do komponenty

Pokud jde o snadné použití ve formulářích, např. v komponentách FormView, jsme víceméně za vodou. Pokaždé, kdy bychom chtěli nechat uživatele zadat datum, přidáme na dané místo tyto tři komponenty a je to. To je ale ovšem dost nepraktické, vždy bychom se měli snažit co nejvíce se vyvarovat opakujících se konstrukcí kódu (v rámci možností samozřejmě). Navíc by se nám hodila nějaká možnost snadno vytáhnout či nastavit datum jako hodnotu typu DateTime. Vlastnost Text komponenty TextBox se pro tento účel příliš nehodí.

Vytvoříme tedy třídu DateTimePicker, která bude obsahovat tyto tři komponenty a navenek bude mít kromě základních vlastností též vlastnosti SelectedDate, SelectedDateText a Format. První vlastnost bude vracet vybrané datum jako DateTime, druhá jako String (bude jen pro čtení, za chvíli uvidíme, proč se bude hodit) a poslední umožňuje nastavovat požadovaný formát data a času.

Dovnitř třídy vložme tento kód:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI.WebControls;
using System.Web.UI;
using AjaxControlToolkit;

namespace MyControls
{

/// <summary>
/// Komponenta, která umožňuje uživateli zadat datum a případně i čas
/// </summary>
public class DateTimePicker : CompositeControl
{
private TextBox textBox;
private CalendarExtender calendarExtender;
private DateTimeValidator validator;

/// <summary>
/// Vytvoří vnitřní komponenty
/// </summary>
protected override void CreateChildControls()
{
Controls.Clear();

textBox = new TextBox() { ID = "TextBox1" };
Controls.Add(textBox);

calendarExtender = new CalendarExtender() { ID = "CalendarExtender1", TargetControlID = "TextBox1" };
Controls.Add(calendarExtender);

validator = new DateTimeValidator() { ID = "DateTimeValidator1", ControlToValidate = "TextBox1", ErrorMessage = "*" };
Controls.Add(validator);
}


}

}

To je úplně základní kostra naší komponenty – dědíme z CompositeControl (tato třída se obvykle používá, pokud píšeme komponentu, jež je poskládána z několika již existujících komponent) a přepsali jsme metodu CreateChildControls, kde vytvoříme naše tři komponenty.

Nyní bychom rádi nadeklarovali výše uvedené tři vlastnosti, které jen přesměrujeme na vnitřní komponenty. Před přístupem k nim je ovšem nutné zavolat metodu EnsureChildControls, která zjistí, zda-li již byla zavolána metoda CreateChildControls a pokud ne, zavolá ji. Po zavolání EnsureChildControls tedy máme jistotu, že proměnné textBox, calendarExtender a validator budou již inicializované a přidané do kolekce Controls.

         /// <summary>
/// Vrací nebo nastavuje řetězec určující formát data
/// </summary>
[
Category("Behavior"), DefaultValue("d"), Bindable(false)]
public string Format
{
get
{
EnsureChildControls();
return calendarExtender.Format;
}
set
{
EnsureChildControls();
calendarExtender.Format = value;
validator.Format = value;
}
}

/// <summary>
/// Vrací nebo nastavuje vybrané datum
/// </summary>
[
Category("Data"), DefaultValue(null), Bindable(true, BindingDirection.TwoWay)]
public DateTime? SelectedDate
{
get
{
EnsureChildControls();
DateTime d;
if (DateTime.TryParseExact(textBox.Text, Format, null, System.Globalization.DateTimeStyles.None, out d))
return (DateTime?)d;
else
return null;
}
set
{
EnsureChildControls();
textBox.Text = (value == null) ? string.Empty : value.Value.ToString(Format);
}
}

/// <summary>
/// Vrací vybrané datum v textové podobě
/// </summary>
[
Browsable(false)]
public string SelectedDateString
{
get { return (SelectedDate == null) ? string.Empty : SelectedDate.Value.ToString(Format); }
}
První vlastnost jen zajistí, aby byly komponenty vytvořeny, a pak vrátí resp. nastaví hodnotu validátoru a extenderu.

Druhá vlastnost je typu DateTime?, tedy Nullable<DateTime> (datum nebo null). Pokud datum není zadáno, vrací tato vlastnost null, pokud adáno je, vrací příslušné datum. Opět parsujeme pomocí funkce DateTime.TryParseExact. Tady neřešíme, zda-li je datum zadáno správně či nikoliv, to má na starosti validátor. Pokud se povede naparsovat, vrátíme ho, pokud ne, je to jako kdyby zadáno vůbec nebylo.

Všimněme si ještě, že atribut Bindable má tentokráte dva argumenty – true, jako že je binding podporován, a BindingDirection.TwoWay, což je obousměrný databinding. Na tuto vlastnost tedy funguje jak vazba Eval, tak i vazba Bind. To se bude hodit při použití u datových zdrojů.

Třetí vlastnost jen zavolá tu druhou a převede výslednou hodnotu na string. Tuto vlastnost budeme potřebovat například kvůli RequiredFieldValidatoru, který by naše komponenta měla podporovat. Vzhledem k tomu, že validátor s komponentou spojujeme pomocí vlastnosti ControlToValidate, ale nikde nezadáváme vlastnost, která se bude validovat, je třeba toto nějak oznámit. Dělá se to atributem ValidationProperty, který přidáme naší třídě DateTimePicker a uvedeme v něm název vlastnosti, která obsahuje textovou reprezentaci hodnoty. Pokud je hodnota této vlastnosti prázdný řetězec, validace se nezdaří. Hodnotu této vlastnosti dostaneme ve třídě validátoru voláním metody GetControlValidationValue, které stačí předat ID komponenty.

     [ValidationProperty("SelectedDateString")]
public class DateTimePicker : CompositeControl
{

Vlastnosti definující vzhled

Komponenta by byla po funkční stránce téměř hotova, ale postrádá možnosti nastavení vzhledu. Přestože osobně nemám rád vlastnosti ForeColor, BorderWidth či HorizontalAlignment, kterými disponuje většina serverových komponent, jelikož tyto věci raději řeším v separátním CSS souboru, pro napsání opravdu kvalitní komponenty by se hodilo, aby tyto vlastnosti fungovaly. Minimálně jedna z nich se při použití ve formulářích hodí a její použití je dokonce i na místě – Width.

Jak tyto vlastnosti definující vzhled vlastně fungují? V ASP.NET všechny komponenty dědí ze třídy Control. To je základní třída, z níž dědí ty úplně nejjednodušší komponenty, jako např. Literal, který jen zobrazí předaný text, nic víc. Komponenty dědící přímo z Control žádné možnosti nastavení vzhledu nemají.

Z třídy Control dědí ale mimo jiné třída WebControl, která je určena pro složitější komponenty a přidává jim právě možnosti pro manipulaci se vzhledem. Základem je vlastnost ControlStyle, která drží objekt, který obsahuje vlastnosti BackColor, BorderColor, BorderStyle, BorderWidth, CssClass, Font, ForeColor, Height a Width. Tyto vlastnosti jsou deklarovány i v samotné třídě WebControl a jen se odkazují na příslušné vlastnosti z jejího ControlStyle. Kromě toho má každá komponenta dědící z WebControl ještě kolekci Style, která obsahuje všechny inline CSS styly, které jsme na ni aplikovali, např. následující konstrukcí.

<asp:CheckBox runat="server" style="background-color: Red" />

Naše komponenta pro zadávání data dědí z CompositeControl, což je třída dědící z WebControl. Máme již tedy deklarovány výše uvedené vlastnosti pro vzhled. Jak se ale naše komponenta vyrenderuje na výstup? Schválně se můžeme podívat a vyzkoušet to. Pokud ji zapíšeme jen s deklarovaným ID a runat=”server”, výsledkem bude toto.

 <span id="DateTimePicker1">
    
<input name="DateTimePicker1$TextBox1" type="text" id="DateTimePicker1_TextBox1" />
    
<span id="DateTimePicker1_DateTimeValidator1" style="visibility:hidden;">*</span>
</span>

Vidíme, že celá naše komponenta je obklíčena elementem span (to se dá změnit přepsáním vlastnosti TagKey). Uvnitř je textové pole renderované jako tag input a validátor jako další span.

V okamžiku, kdy ale naší komponentě nastavíme například Width=”250px”, budeme mít problém. Vlastnost se totiž aplikuje na kořenový element span a ne na prvek input, takže se šířka textového pole (o kterou nám pochopitelně jde) nezmění a zůstane na výchozí hodnotě.

 <span id="DateTimePicker1" style="display: inline-block; width: 250px;">
<input name="DateTimePicker1$TextBox1" type="text" id="DateTimePicker1_TextBox1" />
<span id="DateTimePicker1_DateTimeValidator1" style="visibility: hidden;">*</span>
</span>

Jinak na této ukázce je také vidět, proč stylovací vlastnosti není příliš radno používat – generují totiž inline styly. Přestože spousta lidí tvrdí, že by se neměly používat vůbec, myslím si, že to je příliš extrémní názor a že je mnoho míst, kde mají své opodstatnění. Například složité formuláře, kde každé pole má být opravdu jinak široké a kde by bylo zbytečné vytvářet pro každé jedno pole CSS třídu. Navíc se to špatně dohledává v případě, že člověk chce provést změny. Je nutné při každém použití zvážit, jestli bude lepší mít tuto vlastnost jako inline styl, nebo jako CSS třídu v separátním souboru.

Otázkou ale zůstává, jak zajistit, aby se vlastnosti Width apod. přenesly na element input textového pole. Jednou z možností by bylo všech těchto 9 vlastností přepsat a delegovat nastavované hodnoty na textové pole, to ale není příliš elegantní řešení a když nám někdo sáhne do ControlStyle (což je vlastnost, kterou přepsat nemůžeme), tak se celý mechanismus rozbije. 

Řešením je těsně před renderováním vnějšího elementu span hodnoty styl přesunout z rodičovské komponenty DateTimePicker na dceřinný TextBox. Vzhledem k tomu, že v době renderování se již ViewState nesleduje a změny vlastností, které při renderování uděláte, se již do ViewState nezapíšou, je toto řešení vhodné. Pokud totiž třeba vlastnost Width nastavíme na DateTimePickeru v nějakou rozumnou dobu, třeba v OnLoad, uloží se tato změna do ViewState komponenty DateTimePicker. Při renderování se sice tato vlastnost přesune na TextBox (který ji nastavenou neměl a ve ViewState o ní žádnou informaci nemá) a vyresetuje (aby se nevygeneroval inline styl i pro element span), ve ViewState se ale nic již měnit nebude, a tak při následujícím postbacku hodnotu vlastnosti Width najdeme tam, kde jsme ji nechali. Přitom se ale vyrenderuje do komponenty TextBox.

Jak tuto maškarádu udělat? Vhodným kadidátem je například metoda AddAttributesToRender, která bere právě vlastnosti v ControlStyle a Style a přidá je metodami AddAttribute a AddStyleAttribute do HtmlTextWriteru, který se stará o generování výsledného HTML. Těsně před touto metodou tedy vlastnosti přesuneme na TextBox (který se renderuje až potom).

         /// <summary>
/// Přenese vlastnosti stylů komponenty na vnitřní textové pole
/// </summary>
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
// přenést ControlStyle
textBox.ControlStyle.MergeWith(ControlStyle);
ControlStyle.Reset();

// přenést Style
foreach (string style in Style.Keys)
textBox.Style.Add(style, Style[style]);
Style.Clear();

base.AddAttributesToRender(writer);
}

Tady se musím přiznat, že tuto metodu jsem vymyslel a pevně doufám, že je správná. Nikde jsem nebyl schopen najít nějakou ukázku, která by toto delegování vlastností stylů na vnitřní komponenty řešila, kromě jedné, která mi přišla hloupá, a to je přepsání oněch 9 vlastností. Pokud máte lepší způsob než tento, rád se přiučím. Nicméně toto řešení funguje a nevidím na něm žádné závažné problémy.

Další vlastnosti

Vzhledem k tomu, že naše komponenta je jen trochu vylepšený TextBox, občas bychom ocenili mít přístup k některým vlastnostem původního textového pole. Mám na mysli zejména vlastnosti AutoPostBack pro automatické odeslání stránky v případě změny hodnoty uživatelem, dále ValidationGroup, kterou je třeba nastavit textovému poli i validátoru, aby se validace prováděla jen ve chvíli, kdy je třeba, dále Enabled a ReadOnly a ještě pár dalších.

Ty jistě zvládnete doimplementovat sami, v getteru jen vrátíte hodnotu přísluěné vlastnosti vnitřní komponenty a v setteru jen přiřazenou hodnotu přihrajete vnitřní komponentě. Stačí to udělat podle stejné šablony, jakou má vlastnost Format.

Závěrem

Možná si říkáte, když celá tato komponenta je jen vylepšený TextBox, neměli jsem dědit z přímo něj? Ušetřili bychom si tím některé problémy, například přesouvání stylu na potomka a duplikaci vlastností z předchozí podkapitoly, což je jistě pravda. Bohužel by nám tím zůstala například vlastnost Text, díky níž bychom dovnitř komponenty mohli podvrhnout libovolnou hodnotu (teď to prostě nejde), dále by bylo možné například vlastností TextMode přepnout pole do režimu více řádků či zadávání hesla, což při veškeré možné úctě u naší komponenty nedává smysl.

Pokud dědíme nějakou třídu, není žádná rozumná možnost, jak vlastnosti, které se nám nehodí, prostě a jednoduše. Měli bychom tedy vždy dědit z třídy, která obsahuje největší množinu deklarovaných členů, které potřebujeme, ale nic navíc. Řešení, že to, co se nám nehodí, přepíšeme (pokud to vůbec jde) a budeme vyhazovat výjimku NotSupportedException není právě pěkné a hodí se jen výjimečně.

Místo toho je lepší komponentu TextBox spolu s ostatními zapouzdřit dovnitř jedné kompozitní komponenty a zpřístupnit jen ven vlastnosti, které má smysl nastavovat.

Zde je kód celé komponenty, abyste jej mohli snadno použít. Máte-li otázky, přání, náměty či připomínky, napište do komentářů.

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

namespace MyControls
{

/// <summary>
/// Komponenta, která umožňuje uživateli zadat datum a případně i čas
/// </summary>
[
ValidationProperty("SelectedDateString")]
public class DateTimePicker : CompositeControl
{
private TextBox textBox;
private CalendarExtender calendarExtender;
private DateTimeValidator validator;

/// <summary>
/// Vrací nebo nastavuje řetězec určující formát data
/// </summary>
[
Category("Behavior"), DefaultValue("d"), Bindable(false)]
public string Format
{
get
{
EnsureChildControls();
return calendarExtender.Format;
}
set
{
EnsureChildControls();
calendarExtender.Format = value;
validator.Format = value;
}
}

/// <summary>
/// Vrací nebo nastavuje vybrané datum
/// </summary>
[
Category("Data"), DefaultValue(null), Bindable(true, BindingDirection.TwoWay)]
public DateTime? SelectedDate
{
get
{
EnsureChildControls();
DateTime d;
if (DateTime.TryParseExact(textBox.Text, Format, null, System.Globalization.DateTimeStyles.None, out d))
return (DateTime?)d;
else
return null;
}
set
{
EnsureChildControls();
textBox.Text = (value == null) ? string.Empty : value.Value.ToString(Format);
}
}

/// <summary>
/// Vrací vybrané datum v textové podobě
/// </summary>
[
Browsable(false)]
public string SelectedDateString
{
get { return (SelectedDate == null) ? string.Empty : SelectedDate.Value.ToString(Format); }
}

/// <summary>
/// Vrací nebo nastavuje hodnotu, která určuje, zda-li se po změně hodnoty uživatelem má provést postback
/// </summary>
[
Category("Behavior"), DefaultValue(false), Bindable(false)]
public bool AutoPostBack
{
get
{
EnsureChildControls();
return textBox.AutoPostBack;
}
set
{
EnsureChildControls();
textBox.AutoPostBack = value;
}
}

/// <summary>
/// Vrací nebo nastavuje hodnotu, která určuje, zda-li je komponenta odemknutá pro úpravy
/// </summary>
[
Category("Behavior"), DefaultValue(true), Bindable(false)]
public override bool Enabled
{
get
{
EnsureChildControls();
return textBox.Enabled;
}
set
{
EnsureChildControls();
textBox.Enabled = value;
}
}

/// <summary>
/// Vrací nebo nastavuje chybovou hlášku validátoru v případě, že datum nemá požadovaný formát
/// </summary>
[
Category("Validation"), DefaultValue("*"), Bindable(false), Localizable(true)]
public string DateFormatErrorMessage
{
get
{
EnsureChildControls();
return validator.ErrorMessage;
}
set
{
EnsureChildControls();
validator.ErrorMessage = value;
}
}

/// <summary>
/// Vrací nebo nastavuje validační skupinu
/// </summary>
[
Category("Validation"), DefaultValue(""), Bindable(false)]
public string ValidationGroup
{
get
{
EnsureChildControls();
return validator.ValidationGroup;
}
set
{
EnsureChildControls();
validator.ValidationGroup = value;
textBox.ValidationGroup = value;
}
}

/// <summary>
/// Vytvoří vnitřní komponenty
/// </summary>
protected override void CreateChildControls()
{
Controls.Clear();

textBox = new TextBox() { ID = "TextBox1" };
Controls.Add(textBox);

calendarExtender = new CalendarExtender() { ID = "CalendarExtender1", TargetControlID = "TextBox1", Format = "d" };
Controls.Add(calendarExtender);

validator = new DateTimeValidator() { ID = "DateTimeValidator1", ControlToValidate = "TextBox1", ErrorMessage = "*" };
Controls.Add(validator);
}

/// <summary>
/// Přenese vlastnosti stylů komponenty na vnitřní textové pole
/// </summary>
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
// přenést ControlStyle
textBox.ControlStyle.MergeWith(ControlStyle);
ControlStyle.Reset();

// přenést Style
foreach (string style in Style.Keys)
textBox.Style.Add(style, Style[style]);
Style.Clear();

base.AddAttributesToRender(writer);
}

}

}

 

hodnocení článku

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

 

Všechny díly tohoto seriálu

 

Mohlo by vás také zajímat

LINQ a Entity Framework - díl 3.: LINQ - Rozhraní Ienumerable, iqueryable a yield return

Co se to děje s .NETem

Jednoduchý scheduler v .NETu

Asi to znáte – máte nějaký složitější systém na zpracování velkého objemu dat a čas od času potřebujete vykovat nějakou automatizovanou údržbu – typicky smazat všechny položky starší než několika dní. Možností, jak toho dosáhnout, je hodně. Snažil jsem se vymyslet něco jednoduchého a efektivního.

 

 

Nový příspěvek

 

Diskuse: Komponenta pro zadávání data a času

Dobrý den,

ovládací prvek jsem nezkoušel, nicméně jak se na něj tak dívám, napadla mě jedna věc.

Atribut [ValidationProperty("SelectedDateString")] určí název vlastnosti prvku, která bude validována. Myslím si ale, že pouze na serveru. Jak zajistit také validaci na klientovi? Asi je potřeba někde nějak říci, jakou hodnotu jakého HTML prvku validovat. Nevíte, jak na to? Děkuji.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Na to je potřeba mít napsanou javascriptovou funkci, která to datum zvaliduje. Já jsem na to kdysi používal nějakou javascriptovou knihovnu Globalization od Microsoftu, mělo to i plugin do jQuery.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Díky.

Nešlo mi ani tak o validaci data javascriptem (která je jistě peklo ... možná ta knihovna pomůže), ale spíše o obecnou klientskou validaci vlastních controlů. Zkusím triviální příklad, dejme tomu, že jde o CompositeControl obsahující pouze jeden TextBox. Control bude obsahovat vlastnost Text, která pouze vrací vlastnost Text "vnitřního" Textboxu. Controlu dám atribut [ValidationProperty("Text")].

Co se stane když tento control umístím někam do stránky a přidám za něj např. validátor RequiredFieldValidator ControlToValidate="mujControl" ?

Je jisté, že validace na serveru proběhne OK. Ale (asi) neproběhne validace na klientovi. Kdybych ale validovat standardní server control TextBox, validace na klientovi by proběhla (pokud by nebyla explicitně zakázána).

Nemám teď možnost to vyzkoušet, jde jen o myšlenkové pochody, omlouvám se, jestli zdržuji ... :-)

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Podívejte se na seriál o vývoji vlastních komponent - http://www.webforms.cz/Pokrocili.aspx. Tam tu klientskou validaci mám, je to jeden z prvních dílů.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Ok, děkuji.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Nedaří se nastavit vlastnost Enabled komponenty LinkButton

Co je špatně? Zkoušel jsem: Virtual / Protected

Přesto že mám natvrdo Enabled==False (tady v kódu)

tak je myLinkButton stále přístupný


    public class myLinkButton :LinkButton
    {

        private bool _Enabled;

        public virtual bool Enabled
        {
            get { return false; }
            set { }
        }
    }

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Diskuse: Komponenta pro zadávání data a času

Dobrý den,

testuji komponentu DateTimePicker. Je super, ale hází mi to chybu InvalidCastException, když hodnota načtená z databáze je null.

Bindování je nastevno takto:

 <my:DateTimePicker ID="termin2DTP" runat="server" SelectedDate='<%# Bind("termin2") %>'  />

termin2 je z SQLDataSource a v dané větě je null.

Hláška je:

System.InvalidCastException was unhandled by user code
  Message=Specified cast is not valid.
  Source=App_Web_5nw0gzwt
  StackTrace:
       at ASP.ukol_aspx.__DataBinding__control21(Object sender, EventArgs e) in .....aspx:line 171
       at System.Web.UI.Control.OnDataBinding(EventArgs e)
       at System.Web.UI.WebControls.CompositeControl.DataBind()
       at System.Web.UI.Control.DataBindChildren()
  InnerException: 

Podle zdrojáku se mi zdá, že vkládání null hodnot je tam ošetřeno. Nevíte co s tím? Děkuji. Zdeněk

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Jasně, SqlDataSource totiž místo null strká DbNull.Value. Řešením je, pokud SqlDataSource chcete používat, je přidat vlastnost SelectedValue například typu Object, které dáte stejné atributy, a která převede v getteru DbNull.Value na null a v setteru zase null na DbNull.Value.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

OK, děkuji. Pokusím se o to. ZK

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Takže závěr je následující: Po přidání tohoto kódu a bind na SelectedValue se zdá, že je to OK. Děkuji.

[Category("Data"), DefaultValue(null), Bindable(true, BindingDirection.TwoWay)]
public Object SelectedValue
        {
            get
            {
                EnsureChildControls();
                DateTime d;
                if (DateTime.TryParseExact(textBox.Text, Format, null, System.Globalization.DateTimeStyles.None, out d))
                    return d;
                else
                    return null;
            }
            set
            {
                EnsureChildControls();
                textBox.Text = (value == DBNull.Value ) ? string.Empty : ((DateTime)value).ToString(Format);
            }
        }

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Diskuse: Komponenta pro zadávání data a času

Ako mam pouzit DateTimePicker vo Formulari.

Ked Texbox mam nabindovany napr.

<asp:TextBox ID="ZaciatokTextBox" runat="server" 
                Text='<%# Bind("Zaciatok") %>' Width="70px" />

Neviem pripojit polozku "Zaciatok" k DateTimePicker-u.

Vdaka

nahlásit spamnahlásit spam 0 odpovědětodpovědět

To je přeci jasné z kódu:

<cc:DateTimePicker ID="blabla" runat="server" SelectedDate='<%# Bind("blabla") #>' />

A komponentu musíte zaregistrovat nahoře na stránce nebo ve web.configu.

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