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 TextBoxu). 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í.