Monday, March 21, 2011
Prezentace Moderní trendy ve vývoji aplikací
Přibližně před rokem jsem u dvou firem začínal sérii technologických kurzů subjektivním shrnutím změn (nejen) v aplikacích psaných v .Net Frameworku. Nedávno jsme ji s kolegou náhodou otevřeli a pobavili jsme se nad tím, jak je rok v IT stále dlouhá doba a že zde dvojnásobně platí “tempus fugit”. Napadlo mě, že se nad prezentací možná se pobaví i někdo další, hlavně v pasážích, kde jemně naznačuju zálibu Microsoftu v zařezávání technologií.
U prezentace je třeba mít na paměti:
- Jedná se jen o osnovu “přehledové“ a cca dvouhodinové přednášky.
- Témata, typy projektů a technologie jsou v přednášce voleny podle zájmu zákazníka.
- Snažil jsem se nebýt hned v této úvodní přednášce příliš ostrý a konfliktní.
- Zvolená témata se týkala oblastí, které jsme v dalších dnech probíraly detailněji na konkrétních projektech vytvořených na návazných kurzech. Po pár zkušenostech si myslím, že jediný smyslupný kurz zabývající se technologií či programovacím jazykem je ten, na kterém píšete před účastníky kód. Tato přednáška byla koncipována jako motivační úvod k dalším tématům.
Monday, March 21, 2011 1:11:41 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET | C# | Compact .Net Framework | LINQ | RX Extensions | Silverlight | Web Services | Windows Forms | WP7
Friday, May 9, 2008
LINQ II - přetypovávání i vnořených anonymních datových typů z jiné assembly
V předchozím spotu jsem byl schopen pracovat s anonymními datovými typy, i když byly dotazy a výsledné sady dat vytvořeny v jiné assembly. Odstranění vrozené xenofobie v praxi.:)
Náš kód ale vygeneruje výjimku, jestliže anonymní datový typ z jiné assembly obsahuje další vnořené anonymní datové typy jako v následujícím upraveném příkladu. Vlastnost InnerAT vrací další anonymní datový typ, který pro zajímavost obsahuje odkaz ještě na další anonymní datový typ.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LINQTEST
{
public class TestAT
{
public static object GetResult()
{
string[] rows = { "Toyota", "Lexus", "Audi" };
var test = from row in rows
select new
{
FirstLetter = row[0],
Index = 110,
Original = row,
InnerAT = new { X = row[1], B = new {A=1}}
};
return test;
}
}
}
Řešení spočívá v úpravě extenzí a to tak, že přidáme privátní metodu GetTypeInstance a přeneseme do ní většinu kódu z extenze ToAnonymousType. Metoda GetTypeInstance při neshodě datového typu očekávaného parametrem "našeho - v naší assembly dostupného" konstruktoru anonymního datového typu a datového typu vlastnosti anonymního datového typu z "cizí" assembly rekurzivně přenese data z "cizího" anonymního datového typu do "našeho".
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Collections;
using LINQTEST;
namespace LINQAnonymous
{
/// <summary>
/// Rozšíření pro LINQ
/// </summary>
static class RSLinqExtensions
{
/// <summary>
/// Metoda přetypuje objekt na anonymní typ, jehož struktura byla předána v parametru <paramref name="prototype"/>
/// </summary>
/// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
/// <param name="prototype">Prototyp se strukturou anonymního typu</param>
/// <returns>Instanci anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
/// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
public static T ToAnonymousType<T>(this object obj, T prototype)
where T: class
{
T atiObj = obj as T;
if (atiObj == null)
{
atiObj = GetTypeInstance(obj, prototype.GetType()) as T;
}
return (atiObj);
}
private static object GetTypeInstance(object obj, Type expected)
{
object atiObj = null;
ConstructorInfo constructorInfo = expected.GetConstructors()[0];
if (constructorInfo == null)
{
return null;
}
ParameterInfo[] paramInfos = constructorInfo.GetParameters();
PropertyInfo[] origProperties = obj.GetType().GetProperties();
if (paramInfos.Count() != origProperties.Count())
{
return null;
}
object[] paramArgs = new object[paramInfos.Count()];
for (int i = 0; i < paramArgs.Length; i++)
{
PropertyInfo origProperty = origProperties.Where(prop => prop.Name == paramInfos[i].Name).FirstOrDefault();
if (origProperty == null)
{
return null;
}
object val = origProperty.GetValue(obj, null);
if (origProperty.PropertyType != paramInfos[i].ParameterType)
{
val = GetTypeInstance(val, paramInfos[i].ParameterType);
}
paramArgs[i] = val;
}
atiObj = constructorInfo.Invoke(paramArgs);
return atiObj;
}
/// <summary>
/// Metoda vrátí
/// </summary>
/// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
/// <param name="prototype">Prototyp se strukturou anonymního typu</param>
/// <returns>List instancí anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
/// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
public static List<T> CastToList<T>(this object obj, T prototype)
where T : class
{
List<T> list = new List<T>();
IEnumerable<T> enumerable = obj as IEnumerable<T>;
if (enumerable != null)
{
list.AddRange(enumerable);
}
else
{
IEnumerable enumObjects = obj as IEnumerable;
if (enumObjects == null)
{
return null;
}
foreach (object enumObject in enumObjects)
{
T currObject = ToAnonymousType(enumObject, prototype);
if (currObject == null)
{
//K čistění listu by neměl být důvod, ale garantujeme, že nevrátíme částečně naplněný list
list.Clear();
return list;
}
list.Add(currObject);
}
}
return list;
}
}
Při přetypovávání stačí stále jen zadat prototyp anonymního datové typu.
//Anonymní typ z jiné assembly!
var result2 = TestAT.GetResult().CastToList(new {FirstLetter = default(char),
Index =default(int),
Original = default(string),
InnerAT = new { X = default(char), B = new { A = default(int) } }
})
;
foreach (var res in result2)
{
Console.WriteLine(res.FirstLetter);
Console.WriteLine(res.Original);
}
Console.WriteLine(TestAT.
GetResult().
CastToList(new
{
FirstLetter = default(char),
Index = default(int),
Original = default(string),
InnerAT = new { X = default(char), B = new { A =default(int)} }
}
).
Where(car => car.FirstLetter == 'T')
.FirstOrDefault()
.ToString());
Console.ReadLine();
Friday, May 9, 2008 9:09:26 AM (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET | Compact .Net Framework | LINQ | Windows Forms
Thursday, May 8, 2008
LINQ - anonymní typ deklarovaný v jedné assembly dostupný v metodách další assembly?
Anonymní datové typy v LINQu nelze použít jako návratový typ z metody a jediný způsob, jak anonymní typ z metody předat, je použít jako návratovou hodnotu typ object, protože v .Net Frameworku - jak je všeobecně známo - všechny třídy přímo či nepřímo dědí z třídy Object. Navíc platí, že anonymní typ je kompilátorem vždy deklarován jako internal a jeho použití je tak striktně omezeno na jednu assembly.
Jde o rozumné omezení a anonymní datové typy bychom neměli zneužívat k nesmyslům typu "hezká syntaxe pro generování objektů Dictionary", které si našly cestu i do připravovaného (a už dnes "přehypovaného") MVC frameworku pro ASP.NET.
V různých diskuzích se ale stále dokola objevuje dotaz, jak anonymní typ z metody vráti. A každé omezení se dá samozřejmě obejít - když nefunguje ani bodový systém na silnicích, proč nenajít hrubý trik ve stylu "osoby blízké" i pro erozi různých omezení u anonymního datového typu. :) Znovu alibisticky varuji všechny před zařazením následujících nehezkých triků do svého arzenálu běžných postupů při vývoji, protože všechny postupy spoléhají na chování kompilátoru C#, které není garantováno a které se může v další verzi nebo i jen při vydání service packu .Net Frameworku bez varování změnit.
Pro vrácení anonymního datového typu z metody použijeme hezký hack od Tomáše, který se ujal pod názvem "Cast By Example". Zjednodušeně řečeno - sice nemůžeme používat při přetypovávání názvy anonymních datových typů (tříd), protože anonymní datové typy jsou generovány až při kompilaci, ale můžeme kompilátoru dát při přetypování "vzor", jaký anonymní datový typ nám bude vyhovovat. Podrobnosti si můžete najít v odkazovaném článku Tomáše Petříčka = zde jen připomenu, že technika využívá současného chování kompilátoru, který pro různé deklarace anonymních datových typů se stejnými vlastnostmi generuje v jedné assembly vždy právě jednu třídu.
Napsal jsem jednoduše použitelné extenze, které vám dovolí nejen přetypovat jednu instanci "object" na anonymní datový typ, ale můžete přetypovat množiny záznamů na (anonymně ) typovou kolekci List<NějakýAnonymniTyp>, a dokonce je možné jednoduše použít anonymní datové typy z jiné assembly.
/// <summary>
/// Rozšíření pro LINQ
/// </summary>
static class RSLinqExtensions
{
/// <summary>
/// Metoda přetypuje objekt na anonymní typ, jehož struktura byla předána v parametru <paramref name="prototype"/>
/// </summary>
/// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
/// <param name="prototype">Prototyp se strukturou anonymního typu</param>
/// <returns>Instanci anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
/// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
public static T ToAnonymousType<T>(this object obj, T prototype)
where T: class
{
T atiObj = obj as T;
if (atiObj == null)
{
ConstructorInfo constructorInfo = typeof(T).GetConstructors()[0];
if (constructorInfo == null)
{
return null;
}
ParameterInfo[] paramInfos = constructorInfo.GetParameters();
PropertyInfo[] origProperties = obj.GetType().GetProperties();
if (paramInfos.Count() != origProperties.Count())
{
return null;
}
object[] paramArgs = new object[paramInfos.Count()];
for (int i = 0; i < paramArgs.Length; i++)
{
PropertyInfo origProperty = origProperties.Where(prop => prop.Name == paramInfos[i].Name).FirstOrDefault();
if (origProperty == null)
{
return null;
}
paramArgs[i] = origProperty.GetValue(obj, null);
}
atiObj = constructorInfo.Invoke(paramArgs) as T;
}
return (atiObj);
}
/// <summary>
/// Metoda vrátí
/// </summary>
/// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
/// <param name="prototype">Prototyp se strukturou anonymního typu</param>
/// <returns>List instancí anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
/// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
public static List<T> CastToList<T>(this object obj, T prototype)
where T : class
{
List<T> list = new List<T>();
IEnumerable<T> enumerable = obj as IEnumerable<T>;
if (enumerable != null)
{
list.AddRange(enumerable);
}
else
{
IEnumerable enumObjects = obj as IEnumerable;
if (enumObjects == null)
{
return null;
}
foreach (object enumObject in enumObjects)
{
T currObject = ToAnonymousType(enumObject, prototype);
if (currObject == null)
{
//K čistění listu by neměl být důvod, ale garantujeme, že nevrátíme částečně naplněný list
list.Clear();
return list;
}
list.Add(currObject);
}
}
return list;
}
}
Komentáře u metod by měly dostatečně popisovat funkci extenzí. Metoda ToAnonymousType předpokládá, že chcete přetypovat na instanci anonymního typu (např. při použití metody Single v LINQu), metoda CastToList pracuje s množinou (IEnumerable<T>) instancí anonymního datového typu. Většina kódu v obou metodách ošetřuje situaci, kdy pracujete s anonymním datovým typem z jiné (referencované) assembly, jehož data je potřeba přenést do instance anonymního datového typu v aktuální assembly.
Použití extenzí - nejprve u anonymního datového typu deklarovaného v assembly, ve které je také náš LINQ dotaz.
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Collections;
using LINQTEST;
class Program
{
//Anonymní typ deklarovaný v této (exe) assembly
private static object GetLetters()
{
string[] names = {"Rene", "Petra", "Kamilka"};
var test = from name in names
select new {FirstLetter = name[0], Index=1};
return test;
}
static void Main(string[] args)
{
var result = GetLetters().CastToList(new {FirstLetter = default(char),
Index =default(int)}
);
foreach (var res in result)
{
Console.WriteLine(res.FirstLetter);
}
}
Metodě CastToList jsme predali "vzor" anonymího datového typu (new {FirstLetter = default(char), Index =default(int)}) a hodnoty vlastností jsme u prototypu inicializovali s využitím klíčového slova default. V metodě Main v cyklu foreach je funkční intellisense a můžeme pracovat zcela typově s proměnnou res. Jenom zdůrazním, že nyní žádná reflexe nebyla použita! Metoda CastToList s využitím automatické typové inference kompilátoru pouze zkopírovala prvky v IEnumerable<T> do našeho typového generického Listu.
if (enumerable != null)
{
list.AddRange(enumerable);
}
Reflexe je využita při konverzi anonymního typu deklarovaného v jiné assembly. Předpokládejme, že v jiné assembly nazvané např. LINQTest máme další metodu vracející množinu dat skrytou opět za obecným rozhraním "služebníka zcela neužitečného" neboli třídy object.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LINQTEST
{
public class TestAT
{
public static object GetResult()
{
string[] rows = { "Toyota", "Lexus", "Audi" };
var test = from row in rows
select new { FirstLetter = row[0],
Index=110,
Original = row
};
return test;
}
}
}
Zkompilovanou assembly LINQTest zareferencujeme v našem projektu. Kód pro práci s anonymní datovým typem v jiné assembly se z pohledu uživatele LINQ extenze nijak nezměnil od předchozího příkladu.
class Program
{
static void Main(string[] args)
{
//Anonymní typ z jiné assembly!
var result2 = TestAT.GetResult().CastToList(new {FirstLetter = default(char),
Index =default(int),
Original = default(string)}
);
foreach (var res in result2)
{
Console.WriteLine(res.FirstLetter);
Console.WriteLine(res.Original);
}
Console.WriteLine(TestAT.
GetResult().
CastToList(new
{
FirstLetter = default(char),
Index = default(int),
Original = default(string)
}).
Where(car => car.FirstLetter == 'T')
.FirstOrDefault()
.ToString());
Console.ReadLine();
}
}
Jak si můžete všimnout, po cyklu foreach si požádám o data z jiné assembly znovu a poté nad vrácenou typovou kolekci vytvořím další projekci. A ani mě nemusí zajímat, že se mi pod rukama zcela změnil typ používaných objektů.
Docela zábavná záležitost ne?
LINQ II - přetypovávání i vnořených anonymních datových typů z jiné assembly
Thursday, May 8, 2008 3:00:43 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET | Compact .Net Framework | LINQ | Windows Forms
Monday, October 9, 2006
ASP.NET - jednoduchý přístup z kódu na webovém formuláři k prvkům deklarovaným v šabloně
V ASP.NET 1.x jsme při přístupu k prvkům v šabloně (šablonou rozumíme vlastnost serverového ovládacího prvku typu ITemplate) museli zavolat metodu FindControl a předat ji Id hledaného prvku. A protože návratovou hodnotou metody FindControl je pouze rozhraní třídy Control, byli jsme nuceni přetypovávat na odvozený ovládací prvek.
Label lblMess = LoginControl1.FindControl("lblMessage") as Label;
U prvků DataList, Repeater a dalších, kteří používají šablony pro opakování stejného obsahu pro každý řádek v datovém zdroji (ItemTemplate, AlternateItemTemplate) je použití metody FindControl odůvodněné. Ovládací prvky v šabloně jsou vytvořeny opakovaně pro všechny řádky v datovém zdroji a rodičovský prvek šablony implementací rozhraní INamingContainer zajišťuje, že každá instance šablony je v html formuláři složena z html elementů s unikátními hierarchickými názvy. Metoda FindControl na řádku Datalistu, Repeateru si můžeme zjednodušeně představit jako "překladač" dlouhého, hierarchického a automaticky generovaného Id na jednoduché Id zadané v šabloně. Vidíme-li na stránce Id LoginControl1_ctl00_lblMessage, můžeme se k prvku s Id 'lblMessage' dostat tak, jak jsme si ukázali v kódu výše.
Proč si ale komplikovat život, když máme na stránce vždy maximálně jednu instanci šablony? Šablona prvku WizardStep bude na stránce právě jednou, protože šablonu jednoho kroku v průvodci (asp:wizard) nepoužíváme pro opakovanou instanciaci stejného obsahu, ale jen pro vytvoření vlastního vzhledu jednoho kroku průvodce. Šablonu nám prvek wizard nabízí jen proto, abychom si mohli vytvořit pěkné vlastní uživatelské rozhraní a nebyli sešněrováni ve svém tvůrčím rozletu představami ASP.NET týmu. Pak ale není žádný důvod, abychom k prvkům šablony přistupovali přes metodu FindControl. Prvky jsou na stránce pouze jednou a je bezpečné na ně odkazovat přímo na úrovni stránky jako na každý jiný ovládací prvek v ASP.NET formuláři, protože nehrozí kolize jejich Id.
ASP.NET 2.0 dovoluje u každé vlastnosti typu ITemplate určit, jestli bude šablona instanciována na jedné stránce opakovaně, anebo zda se na šablona stránce vyskytne nanejvýš jednou. Informaci o tom, jak budete šablonu používat, nese nový atribut TemplateInstance, kterým dekorujete vlastnost ITemplate.
[TemplateInstance(TemplateInstance.Single)]
Hodnotu Single z enumerace TemplateInstance ASP.NET interpretuje jako příkaz k vygenerování typových proměnných na úrovni stránky pro všechny ovládací prvky v šabloně.
Když atribut TemplateInstance nepoužijeme nebo zvolíme režim TemplateInstance.Multiple, k vygenerování proměnných nedojde a ASP.NET 2.0 se k prvkům k šabloně chová stejně jako ASP.NET 1.x.
Zde je jednoduchý serverový ovládací prvek LoginControl, který obsahuje dvě šablony - jednu pro anonymní uživatele a druhou, asi nepřekvapivě, pro přihlášené uživatele. Obě šablony jsou dekorovány atributem TemplateInstance s hodnotou TemplateInstance.Single - jako autoři ovládacího prvku víme, že určitě nebudeme používat více instancí jedné šablony.
namespace RStein.Web.UI.WebControls
{
/// <summary>
/// Serverový ovládací prvek, který dovoluje definovat odlišné šablony pro anonymního a přihlášeného uživatele
/// </summary>
[DefaultProperty("AnonymousUserMessage")]
[DefaultEvent("LoginRequest")]
[ToolboxData("<{0}:LoginControl runat=server></{0}:LoginControl>")]
public class LoginControl : CompositeControl
{
#region Delegates
public delegate void LoginItemCommandEventHandler (object sender, CommandEventArgs e);
#endregion Delegates
#region Public Constants
/// <summary>
/// popisek na tlačítko pro událost <see cref="LoginRequest"/>
/// </summary>
public const string LOGIN_BUTTON_NAME = "Login";
/// <summary>
/// Konstanta reprezentuje popisek na tlačítko pro událost <see cref="LogoutRequest"/>
/// </summary>
public const string LOGOUT_BUTTON_NAME = "Logout";
#endregion Public Constants
#region Events Keys
/// <summary>
/// Klíč události <see cref="LoginRequest"/>
/// </summary>
public static readonly Object LoginRequestKey = new Object();
/// <summary>
/// Klíč události <see cref="LoginRequest"/>
/// </summary>
public static readonly Object LogoutRequestKey = new Object();
/// <summary>
/// Klíč události <see cref="ItemCommand"/>
/// </summary>
public static readonly Object ItemCommandKey = new Object();
#endregion Events Keys
#region Public Events
/// <summary>
/// Událost 'Přihlásit uživatele' je vyvolána po kliknutuí na tlačítko s popiskem <see cref="LoginButtonName"/>
/// </summary>
public event EventHandler LoginRequest
{
add
{
Events.AddHandler(LoginRequestKey, value);
}
remove
{
Events.RemoveHandler(LoginRequestKey, value);
}
}
/// <summary>
/// Událost 'Odhlásit uživatele' je vyvolána po kliknutuí na tlačítko s popiskem <see cref="LogoutButtonName"/>
/// </summary>
public event EventHandler LogoutRequest
{
add
{
Events.AddHandler(LogoutRequestKey, value);
}
remove
{
Events.RemoveHandler(LogoutRequestKey, value);
}
}
/// <summary>
/// Událost zprostředkovává události vnořených ovládacích prvků
/// </summary>
public event LoginItemCommandEventHandler ItemCommand
{
add
{
Events.AddHandler(ItemCommandKey, value);
}
remove
{
Events.RemoveHandler(ItemCommandKey, value);
}
}
#endregion Public Events
#region Private variables
private LoginControlContent m_content;
private ITemplate m_anonymousTemplate;
private ITemplate m_loggedInTemplate;
#endregion Private variables
#region Contructors
/// <summary>
/// Konstruktor
/// </summary>
public LoginControl()
{
}
#endregion Contructors
#region Public properties
/// <summary>
/// Vlastnost dovoluje přistupovat k prvkům šablony přes FindControl
/// </summary>
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public LoginControlContent Content
{
get
{
EnsureChildControls();
return m_content;
}
}
/// <summary>
/// Šablona pro přihlášeného uživatele
/// </summary>
[Browsable(false)]
[DefaultValue(null)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(LoginControlContent))]
[TemplateInstance(TemplateInstance.Single)]
public ITemplate LoggedInTemplate
{
get
{
return m_loggedInTemplate;
}
set
{
m_loggedInTemplate = value;
}
}
/// <summary>
/// Šablona pro nepřihlášeného uživatele
/// </summary>
[Browsable(false)]
[DefaultValue(null)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(LoginControlContent))]
[TemplateInstance(TemplateInstance.Single)]
public ITemplate AnonymousTemplate
{
get
{
return m_anonymousTemplate;
}
set
{
m_anonymousTemplate = value;
}
}
/// <summary>
/// Informace zobrazená nepřihlášenému uživateli
/// </summary>
[Category("Behavior")]
[Bindable(true)]
[DefaultValue("")]
[Description("Informace zobrazená nepřihlášenému uživateli")]
public string AnonymousUserMessage
{
get
{
string message = (string) ViewState["AnonymousUserMessage"];
return (message == null ? String.Empty : message);
}
set
{
ViewState["AnonymousUserMessage"] = value;
}
}
/// <summary>
/// Informace zobrazená přihlášenému uživateli
/// </summary>
[Category("Behavior")]
[Bindable(true)]
[DefaultValue("")]
[Description("Informace zobrazená přihlášenému uživateli")]
public string LoggedInMessage
{
get
{
string message = (string) ViewState["LoggedInMessage"];
return (message == null ? String.Empty : message);
}
set
{
ViewState["LoggedInMessage"] = value;
}
}
#endregion Public properties
#region Protected properties
/// <summary>
/// Prvek bude uzavřen v HTML značce DIV
/// </summary>
protected override HtmlTextWriterTag TagKey
{
get
{
return HtmlTextWriterTag.Div;
}
}
#endregion Protected properties
#region Protected methods
/// <summary>
/// Vytvoření vnořených ovládacích prvků
/// </summary>
protected override void CreateChildControls()
{
Controls.Clear();
if (Page.User.Identity.IsAuthenticated)
{
m_content = new LoginControlContent(Page.User.Identity.Name, AnonymousUserMessage, LoggedInMessage);
ITemplate template = null;
if (m_loggedInTemplate != null)
{
template = m_loggedInTemplate;
}
else
{
template = new DefaultLoggedInTemplate();
}
template.InstantiateIn(m_content);
}
else
{
m_content = new LoginControlContent(String.Empty, AnonymousUserMessage, LoggedInMessage);
ITemplate template = null;
if (m_anonymousTemplate != null)
{
template = m_anonymousTemplate;
}
else
{
template = new DefaultAnonymousTemplate();
}
template.InstantiateIn(m_content);
}
this.Controls.Add(m_content);
}
/// <summary>
/// Přpsání metody, která zachycuje bublané události
/// </summary>
/// <param name="source">Zdroj bublané události</param>
/// <param name="args">Argumenty bublané události</param>
/// <returns><see cref="true"/> - Událost byla zpracována, <see cref="false"/> - Událost nebyla zpracována</returns>
protected override bool OnBubbleEvent(object source, EventArgs e)
{
CommandEventArgs ex = e as CommandEventArgs;
if (ex != null)
{
if (ex.CommandName.ToLower() == LOGIN_BUTTON_NAME.ToLower())
{
OnLoginRequest(new EventArgs());
}
else if (ex.CommandName.ToLower() == LOGOUT_BUTTON_NAME.ToLower())
{
OnLogoutRequest(new EventArgs());
}
else
{
OnItemCommand(ex);
}
return true;
}
return false;
}
/// <summary>
/// Metoda odpovědná za vyvolání události loginRequest
/// </summary>
/// <param name="e">Parametry události</param>
protected virtual void OnLoginRequest(EventArgs e)
{
EventHandler eh = (EventHandler) Events[LoginRequestKey];
if (eh != null)
{
eh(this, e);
}
}
/// <summary>
/// Metoda odpovědná za vyvolání události LogoutRequest
/// </summary>
/// <param name="e">Parametry události</param>
protected virtual void OnLogoutRequest(EventArgs e)
{
EventHandler eh = (EventHandler) Events[LogoutRequestKey];
if (eh != null)
{
eh(this, e);
}
}
/// <summary>
/// Metoda odpovědná za vyvolání události ItemCommand
/// </summary>
/// <param name="e">Parametry události</param>
protected virtual void OnItemCommand(CommandEventArgs e)
{
LoginItemCommandEventHandler eh = (LoginItemCommandEventHandler) Events[ItemCommandKey];
if (eh != null)
{
eh(this, e);
}
}
#endregion Protected methods
}
}
LoginControl použijeme na testovací stránce a zadáme anonymní šablonu.
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register Assembly="WebControlLibrary1" Namespace="RStein.Web.UI.WebControls"
TagPrefix="cc2" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Testovací stránka</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<cc2:LoginControl ID="LoginControl1" runat="server" AnonymousUserMessage="Přihlašte se prosím" OnLoginRequest="LoginControl1_LoginRequest">
<AnonymousTemplate>
<asp:label runat="server" ID="lblMessage"><%#Container.AnonymousMessage%> </asp:label> <br />
<asp:LinkButton Id="Login" runat="server" Text="Přihlásit se" />
</AnonymousTemplate>
</cc2:LoginControl>
<
</div>
</form>
</body>
</html>
Tlačítko Login i popisek lblMessage deklarované v šabloně jsou přímo dostupné ze stránky, jak si můžeme ověřit, když v události PreRender stránky změníme barvu pozadí popisku lblMessage. Nemusíme používat metodu FindControl, ani přetypovávat, což jsou docela příjemná vylepšení kódu ;)
private void Page_PreRender(object sender, EventArgs e)
{
LoginControl1.DataBind();
if (lblMessage != null)
{
lblMessage.BackColor = Color.Cyan;
}
}
Abyste mohli ovládací prvek LoginControl zkompilovat, následuje kód používaných pomocných tříd.
using System;
using System.Web.UI.WebControls;
using System.Web.UI;
using System.ComponentModel;
namespace RStein.Web.UI.WebControls
{
/// <summary>
/// Kontejner pro šablony
/// </summary>
[ToolboxItem(false)]
public class LoginControlContent : Control, INamingContainer
{
#region Private variables
private string m_userName;
private string m_anonymousMessage;
private string m_loggedInMessage;
#endregion Private variables
/// <summary>
/// Konstruktor
/// </summary>
/// <param name="userName">Jméno uživatele</param>
/// <param name="anonymousMessage">Zpráva pro nepřihlášeného uživatele</param>
/// <param name="loggedInMessage">Zpráva pro přihlášeného uživatele</param>
internal LoginControlContent(string userName, string anonymousMessage, string loggedInMessage)
{
m_userName = userName;
m_anonymousMessage = anonymousMessage;
m_loggedInMessage = loggedInMessage;
}
/// <summary>
/// Jméno uživatele
/// </summary>
public string UserName
{
get
{
return m_userName;
}
}
/// <summary>
///Zpráva pro nepřihlášeného uživatele
/// </summary>
public string AnonymousMessage
{
get
{
return m_anonymousMessage;
}
}
/// <summary>
///Zpráva pro přihlášeného uživatele
/// </summary>
public string LoggedInMessage
{
get
{
return m_loggedInMessage;
}
}
}
}
using System;
using System.Web.UI.WebControls;
using System.Web.UI;
namespace RStein.Web.UI.WebControls
{
/// <summary>
/// Standardní šablona pro nepřihlášeného uživatele
/// </summary>
public class DefaultAnonymousTemplate : ITemplate
{
#region Constructors
/// <summary>
/// Konstruktor
/// </summary>
internal DefaultAnonymousTemplate()
{
}
#endregion Constructors
#region public methods
#region ITemplate Members
/// <summary>
/// Implementace InstantiateIn - vytvoření ovládacích prvků anonymní šablony
/// </summary>
/// <param name="container">Ovládací prvek, do jehož kolekce Controls bzdou prvky šablony přidány</param>
public void InstantiateIn(Control container)
{
Label lblMessage = new Label();
lblMessage.ID = "lblMessage";
lblMessage.DataBinding += new EventHandler(lblMesssage_DataBind);
LiteralControl separator = new LiteralControl(" ");
LinkButton btnLogin = new LinkButton();
btnLogin.ID = "btnLogin";
btnLogin.Text = "Přihlásit";
btnLogin.CommandName = LoginControl.LOGIN_BUTTON_NAME;
container.Controls.Add(lblMessage);
container.Controls.Add(separator);
container.Controls.Add(btnLogin);
}
#endregion ITemplate Members
#endregion public methods
#region private methods
//Handler události DataBind prvku lblMessage
private void lblMesssage_DataBind(object sender, EventArgs e)
{
Label lblMessage = (Label) sender;
LoginControlContent currContent = lblMessage.NamingContainer as LoginControlContent;
lblMessage.Text = currContent.AnonymousMessage;
}
#endregion private methods
}
}
using System;
using System.Web.UI.WebControls;
using System.Web.UI;
namespace RStein.Web.UI.WebControls
{
/// <summary>
/// Standardní šablona pro přihlášeného uživatele
/// </summary>
public class DefaultLoggedInTemplate : ITemplate
{
#region constructors
/// <summary>
/// Konstruktor
/// </summary>
internal DefaultLoggedInTemplate()
{
}
#endregion constructors
#region public methods
#region ITemplate Members
/// <summary>
/// Implementace InstantiateIn - vytvoření ovládacích prvků anonymní šablony
/// </summary>
/// <param name="container">Ovládací prvek, do jehož kolekce Controls bzdou prvky šablony přidány</param>
public void InstantiateIn(Control container)
{
Label lblMessage = new Label();
lblMessage.ID = "lblMessage";
lblMessage.DataBinding += new EventHandler(lblMesssage_DataBind);
LiteralControl separator = new LiteralControl(" ");
LinkButton btnLogout = new LinkButton();
btnLogout.ID = "btnLogout";
btnLogout.Text = "Odhlásit";
btnLogout.CommandName = LoginControl.LOGOUT_BUTTON_NAME;
container.Controls.Add(lblMessage);
container.Controls.Add(separator);
container.Controls.Add(btnLogout);
}
#endregion ITemplate Members
#endregion public methods
#region private methods
//Handler události DataBind prvku lblMessage
private void lblMesssage_DataBind(object sender, EventArgs e)
{
Label lblMessage = (Label) sender;
LoginControlContent currContent = lblMessage.NamingContainer as LoginControlContent;
lblMessage.Text = currContent.LoggedInMessage + " " + currContent.UserName;
}
#endregion private methods
}
}
Monday, October 9, 2006 3:42:53 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET
Thursday, July 20, 2006
Sloupec v GridView s potvrzením vymazání záznamu (na klientovi) II - sloupec se šablonou
V prvním spotu jsem ukázal, jak napsat pro GridView nový sloupec zobrazující tlačítko pro vymazání záznamu. Po stisknutí tlačítka se zobrazilo okno vytvořené v Javascriptu, ve kterém byl uživatel požádán o potvrzení vymazání záznamu. Náš nový sloupec byl potomkem všem důvěrně známého sloupce ButtonField. Jak jsme ale viděli v kódu třídy DeleteButtonColumn, neobešli jsme se bez několika ne úplně čistých praktik.
Stejný sloupec se dá také vytvořit jako potomek třídy TemplateField. Místo deklarativní šablony na stránce ale vytvoříme vlastná šablonu pro náš nový sloupec implemetací rozhraní ITemplate. Jak se asi shodneme, šablony jsou jedna z nejlepších věcí v ASP.NET již od verze 1.0 - místo toho, aby nám tvůrci ASP.NET diktovali, jak a z čeho musejí být ASP.NET prvky vystavěny, můžeme v šablonách dodat vlastní obsah ASP.NET prvku či jedné jeho vlastnosti a přitom si stále užívat všech výhod událostního modelu i předdefinovaného chování ASP.NET komponenty.
Nejprve kód našeho nového sloupce pro GridView:
namespace RStein.Web.UI
{
/// <summary>
/// Nový typ sloupce pro GridView obsahující tlačítko pro smazání záznamu s potvrzením na klientovi (JS)
/// </summary>
public class DeleteButtonField2 : TemplateField
{
#region Public constants
/// <summary>
/// Id vygenerovaného tlačítka
/// </summary>
public const string DELETE_BUTTON_ID = "DeleteButton";
#endregion Public constants
#region Private constants
public const string DELETE_COMMAND = "Delete";
public const string DEFAULT_BUTTON_TEXT = "Smazat";
public const string DEFAULT_CONFIRM_MESSAGE = "Opravdu smazat záznam?";
public const string DELETE_JS = "javascript: if (!confirm('{0}')) return false;";
#endregion Private constants
#region Private fields
private DeleteButtonTemplate m_currTemplate;
#endregion Private fields
#region Constructors
/// <summary>
/// Konstruktor
/// </summary>
public DeleteButtonField2()
{
m_currTemplate = new DeleteButtonTemplate(this);
ItemTemplate = m_currTemplate;
AlternatingItemTemplate = m_currTemplate;
EditItemTemplate = m_currTemplate;
}
#endregion Constructors
#region Public properties
/// <summary>
/// Typ tlačítka (odkaz-LinkButton, Běžné tlačítko-Button)
/// </summary>
/// <exception cref="NotSupportedEception">Pokus o nastavení vlastnosti ButtonType na nepodporovaný typ Image</exception>
public ButtonType ButtonType
{
get
{
object buttType = ViewState["ButtonType"];
if (buttType == null)
{
return ButtonType.Button;
}
return ((ButtonType)buttType);
}
set
{
if (value == ButtonType.Image)
{
throw new NotSupportedException("Image button is currently not supported");
}
ViewState["ButtonType"] = value;
}
}
/// <summary>
///Text na tlačítku
/// </summary>
public string ButtonText
{
get
{
string btnDescription = (string)ViewState["ButtonDescription"];
if (btnDescription == null)
{
return DEFAULT_BUTTON_TEXT;
}
return (btnDescription);
}
set
{
ViewState["ButtonDescription"] = value;
}
}
/// <summary>
///Text varování, které se má zobrazit uživateli při pokusu vymazat záznam
/// </summary>
public string ConfirmMessage
{
get
{
string confirmMessage = (string)ViewState["ConfirmMessage"];
if (confirmMessage == null)
{
return DEFAULT_CONFIRM_MESSAGE;
}
return (confirmMessage);
}
set
{
ViewState["ConfirmMessage"] = value;
}
}
#endregion Public properties
}
}
Třída DeleteButtonField2 je potomkem třídy Template Field. Ve svém konstruktoru do zděděných vlastností ItemTemplate (výchozí šablona pro řádek), AlternatingItemTemplate (šablona pro alternativní zobrazení řádku) a EditItemTemplate (šablona použitá při editaci řádku) dosadí instanci šablony DeleteButtonTemplate, jejíž kód uvidíme za chvíli. Šabloně je do kostruktoru předán odkaz na rodičovský sloupec, protože šablona DeleteButtonTemplate vytváří svůj obsah také podle hodnot veřejných vlastností třídy DeleteButtonField. Vlastnost ButtonType určuje typ zobrazovaného tlačítka. Podporujeme nyní pouze varianty Button (běžné tlačítko) a LinkButton (tlačítko ve formě hypertextového odkazu). Ve vlastnosti ButtonText je uložen popisek tlačítka a do vlastnosti ConfirmMessage můžeme uložit text potvrzující zprávy, která má být uživateli zobrazena v javascriptovém okně (text JS dialogu Confirm).
Jedním z důvodů, proč vlastnosti v třídě DeleteButtonField2 nedelegují při přístupu ke svým vlastnostem vykonání přímo na stejné vlastnosti, které by mohly být nadeklarovány v šabloně DeleteButtonTemplate, je potřeba využívat ViewState. Náš sloupec participuje na ViewState automaticky, kdežto do šablony DeleteButtonTemplate bychom podporu pro ViewState museli dodat vlastní realizací rozhraní IStateManager. Pro naše účely je ale využití ViewState v třídě DeleteButtonField2 dostačující.
namespace RStein.Web.UI
{
/// <summary>
/// Šablona s tlačítkem pro potvrzení smazání záznamu z <see cref="GridView"/>
/// </summary>
internal class DeleteButtonTemplate : ITemplate
{
#region Private members
private DeleteButtonField2 m_parent;
#endregion Private members
#region Constructors
/// <summary>
/// Konstruktor
/// </summary>
public DeleteButtonTemplate(DeleteButtonField2 parent)
{
if (parent == null)
{
throw new ArgumentNullException("parent");
}
m_parent = parent;
}
#endregion Constructors
#region Public methods
#region ITemplate Members
/// <summary>
/// Metoda, v níž jsou vytvořeny ovládací prvky šablony a přidány do kolekce Controls argumentu container;
/// </summary>
/// <param name="container">Ovládací prvek, v němž je šablona instanciována</param>
public void InstantiateIn(Control container)
{
IButtonControl newButton = null;
if (m_parent.ButtonType == ButtonType.Button)
{
newButton = new Button();
((Button)newButton).OnClientClick = String.Format(DeleteButtonField2.DELETE_JS, m_parent.ConfirmMessage);
}
else
{
newButton = new LinkButton();
((LinkButton)newButton).OnClientClick = String.Format(DeleteButtonField2.DELETE_JS, m_parent.ConfirmMessage);
}
newButton.CommandName = DeleteButtonField2.DELETE_COMMAND;
newButton.Text = m_parent.ButtonText;
Control newControl = newButton as Control;
newControl.ID = DeleteButtonField2.DELETE_BUTTON_ID;
container.Controls.Add(newControl);
}
#endregion ITemplate Members
#endregion Public methods
}
}
Třída DeleteButtonTemplate musí implementovat rozhraní ITemplate. Rozhraní ITemplate je složeno z jediné metody s názvem InstantiateIn. Metoda InstantiateIn dostane v argumentu container odkaz na "nadřízený" ovládací prvek, jehož součástí je šablona ITemplate, a její odpovědností je přidat do kolekce ovládacích prvků "nadřízeného" ovládacího prvku ovládací prvky (celý obsah) šablony.
V metodě InstantiateIn vytvoříme tlačítko (Buton nebo LinkButton), kód pro pro potvrzení vymazání záznamu vložíme do vlastnosti OnClientClick, a zadáme uživatelský popisek (Text) nového tlačítka. Dále nastavíme jméno přlkazu (CommandName) tlačítka na řetězec 'Delete', který je pro GridView při zpracovávání událostí "wellknown" signálem, že si uživatel přeje vymazat záznam. Nastavíme Id nového tlačítka a nakonec jej vložíme do kolekce Controls "nadřízeného" prvku v argumentu container.
Použití nového sloupce se ničím neliší od použití sloupce vytvořeného v přechozím spotu:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register Assembly="ClassLibrary1" Namespace="RStein.Web.UI" TagPrefix="custom" %>
<!
DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<
html xmlns="http://www.w3.org/1999/xhtml" >
<
head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" OnRowDeleting="GridView1_RowDeleting" DataKeyNames="Id" DataSourceID="SqlDataSource1">
<Columns>
<asp:BoundField DataField="Name" HeaderText="Name" SortExpression="Name" />
<asp:BoundField DataField="State" HeaderText="State" SortExpression="State" />
<custom:DeleteButtonField2 ButtonText="Smazat" ButtonType="Button" ConfirmMessage="Smazat z znam?"/>
</Columns>
</asp:GridView>
</div>
</form>
</body>
</html>
Thursday, July 20, 2006 1:08:06 PM (Central Europe Standard Time, UTC+01:00)
ASP.NET
Friday, May 19, 2006
Sloupec v GridView s potvrzením vymazání záznamu (na klientovi)
Jestliže chcete mít v GridView sloupec, který uživatele požádá na klientovi (s využitím JavaScriptu) o potvrzeni smazání záznamu, je doporučeno používat TemplateField, jehož šablona ItemTemplate obsahuje ve vlastnosti OnClientClick tlačítka klientský kód.
<asp
:TemplateField ShowHeader="False">
<ItemTemplate>
<asp:ImageButton ID="ImageButton1" runat="server" CausesValidation="False" CommandName="Delete" ImageUrl="~/Images/delete.gif" OnClientClick="javascript:return confirm('Opravdu smazat záznam?');" />
</ItemTemplate>
</asp:TemplateField>
ButtonField by byl sice vhodnější, ale tvůrci ASP.NET zadání kódu v JavaScriptu pro ButtonField nepodporují. Třída ButtonField nenabízí ani přístup k vlastnostem prvku Button, který je vyrenderován na každém řádku GridView.
Chceme-li napsat svůj vlastní typový sloupec pro mazání záznamů, který budeme používat ve všech svých projektech, použijeme třídu ButtonField alespoň jako předka svého nového sloupce a lehce "dirty" kódem v v přepsané metodě InitializeCell přidáme tlačítku vytvořenému v "našem" sloupci pro každý řádek GridView jednoduchý kód v JavaScriptu, jenž bude vyžadovat od uživatele potvrzení smazání záznamu.
/// <summary>
/// Nový typ sloupce pro GridView obsahující tlačítko pro smazání záznamu s potvrzením na klientovi (JS)
/// </summary>
public class DeleteButtonField : ButtonField
{
#region Private constants
private const string DEFAULT_CONFIRM_MESSAGE = "Opravdu smazat záznam?";
private const string DELETE_COMMAND = "Delete";
private const string DELETE_JS = "javascript: if (!confirm('{0}')) return false;";
#endregion Private constants
public DeleteButtonField() : base()
{
}
#region Public properties
/// <summary>
///Text varování, které se má zobrazit uživateli při pokusu vymazat záznam
/// </summary>
public string ConfirmMessage
{
get
{
string confirmMessage = (string)ViewState["ConfirmMessage"];
if (confirmMessage == null)
{
return DEFAULT_CONFIRM_MESSAGE;
}
return (confirmMessage);
}
set
{
ViewState["ConfirmMessage"] = value;
}
}
#endregion Public properties
#region Public properties
/// <summary>
/// Metoda volaná při incializaci buňky Gridu
/// </summary>
/// <param name="cell">Inicializovaná buňka</param>
/// <param name="cellType">Typ buňky</param>
/// <param name="rowState">Stav řádku buňky</param>
/// <param name="rowIndex">Index řádku buňky</param>
public override void InitializeCell(DataControlFieldCell cell, DataControlCellType cellType, DataControlRowState rowState, int rowIndex)
{
base.InitializeCell(cell, cellType, rowState, rowIndex);
if (cell.Controls.Count > 0)
{
IButtonControl control = cell.Controls[0] as IButtonControl;
if (control != null)
{
control.CommandName = DELETE_COMMAND;
LinkButton lb = control as LinkButton;
Button btn = control as Button;
ImageButton iBtn = control as ImageButton;
string fullConfirmMessage = String.Format(DELETE_JS, ConfirmMessage);
if (lb != null)
{
lb.OnClientClick = fullConfirmMessage;
}
else if (btn != null)
{
btn.OnClientClick = fullConfirmMessage;
}
else if (iBtn != null)
{
iBtn.OnClientClick = fullConfirmMessage;
}
}
}
}
#endregion Public methods
}
Kód není nijak složitý. Kromě konstant, jejichž význam je dostatečně zřejmý z jejich názvu, máme v našem novém sloupci vlastnost ConfirmMessage, která obsahuje text zobrazovaný v dialogu pro potvrzení smazání záznamu. V metodě InitializeCell nejdříve zavoláme implementaci InitializeCell třídy ButtonField. Poté zjistíme, jestli buňka gridu (cell) obsahuje nějaké ovládací prvky. Jestliže ne, byla metoda volána pro řádek reprezentující záhlaví nebo zápatí gridu, a v ní žádné tlačítko nebude. Když buňka obsahuje alespoň jeden ovládací prvek, zjistíme zda první prvek v kolekci1 je typu IButtonControl (rozhraní IButtonControl implementují třídy LinkButton, Button i ImageButton) a nastavíme jeho vlastnost CommandName na text Delete. Jak asi víte, řetězec "Delete" je jedním z příkazů, které GridView rozeznává a speciálně ošetřuje, takže při bublání událost Click tlačítka s CommandName nastaveným na "Delete" je vyvolána událost RowDeleting (a případně další) místo generické události RowCommand.
Protože rozhraní IButtonControl nenabízí vlastnost OnClientClick pro zadání potvrzujícího kódu v Javascriptu, musíme přetypovat na konkrétní třídu Button a poté teprve můžeme použít vlastnost OnClientClick. Další "dirty" kód - byl bych raději, kdyby byla vlastnost OnClientClick přímo součástí rozhraní IButtonControl, anebo by mohl existovat společný abstraktní předek s vlastností OnClientClick pro třídy ImageButton, LinkButton a Button.
Nový sloupec ve svém projektu použijete takto:
Zaregistrujete nový sloupec na stránce jako běžný serverový ovládací prvek zadáním jména jeho assembly, jmenného prostoru a rezervací prefixu značek pro své prvky v direktivě @Register.
<%
@ Register Assembly="Rstein.Web.Ui" Namespace="Rstein.Web.Ui" TagPrefix="custom" %>
Sloupec přidejte do kolekce Columns prvku GridView a nastavte dle svých požadavků vlastnosti sloupce.
<asp
:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" OnRowDeleting="GridView1_RowDeleting">
<Columns>
<custom:DeleteButtonField Text="Smazat" ButtonType="Button" ConfirmMessage="Smazat záznam?"/>
</Columns>
</asp:GridView>
Příště si ukážeme, jak podobné sloupce pro Grid udělat lépe s využitím šablon vytvořených v kódu a také doplníme pro své sloupce kompletní podporu pro design time.
- Zde je ten zmiňovaný "dirty" postup. I když jsem si ověřil Reflectorem, že prvním prvkem kolekce je vždy IButtonControl vytvářený třídou ButtonColumn, jde o typický případ svazování svého kódu s kódem, jehož změny nemáte pod kontrolou a přitom se nespoléháte jen na jeho "typově bezpečné" veřejné API, takže se v dalších verzích můžete dočkat nepříjemných překvapení :(
Friday, May 19, 2006 5:44:04 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | ASP.NET
Wednesday, May 26, 2004
Svůdná asynchronní volání v ASP.NET 2.0
ASP.NET 2.0 ulehčí vývojářům práci nativní podporou asynchronních volání. V aktuální verzi 1.1 je prováděno pouze synchronní odesílání formuláře („postback“) Při větším množství „postbacků“, které musíme používat například proto, že plníme některé serverové ovládací prvky podle uživatelem zadaných hodnot v jiných ovládacích prvcích, rychle odhalíme méně příjemné aspekty „postbacků“. Uživatelé, zvláště ti s pomalejším připojením, takové aplikace nepoužívají rádi, protože jim obrazovka neustále nepříjemně problikává nebo musejí nezanedbatelnou dobu čekat na nové zobrazení stránky.
Řešení je mnoho. Ti troufalejší z nás na stránce uloží všechna data bez ohledu na to, že každý uživatel využije jen malý zlomek. Podmíněné plnění prvků je zajištěno JavaScriptem. Nevýhodou je, že uživatelé zbytečně stahují do svého prohlížeče i data, která nikdy nepoužijí. Při větším objemu dat je toto řešení nepřijatelné.
Lepším řešením je například asynchronní volání WWW služby z JavaScriptu nebo využití objektu XmlHttp. Uživatel pracuje a aplikace na pozadí stahuje potřebná data ze serveru.
Do ASP.NET 2.0 bylo přidáno nové API pro asynchronní volání, které vývojáře zbavuje nutnosti znát detailně principy a způsoby zajištění asynchronního volání. Interně ale toto API (alespoň v IE) stále pracuje s komponentou XmlHttp.
Jak nové API vypadá?
Serverový ovládací prvek musí implementovat rozhraní ICallBackEventHandler.
interface ICallBackEventHandler
{
string RaiseCallbackEvent(string eventArgument);
}
Metoda RaiseCallBackEvent přijímá jeden argument typu string s názvem eventArgument, který je zaslán z klientské funkce na stránce v prohlížeči. Může se jednat například o identifikátor kategorie, jejíž výrobky mají být načteny. Metoda RaiseCallBackEvent načte data, ať už z databáze, XML souboru nebo jiného datového zdroje, a ve formě řetězce je vrátí. Návratová hodnota metody je zaslána do prohlížeče, který ji zobrazí nebo jinak zpracuje.
Serverový ovládací prvek si při svém renderování vyžádá od stránky kód v JavaScriptu, který z klienta asynchronně zavolá aplikační logiku na serveru. Do třídy Page byla přidána přetížená metoda GetCallbackEventReference. Zde je jedna z jejích verzí.
GetCallbackEventReference(Control control, string content, string callback, string context);
V argumentu control metoda dostane odkaz na serverový ovládací prvek, pro nějž chceme vygenerovat skript, argument content je názvem metody v JavaScriptu, která poskytne parametr (eventArgument), se kterým pracuje výše popsaná metoda RaiseCallBackEvent. V argumentu callback je název další funkce v JavaScriptu, které má být předán výsledek vrácený metodou RaiseCallBackEvent. Tato fukce se většinou postará o aktualizaci uživatelského rozhraní. V argumentu context je název proměnné v JavaScriptu, kterou používáme pro odlišení různých asynchronních volání. Hodnota proměnné context není nikdy zasílána na server, používají ji jen funkce na klientovi.
Naše funkce v JavaScriptu, jíž je předán výsledek asynchronního volání, musí mít tuto signaturu.
function NazevFunkce (raiseCallbackEventMethodResult, context)
Jak mnou zvolené názvy argumentů napovidaji, v prvnim argumentu je výsledek asynchronního volání a ve druhém obsah proměnné context v okamžiku zahájení asynchronního volání.
V jiné verzi metody GetCallbackEventReference lze zadat také název funkce v JavaScriptu, jež bude zavolána, když v metodě RaiseCallbackEvent dojde k výjimce.
A to je celý trik. V rychlosti zrekapitulujme, jak celý scénář probíhá. Metodou GetCallbackEventReference vygenerovaný klientský kód zavolá na klientovi metodu __doCallBack. Ta získá data z funkce „content“ a odešle je na server, kde běhové prostředí nalezne serverový ovládací prvek, jemuž jsou data určena, zavolá jeho metodu RaiseCallbackEvent a výsledná data zašle zpět na klienta metodě „callback“.
Do třídy HttpBrowserCapabilities byly přidány dvě vlastnosti, abychom si mohli při renderování ovládacího prvku ověřit, zda cílový prohlížeč asynchronní volání podporuje. Vlastnost SupportsCallback nese informaci o přítomnosti nebo absenci podpory asynchronního volání v prohlížeči, ale nesděluje nic o tom, jak jsou případná asynchronní volání realizována. Vlastnost SupportsXmlHttp sděluje, jestli prohlížeč pro asynchronní volání může použít objekt XmlHttp.
Wednesday, May 26, 2004 8:11:00 PM (Central Europe Standard Time, UTC+01:00)
ASP.NET
Friday, May 14, 2004
Kniha Network programming for the Microsoft .Net Framework
Kniha Network programming for the Microsoft .Net Framework je úvodem do síťového programování. Slovo úvodem bych zdůraznil, protože žádné rozsáhlé případové studie v ní nenaleznete.
Po obligátním a povrchním úvodu do .Net Frameworku, nás autoři seznámí se “streamy” a pochopitelně se soustředí na třídu NetworkStream. Následuje popis práce s thready a ukázky asynchronního návrhového vzoru (metody Begin*, End*)- i když je problematice věnováno plných 19 stran, nečekejte žádné "best practices", ale spíše upovídaný přepis toho, co je v MSDN. Ukázek asynchronního návrhového vzoru je v celé knize hříšně opulentní množství , takže tato kapitola je jen "lákavou" návnadou.
Dále je v knize vysvětlen význam serializace a probrány jsou XML, binární a SOAP serializace. Když už je serializace probírána do takových detailů, jakými jsou XML atributy, mohla být zmíněna i dynamická redefinice atributů za asistence třídy XmlAttributeOverrides.
Lehký úvod do jmenného prostoru System.Net, jehož smysl mi v kompozici knihy uniká, je vystřídán popisem protokolů IPV4 a IPV6 a metod třídy DNS. Tato kapitola mi připadá z celé knihy nejpovedenější, protože z ní čiší, že její autor své téma podrobně zná, ale přitom dokáže na vyhrazeném prostoru vybrat to podstatné a nezabíhat do zdržujících detailů.
Následující dvě kapitoly tvoří jádro knihy, protože se zabývají klientskými a serverovými sokety a také jejich nadstavbami s jednodušším API (TcpClient, TcpListener). Bohužel tyto kapitoly trpí také největší nevyvážeností ve zpracování tématu. Přístup autorů kolísá od pasivního přejímání informací z MSDN k neustálému zdůrazňování, že pro většinu synchronních metod existují jejich asynchronní doplňky. Když jsem se tuto zajímavou informaci dověděl potřetí a prohlédl si další úmorný Step By Step příklad určený pravděpodobně programátorům po akutní lobotomii provedené brokovnicí, měl jsem chuť připojit se dobře cíleným výstřelem k zástupu šťastných idiotů, abych dokázal kapitoly dočíst. Tam, kde by měla být tématika zpracována velmi prodrobně i s ukázkami, se autoři omezí na shrnující tabulky, jejichž pravděpodobně jediným smyslem je rychlé navýšení počtu popsaných normostran. Moje výtka směřuje hlavně k zcela nedostatečné dokumentaci enumerace SocketOption.
Kromě TCP/IP protokolu jsou zde i ukázky UDP protokolu a implementace takzvaných "Raw" soketů. "Raw" sokety jsou vysvětleny na příkladu ICMP protokolu (což není nic jiného než všem důvěrně známý PING). Slušelo by se zmínit, že na Windows 2000 a XP mohou "Raw" sokety otevřít pouze administrátoři. (ping.exe může spustit každý uživatel, jde asi o jedinou výjimku).
Důležitost tříd WebClient, WebResponse a WebRequest je vyzdvižena v kapitole nazvané "Using the Network". Tato kapitola je povedená, i když zde asi nenaleznete nic překvapivého. Zmíněny jsou cookies, různé způsoby autentizace, certifikáty, SSL.
Z dalších kapitol rychle získáte pocit, že se autoři zavázali dodat dílko o vyšším počtu stran, a proto zařadili témata jako jsou Web Services a .Net Remoting. Sice jsou vždy naťuknuty pokročilé vlastnosti (SOAP extenze, Channel sinks), ale z jejich odfláknutého vysvětlení si začátečník bude brzy zoufat a slabší nátura se k těmto konceptům kvůli neumětelství autorů již nikdy nevrátí. Jako leitmotiv knihy:) se zde objevuje podrobné vysvětlení asynchronního volání WWW služeb.
Autoři se také s těžko pochopitelnou vervou pustí i do vysvětlování Code Acces Security. Jejich snaha se ale omezí na výčet oprávnění a hrubý přehled významu konstitutivních složek CAS, takže kdybych se s CAS setkal poprvé, získám mylný dojem, že CAS je nesmysl bez jakékoli užitné hodnoty. Jsme také poučeni, že bychom měli komunikaci mezi sokety kryptovat, protože svět plný hackerů čeká na naše pakety. Děkuji za osvětu, hned se cítím jako up-to-date vývojář, teď si najdu jen ve slovníku cizích slov význam termínu kryptovat, abych ty hackery svými programy sejmul. :)
Doporučení pro psaní výkonných aplikací jsou shrnuta v mozaice nazvané "Network performance and scalability. Zajímavá je hlavně část týkající se doporučení pro používání "Nagle" algoritmu, ale ani ostatní rady vašim aplikacím neublíží.
Zcela do počtu je kratičká poslední kapitola Advancements in .Net Framework programming, jejíž originální teze by se dala shrnout "Nebojte se, v příští verzi bude zase vše lepší, výkonnější a bezpečnější".
Co říci závěrem? Jestliže se sokety začínáte a jste úplní zelenáči, kniha se Vam bude hodit, i když si z ní odnesete hlavně poznatek, že sokety jsou tady proto, aby bylo na čem demonstrovat dokonalý asynchronní návrhový vzor. :) Jste-li pokročilý vývojář a v knize hledáte neotřelé rady starých zkušených harcovníků, schovejte peněženku, zamáčkněte slzu kanoucí za odpíraným poznáním a vyberte si jiný titul.
Anotace
Název: Network Programming For the Microsoft .Net Framework
Autoři: Jones, A; Ohlund, J; Olson, L;
Nakladatelství: Microsoft Press 2004
Friday, May 14, 2004 8:05:00 PM (Central Europe Standard Time, UTC+01:00)
.NET Framework | .Net Remoting | ASP.NET | Web Services
Friday, May 7, 2004
Uložení důležitého stavu serverových ovládacích prvků v ASP.NET 2.0
ASP.NET 1.0 přineslo pro vývojáře WWW aplikací několik zásadních novinek. Jednou z nejlepších je simulace stavového chovaní prvků pomocí ViewState. U každého serverového ovládací prvku je před vyrenderováním stránky volána metoda SaveViewState, ve které prvek uloží všechny informace potřebné k obnovení stavu. Kompletní stavová informace stránky je poté uložena ve skrytém (hidden) poli na klientovi. Po odeslání formuláře (PostBacku) je volána metoda LoadViewState všech prvků a je jí předán stav uložený metodou SaveViewState. Metoda LoadViewState obnoví stav prvku a poskytne tak vývojáři i uživateli skoro dokonalou iluzi stavového chování známého hlavně z těžkých klientů. Slovo "skoro" jsem použil záměrně, protože je často nutné z výkonnostních důvodů ukládání ViewState zakázat, aby na klienta a pak zpět na server nebyly přenášeny velké objemy dat. Můžeme sice přepsat metody LoadViewStateFromPersistenceMedium a SaveViewStateToPersistenceMedium třídy Page a ukládat ViewState například do databáze na serveru, ale v praxi se tento postup příliš neujal. Ukládání ViewState můžeme zakázat všem ovládacím prvkům na stránce, častěji ale ukládání ViewState zakážeme pouze u vybraných prvků, jež by do ViewState přidaly nejobjemnější položky. Po zakázání ViewState se ale většinou setkáme s problémy, jejichž řešení zabere nezanedbatelné množství času. Například DropDownList si po zákazu ViewState "nepamatuje" vybranou položku (SelectedIndex), takže uložení a nastavení indexu vybrané položky musíme při každém odeslání formuláře řešit sami. ViewState v .Net Frameworku 1.0 vyznává zásadu "všechno nebo nic", ale my bychom často potřebovali ukládat důležitý stav bez ohledu na nastavení ViewState vývojářem. Nemusíme ukládat do ViewState zrovna celý číselník, ale index vybrané položky přenosovou linku nijak nezatíží a programování extrémně zjednoduší.
ASP.NET 2.0 proto přichází s novinkou - kromě ViewState lze ukládat i takzvaný ControlState. O ukládání do ControlState rozhoduje výhradně vývojář serverového ovládacího prvku, jehož odpovědností je, aby do ControlState byla uložena jen data důležitá pro základní činnost prvku. Určitě ale do ControlState nesmí být ukládány DataSety, kolekce ani jiné objemné objekty. Dále platí, že stavová data jsou uložena do ControlState nebo do ViewState, nikdy ne do obou stavových úložišť! Výsledný ControlState celé stránky je ukládán na klientovi ve skrytém poli s nepřekvapivým názvem __CONTROLSTATE.
Ukládání do ControlState se příliš neliší od ukládání do ViewState. Hlavním rozdílem je to, že serverový ovládací prvek si musí vynutit ukládání a načítání ControlState voláním metody RegisterRequiresControlState třídy Page.
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
Page.RegisterRequiresControlState(this);
}
Ve veřejné metodě SaveControlState před vyrenderováním stránky uložíme důležitý stav prvku a vrátíme jej. Návratovým typem metody je typ object, takže můžeme vrátit jakýkoli serializovatelný(!) objekt. V ukázkovém kódu je ukládán ControlState bázové třídy a poté hodnota vlastnosti SelectedIndex.
public override object SaveControlState()
{
object baseState = base.SaveControlState();
object[] savedSate = { baseState, this.SelectedIndex};
return savedState;
}
Metoda LoadControlState po odeslání formuláře obnoví dříve uložený ControlState. Z argumentu state je po přetypování obnoven nejdříve ControlState bázové třídy a poté hodnota vlastnosti SelectedIndex.
public override void LoadControlState(object state)
{
object[] savedState =(object[]) state;
base.LoadControlState(savedState[0]);
this.SelectedIndex = (int) savedState[1] ;
}
Friday, May 7, 2004 3:47:00 PM (Central Europe Standard Time, UTC+01:00)
ASP.NET
Sunday, April 25, 2004
Výběr ze seznamu Id serverových ovládacích prvků na stránce
V konferenci EMWAC se objevil příspěvek http://konference.vyvojar.cz/post.aspx?id=53046, jehož autor chce do vlastnosti ve svém serverovém ovládacím prvku uložit Id jiného ovládacího prvku na stránce. Vývojář by neměl zapisovat Id přímo, ale vybírat ze seznamu na stránce vlastností. První myšlenkou autora příspěvku bylo nadeklarovat vlastnost jako WebControl - to je samozřejmě nesmysl. Když zkusíme perzistovat vlastnost na stránce (atribut PersistenceMode), tak dosáhneme pouze toho, že validátor ASPX stránky nás bude upozorňovat, že máme na stránce 2 prvky se stejným Id.
To, o co se autor snaží, umí všichni validátoři. Jejich vlastnost ControlToValidate nabídne Id všech prvků na stránce, kteří obsahují vlastnost dekorovanou atributem ValidationProperty. Vlastnost ControlToValidate je samozřejmě typu string. Seznam Id naplní speciální TypeConverter s názvem ValidatedControlConverter.
ValidatedControlConverter nelze použít pro načtení seznamu Id všech prvků na stránce, protože, jak jsem již napsal, ta bere v úvahu jen prvky s vlastností označenou atributem ValidationProperty. Proto jsem napsal ControlConverter, který nalezne Id všech prvků na stránce.
public
class
ControlConverter : StringConverter
{
public
ControlConverter() :
base
()
{
}
public
override
StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
if
((context ==
null
) || (context.Container ==
null
))
return
null
;
object
[] foundControls = enumerateControls(context.Container);
if
(foundControls ==
null
)
return
null
;
return
new
StandardValuesCollection(foundControls);
}
public
override
bool
GetStandardValuesSupported(ITypeDescriptorContext context)
{
return
true
;
}
public
override
bool
GetStandardValuesExclusive(ITypeDescriptorContext context)
{
return
false
;
}
private
object
[] enumerateControls(IContainer container)
{
ArrayList retList = new ArrayList();
foreach
(IComponent component
in
container.Components)
{
Control foundControl = component as Control;
if
(foundControl ==
null
)
continue
;
if
((foundControl.ID ==
null
) ||
(foundControl.ID == String.Empty))
continue
;
retList.Add(foundControl.ID);
}
return
retList.ToArray();
}
}
Converter se u vlastnosti použije takto:
[Browsable(
true
)]
[TypeConverter(
typeof
(ControlConverter))]
public
string
MyControl
{
get
{
string
myControl = (
string
) ViewState["MyControl"];
return
(myControl ==
null
? String.Empty : myControl);
}
set
{
ViewState["myControl"] =
value
;
}
}
Sunday, April 25, 2004 6:44:00 PM (Central Europe Standard Time, UTC+01:00)
ASP.NET | .NET Framework