arrow Blog
rss

Un exemplu de utilizare al Substitution API în ASP.NET

October 25th, 2011

Substitution API este de fapt un nume mai pompos pentru a descrie funcționalitatea oferită de metoda HttpResponse.WriteSubstitution(callback) care, în loc să scrie un șir de caractere în stream-ul răspunsului, ”scrie” un placeholder (înlocuitor) ce constă din funcția dată ca parametru care, atunci când răspunsul este trimis către client, este executată iar poziția ei în răspuns este înlocuită cu rezultatul execuției. Astfel, acest procedeu răspunde nevoii de actualiza dinamic porțiuni dintr-o pagină (sau dintr-un UserControl – .ascx) cache-uită folosind API-ul de output caching.

Datele problemei

Avem următoarea sarcină de lucru: trebuie să prezentăm o pagină al cărei conținut nu se schimbă foarte des și este același, indiferent de utilizatorul autentificat, fiind astfel un candidat foarte bun pentru output caching. Totuși, există o mică zonă din pagină care poate varia: link-ul de log-in (care apare dacă utilizatorul nu e autentificat), sau de log-out (care apare dacă utilizatorul este autentificat). Problema este că, odată salvat răspunsul în cache, acel link va rămâne același din momentul primei accesări a paginii, indiferent utilizatorul se autentifică sau nu ulterior.

Prima soluție

Prima soluție, cea mai simplă, implică folosirea controlului asp:Substitution cu o metodă callback care produce întreg HTML-ul necesar afișării link-ului. Adică ceva de genul:

<asp:Substitution ID="LinkSubstitution" runat="server" MethodName="GenerateLink" />

unde GenerateLink este definită în code-behind astfel:

public static string GenerateLink(HttpContext context)
{
    bool auth = false;
    if (context.Request["IsAuthenticated"] == "true")
        auth = true;
   if (auth)
       return "<a href=\"Logout.aspx\">Log-out</a>";
   else
       return "<a href=\"Login.aspx\">Log-in</a>";
}

Observație: De reținut e faptul că metoda callback nu poate aparține unui instanțe de obiect de tip Page sau Control. Trebuie să fie ori statică, ori să aparțină unei instanțe de obiect de tip diferit.

O rezolvare mai bună

Soluția de  mai sus, deși ușor de implementat, nu este deloc elegantă, pretându-se mai degrabă ca artificiu temporar și nu ca o soluție de durată.

Ideal ar fi să avem o componentă care să ne ofere posibilitatea de varia codul HTML generat în funcție de rezultatul callback-ului dat de noi. În cazul nostru, acest rezultat ar fi format din: text-ul link-ului, adresa lui (atributul href) și, opțional, atributul title al acestuia. Aceste informații le vom grupa într-o mică clasă, după cum urmează:

public class HtmlDynamicAnchorData
{
     public string Href;
     public string Text;
     public string Title;

     public HtmlDynamicAnchorData(string href, string txt)
     {
         Href = href;
         Text = txt;
     }

     public HtmlDynamicAnchorData(string href, string txt, string t)
         : this(href, txt)
     {
         Title = t;
     }
}

Pentru a crea o componentă reutilizabilă și ușor de folosit, am ales varianta dezvoltării ei ca un server control, pe care l-am denumit HtmlDynamicAnchor. Clasa de bază pentru acesta este System.Web.UI.Control, întrucât nu avem nevoie decât de funcționalități minime, cum ar fi folosirea view-state-ului și renderizarea.

Controlul expune două proprietăți:

- CssClass – folosită pentru a stabili clasa CSS a link-ului – opțional;

- MethodName – folosită pentru a stabili numele metodei ce va returna datele necesare: titlu, url și text – obligatoriu. Această metodă trebuie să primească drept parametru un obiect de tip HttpContext și să returneze un obiect de tip HtmlDynamicAnchorData.

În plus, am suprascris metoda Control.RenderControl, astfel încât, în loc de a scrie output-ul propriu-zis, scrie un placeholder pentru a marca faptul că va fi actualizat dinamic chiar și atunci când pagina sau control-ul din care face parte va fi cache-uit:

