Docela užitečnou funkcionalitou na řadě webů je možnost zapamatovat si hodnotu zadanou do textboxu v cookie, aby ji uživatel nemusel vyplňovat pořád znovu. Typické použití je například v komentářích (i na tomto webu). Psát ji na každém webu zvlášť je ovšem poněkud otravné, chtělo by to nějaké univerzální řešení. Zajímavou cestou je použití Control extenderů, známých spíše ve světě AJAXu.
Control extendery se objevily v ASP.NET AJAX Extensions pro ASP.NET 2.0, součástí .NET Frameworku jsou od verze 3.0. Control extender je technicky běžný server control, který ale nefunguje samostatně, ale rozšiřuje schopnosti nějakého jiného prvku (v terminologii extenderů se jim říká target controls). Control Extendery tak, jak je známe dnes, se zpravidla používají pro klientské, JavaScriptové funkce. Typickým příkladem je třeba AutoCompleteExtender
, který rozšiřuje prvek TextBox
o schopnost server-driven autocomplete seznamu. Extendery lze nicméně využít i na serverové straně. Typickým případem jsou query extendery v ASP.NET 4.0. A nebo právě CookieStoreExtender
, o němž bude řeč níže.
Základní idea
O práci s cookies v ASP.NET jsem psal už před několika lety, takže nyní budu stručný. Cookies lze vnímat jako jedno nebo dvouúrovňovou strukturu. Cookie může mít buďto jednu stringovou hodnotu a nebo může obsahovat kolekci hodnot, adresovatelných pomocí názvu. K přijatým cookies můžete přistupovat pomocí kolekce Request.Cookies
a k odesílaným pomocí Response.Cookies
.
Základní idea zapamatování spočívá v tom, že někdy ve fázi Load se podíváme, zda nemáme pro konkrétní TextBox
uloženou vhodnou cookie a pokud ano, načteme její hodnotu jako hodnotu tohoto TextBoxu
. V event handleru pro událost TextChanged
pak změněnou hodnotu uložíme pro další použití.
Ještě je třeba vybudovat infrastrukturu pro práci s cookies, zejména tedy jak se bude cookie i její hodnota jmenovat a jak dlouho vydrží. Pokud cookie nenastavíte datum expirace, bude se pamatovat jenom dokud uživatel nezavře okno prohlížeče, což není pro tento účel příhodné.
Implementace extenderu
Extender sám je třída, která je buďto poděděná od base class System.Web.UI.ControlExtender
(to budeme používat) a nebo implementuje interface System.Web.UI.IExtenderControl
(pro speciální případy). Abychom mohli extendery psát, musíme referencovat assembly System.Web.Extensions.dll
ve verzi 1.0 (pro .NET 2.0 nebo 3.0) nebo 3.5 (pro .NET 3.5).
Třídu odekorujeme atributem TargetControlType, kterým řekneme, jaký typ prvku vlastně rozšiřujeme.
Výše uvedená base class nás donutí implementovat metody GetScriptDescriptors
a GetScriptReferences
. Ty nás v daném okamžiku nezajímají, protože klientské skriptování nebudeme potřebovat. Jejich implementace bude jednoduchá:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Web;
namespace Altairis.Web.UI.WebControls {
[TargetControlType(typeof(TextBox))]
public class CookieStoreExtender : ExtenderControl {
protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl) {
yield break;
}
protected override IEnumerable<ScriptReference> GetScriptReferences() {
yield break;
}
}
}
Zato potřebujeme vytvořit několik konfiguračních vlastností: CookieName
, CookieValueName
a CookieExpirationDays
. Jejich význam je myslím jasný. Zároveň jsem implementoval rozumné výchozí hodnoty: název cookie je CSEXT
, expirace 30 dnů a název hodnoty se určuje podle názvu rozšiřovaného prvku (ve vlastnosti EffectiveCookieValueName
).
Zdrojový kód vypadá následovně (změny vyznačeny tučně):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Web;
namespace Altairis.Web.UI.WebControls {
[TargetControlType(typeof(TextBox))]
public class CookieStoreExtender : ExtenderControl {
private const string DEFAULT_COOKIE_NAME = "CSEXT";
private const int DEFAULT_COOKIE_EXPIRATION_DAYS = 30;
public CookieStoreExtender() {
this.CookieExpirationDays = DEFAULT_COOKIE_EXPIRATION_DAYS;
this.CookieName = DEFAULT_COOKIE_NAME;
}
[Category("Cookie"), DefaultValue(DEFAULT_COOKIE_NAME)]
[Description("Storage cookie name. Defaults to 'CSEXT'.")]
public string CookieName {
get { return (string)this.ViewState["CookieName"]; }
set { this.ViewState["CookieName"] = value; }
}
[Category("Cookie")]
[Description("Cookie value name. Defaults to ID of target control.")]
public string CookieValueName {
get { return (string)this.ViewState["CookieValueName"]; }
set { this.ViewState["CookieValueName"] = value; }
}
[Category("Cookie"), DefaultValue(DEFAULT_COOKIE_EXPIRATION_DAYS)]
[Description("Days to cookie expire. Defaults to one month.")]
public int CookieExpirationDays {
get { return (int)this.ViewState["CookieExpirationDays"]; }
set { this.ViewState["CookieExpirationDays"] = value; }
}
private string EffectiveCookieValueName {
get {
if (string.IsNullOrEmpty(this.CookieValueName)) return this.TargetControlID;
return this.CookieValueName;
}
}
protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl) {
yield break;
}
protected override IEnumerable<ScriptReference> GetScriptReferences() {
yield break;
}
}
}
Zbývá jenom reagovat na událost Load
(vlastní) a TextChanged
(rozšiřovaného TextBox
u). Kompletní zdrojový kód bude tedy vypadat následovně:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Web;
namespace Altairis.Web.UI.WebControls {
[TargetControlType(typeof(TextBox))]
public class CookieStoreExtender : ExtenderControl {
private const string DEFAULT_COOKIE_NAME = "CSEXT";
private const int DEFAULT_COOKIE_EXPIRATION_DAYS = 30;
public CookieStoreExtender() {
this.CookieExpirationDays = DEFAULT_COOKIE_EXPIRATION_DAYS;
this.CookieName = DEFAULT_COOKIE_NAME;
}
protected override void OnLoad(EventArgs e) {
base.OnLoad(e);
// Get target TextBox
var targetTextBox = this.NamingContainer.FindControl(this.TargetControlID) as TextBox;
if (targetTextBox == null) throw new ArgumentException("Target control does not exist or is not a TextBox control.");
// Attach handler to store changed value
targetTextBox.TextChanged += new EventHandler(TargetTextBox_TextChanged);
// Load extended control value from cookie, if exists
if (!this.Page.IsPostBack) {
var cookie = this.Page.Request.Cookies[CookieName];
if (cookie == null) return; // no cookie found
var value = cookie[this.EffectiveCookieValueName];
if (string.IsNullOrEmpty(value)) return; // cookie value is empty
targetTextBox.Text = value;
}
}
void TargetTextBox_TextChanged(object sender, EventArgs e) {
// Get target TextBox
var tb = sender as TextBox;
// Get or create cookie
var cookie = tb.Page.Response.Cookies[this.CookieName];
if (cookie == null) {
cookie = new HttpCookie(this.CookieName);
tb.Page.Response.Cookies.Add(cookie);
}
// Update cookie values
cookie.Expires = DateTime.Now.AddDays(this.CookieExpirationDays);
cookie.Values[this.EffectiveCookieValueName] = tb.Text;
}
#region Configuration
[Category("Cookie"), DefaultValue(DEFAULT_COOKIE_NAME)]
[Description("Storage cookie name. Defaults to 'CSEXT'.")]
public string CookieName {
get { return (string)this.ViewState["CookieName"]; }
set { this.ViewState["CookieName"] = value; }
}
[Category("Cookie")]
[Description("Cookie value name. Defaults to ID of target control.")]
public string CookieValueName {
get { return (string)this.ViewState["CookieValueName"]; }
set { this.ViewState["CookieValueName"] = value; }
}
[Category("Cookie"), DefaultValue(DEFAULT_COOKIE_EXPIRATION_DAYS)]
[Description("Days to cookie expire. Defaults to one month.")]
public int CookieExpirationDays {
get { return (int)this.ViewState["CookieExpirationDays"]; }
set { this.ViewState["CookieExpirationDays"] = value; }
}
private string EffectiveCookieValueName {
get {
if (string.IsNullOrEmpty(this.CookieValueName)) return this.TargetControlID;
return this.CookieValueName;
}
}
#endregion
#region ExtenderControl required members
// Not implemented, because we don't use client scripting here
protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl) {
yield break;
}
protected override IEnumerable<ScriptReference> GetScriptReferences() {
yield break;
}
#endregion
}
}
Použití extenderu
Použití extenderu je velmi jednoduché, po registraci jej použijeme jako běžný control:
<asp:TextBox ID="SenderNameTextBox" runat="server" />
<altairis:CookieStoreExtender ID="CookieStoreExtender1" runat="server"
TargetControlID="SenderNameTextBox"
CookieName="GuestBook"
CookieValueName="Name" />
Zde nastavujeme ručně název cookie i hodnoty, ale jediným povinným parametrem je TargetControlID
.
Popsané řešení zdaleka není jediné možné, ale přijde mi docela zábavné a elegantní.