public override void RenderControl(HtmlTextWriter output)
{
    TemplateControl tc = TemplateControl;
    Type dlgType = typeof(HtmlDynamicAnchorDataDelegate);
    HtmlDynamicAnchorDataDelegate dataDlg = null;
    Dictionary<string, string> attribs = null;
    HtmlDynamicAnchorSubstitution subst = null;
    string attrib = null;

    try
    {
        dataDlg = (HtmlDynamicAnchorDataDelegate)
           Delegate.CreateDelegate(dlgType, tc.GetType(), MethodName);
    }
    catch (Exception)
    {
        dataDlg = null;
    }

    if (dataDlg == null)
        ThrowBadMethodName(MethodName);

    attribs = new Dictionary<string, string>();
    attrib = CssClass;
    if (!string.IsNullOrEmpty(attrib))
        attribs.Add("class", attrib);

    subst = new HtmlDynamicAnchorSubstitution(dataDlg, attribs);
    subst.RegisterCallback(HttpContext.Current);
}

unde HtmlDynamicAnchorDataDelegate este definită astfel:

public delegate HtmlDynamicAnchorData
        HtmlDynamicAnchorDataDelegate(HttpContext context);

Majoritatea operațiilor efectuate de această metodă ar trebui sa fie clare, punctul de interes constituindu-l ultimele două linii. Astfel, HtmlDynamicAnchorSubstitution reprezintă o clasă ajutătoare care:

- prin intermediul metodei RegisterCallback apelează HttpResponse.WriteSubstitution având ca parametru metoda privată Render(context);

public void RegisterCallback(HttpContext context)
{
    HttpResponseSubstitutionCallback callback = null;
    HttpResponse response = context.Response;

    callback = new HttpResponseSubstitutionCallback(Render);
    response.WriteSubstitution(callback);
}

- ține o referință către delegate-ul al cărui nume l-am transmis controlului nostru, astfel încât, atunci cand metoda privată Render este apelată, delegate-ul respectiv va fi folosit pentru a obține datele necesare redării link-ului propriu-zis.

private string Render(HttpContext context)
{
    StringBuilder sb = null;
    HtmlDynamicAnchorData data = null;

    if (mDataDlg != null)
        data = mDataDlg(context);
    data = mDataDlg(context);
    if (data == null)
        return string.Empty;

    sb = new StringBuilder();
    sb.Append("<a");

    foreach (string attribute in mAttributes.Keys)
    {
        if (attribute == "href" || attribute == "title")
            continue;
        AppendAttribute(sb, attribute, mAttributes[attribute]);
    }

    if (!string.IsNullOrEmpty(data.Href))
        AppendAttribute(sb, "href", data.Href);
    if (!string.IsNullOrEmpty(data.Title))
        AppendAttribute(sb, "title", data.Title);

    sb.Append(">");
    if (mDataDlg != null)
        sb.Append(HttpUtility.HtmlEncode(data.InnerText));
    sb.Append("</a>");

    return sb.ToString();
}

Pentru motivul care ne impune folosirea unei clase ajutătoare, vezi obsevația de mai sus.

Folosirea controlului astfel creat

În primul rând, trebuie definită (spre exemplu, în code-behind) metoda care va genera datele pentru redarea controlului:

public static HtmlDynamicAnchorData GetLinkData(HttpContext context)
{
    bool auth = false;
    if (context.Request["IsAuthenticated"] == "true")
        auth = true;
   if (!auth)
       return new HtmlDynamicAnchorData("Login.aspx", "Log-in");
   else
       return new HtmlDynamicAnchorData("Logout.aspx", "Log-out");
}

Folosirea este apoi cât se poate de simplă:

- mai întâi înregistrăm controlul:

<%@ Register Assembly="AssemblyName"
    Namespace="ContorlNamespace" TagPrefix="ui" %>

- apoi îl plasăm unde dorim în pagină:

<ui:HtmlDynamicAnchor ID="DynamicAnchor"
    runat="server" MethodName="GetLinkData" />

În sfârșit, atașez acestui articol un mic proiect demonstrativ, care include întreg codul sursă pentru acest control plus un exemplu de utilizare. Click aici pentru descărcare.

Și în caz că vă întrebați, ce legătură există între articol și imagine, aflați că procedeul descris aici se mai numeste și ”donut caching” :) .

Taguri: , ,
202 afisari

Comenteaza

*