Nuno Gomes /*Aventuras e Desventuras de um programador*/

var assuntos = new { Linguagem = "C#", Tecnologia = "ASP.NET" };

Controlos ASP.NET – ControlAdapter, um caso prático

O ControlAdapter é uma entidade disponível desde a versão 2.0 da Fx.NET e cujo objectivo é permitir adaptar a renderização de um controlo em função de necessidades especificas, sem que no entanto a funcionalidade base do controlo seja alterada.

O uso de ControlAdapter é muito comum na renderização para plataformas especificas, nomeadamente plataformas Mobile.

No caso particular que vou expor o ControlAdapter foi usado ainda com outro objectivo, acrescentar uma funcionalidade a determinado controlo. A ideia foi acrescentar um Captcha a todos os controlos do tipo WeblogPostCommentForm usados no CommunityServer do pontonetpt.com.

Desafio

A complexidade de um controladapter está muitas vezes associada a própria complexidade/estrutura do controlo base. Este é precisamente um desses casos, pois o controlo base carrega dinâmicamente seu conteúdo (leia-se controlos) através de vários ITemplate.

Para quem já usou o ITemplate sabe que embora seja uma optima opção para permitir a composição do controlo levanta uma grande questão:

“A estrutura hierarquica de controlos não é fixa logo não é possivel assumir qualquer estrutura.”

Uma análise mais cuidada revelou precisamente isto, os templates variam com o tema mas acabam sempre por possuir algures o botão um submissão com o mesmo ID (que conveniente!!).

Este botão vai ser a âncora para adicionar os controlos que constituem o Captcha.

Na minha análise notei também que o WeblogPostCommentForm herda a sua implementação do controlo WrappedFormBase. Este controlo é a base para todos os formulários de introdução de dados do CommunityServer.

Conhecendo esta relação de herança o objectivo passou a ser a criação de um ControlAdapter que pudesse ser extendido para permitir o uso do Captcha nas seguintes funcionalidades:

  • criação de utilizador
  • adição de comentários

Assim, determinei desde logo que o meu ControlAdapter base teria a seguinte assinatura:

public abstract class WrappedFormBaseCaptchaAdapter<T> : ControlAdapter where T : WrappedFormBase
{
}
Óptimo, agora só faltava tudo o resto …

Captcha

O Captcha será constituido por:

  • Uma imagem gerada dinâmicamente com um conjunto de números aleatórios
  • Uma caixa de texto para introduzir os números tal como aparecem na imagem
  • Um validador para aferir a correspondência entre os números introduzidos e a números presentes na imagem

Esta é uma implementação tipica de Captcha e não levanta grandes problemas. O problema é, tal como já foi referido, determinar o controlo âncora para permitir posicionar o Captcha.

Este controlo âncora tem dois vectores de incerteza:

  • varia com o nosso controlo alvo
  • varia com o tema usado

Para suportar este dinamismo optei pela seguinte implementação:

private List<string> _validAnchorIds = null;
protected virtual List<string> ValidAnchorIds
{
    get
    {
        if (this._validAnchorIds == null)
        {
            this._validAnchorIds = new List<string>();
            this._validAnchorIds.Add("btnSubmit");
        }
        return this._validAnchorIds;
    }
}

private Control GetAnchorControl(T wrapper)
{
    if (this.ValidAnchorIds == null || this.ValidAnchorIds.Count == 0)
    {
        throw new ArgumentException("Cannot be null or empty", "validAnchorNames");
    }

    var q = from anchorId in this.ValidAnchorIds
            let anchorControl = CSControlUtility.Instance().FindControl(wrapper, anchorId)
            where anchorControl != null
            select anchorControl;

    return q.FirstOrDefault();
}

É assim possivel, através da propriedade ValidAnchorIds, configurar qual o Id dos controlos válidos para servir de âncora.

O método GetAnchorControl serve para obter o controlo âncora. O porquê de usar ou não LINQ To Objects já foi discutido aqui, o que há a salientar é o uso do método CSControlUtility.Instance().FindControl da livraria do CommunityServer.

Assumindo que foi encontrado um controlo âncora é possivel embutir o Captcha no controlo base. Não há qualquer ciência nesta tarefa, procede-se da mesma forma que nos restantes controlos:

protected sealed override void CreateChildControls()
{
    base.CreateChildControls();

    if (this.IsCaptchaRequired)
    {
        T wrapper = base.Control as T;
        if (wrapper != null)
        {
            Control anchorControl = GetAnchorControl(wrapper);
            if (anchorControl != null)
            {
                _imgCaptcha = new System.Web.UI.WebControls.Image();

                […]
                Panel phCaptcha = new Panel();
                phCaptcha.CssClass = "CommonFormField";
                phCaptcha.ID = "Captcha";
                phCaptcha.Controls.Add(_imgCaptcha);
                phCaptcha.Controls.Add(new LiteralControl("<br />"));
                Label label = new Label();
                label.Text = "Enter the numbers above: ";

                phCaptcha.Controls.Add(label);
                phCaptcha.Controls.Add(txtCaptcha);
                phCaptcha.Controls.Add(captchaPostValidator);

                int index = anchorControl.Parent.Controls.IndexOf(anchorControl);
                anchorControl.Parent.Controls.AddAt(index, phCaptcha);
            }
        }
    }
}

Uma leitura atenta do código anterior revela que a imagem de suporte ao Captcha não tem qualquer Url associado. Essa tarefa é realizada mais à frente no ciclo de vida do Adapter:

protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);

    if (this._imgCaptcha != null)
    {
        CaptchaData captchaData = CaptchaManager.GetCaptcha();

        this._imgCaptcha.ImageUrl = captchaData.ImageUrl;
        this._imgCaptcha.AlternateText = "If you can't read this number refresh your screen";

        HttpContext.Current.Response.Cookies[CookieName].Value = CipherManager.Encrypt(captchaData.Code);
        HttpContext.Current.Response.Cookies[CookieName].HttpOnly = true;
    }
}

Aqui acontecem algum pormenores interessantes.

Primeiro introduzo o conceito de CaptchaManager, um singleton que nos permite obter a informação especifica do Captcha numa estrutura denominada CaptchaData.

O CaptchaManager implementa o padrão “Provider Pattern” e permite que através de simples alterações de configuração o algoritmos de geração do Captcha seja alterado.

Segundo, persisto a chave do Captcha (devidamente encriptada) num Cookie marcado como HttpOnly para usar na validação do próximo pedido:

protected sealed override void OnInit(EventArgs e)
{
    base.OnInit(e);
    if (this.Page.IsPostBack && this.IsCaptchaRequired)
    {
        this.Page.Validate();
    }
}

Uma vez adicionada a validação ficamos com esqueleto base construido.

Configurar um adapter específico ficou então bastante simples. Aqui fica a implementação para o WeblogPostCommentForm:

public class WeblogPostCommentFormCaptchaAdapter : WrappedFormBaseCaptchaAdapter<WrappedFormBase>
{
    #region Overriden Methods

    protected override List<string> ValidAnchorIds
    {
        get
        {
            List<string> validAnchorNames = base.ValidAnchorIds;
            validAnchorNames.Add("CommentSubmit");
            return validAnchorNames;
        }
    }

    protected override string DefaultValidationGroup
    {
        get { return "CreateCommentForm"; }
    }

    #endregion Overriden Methods
}

Configuração

Uma vez criado o Adapter resta apenas proceder à configuração no ficheiro default.browser:

<?xml version='1.0' encoding='utf-8'?>
<browsers>
  <browser refID="Default">
    <controlAdapters>
      <!-- Adapter for the WeblogPostCommentForm control in order to add the Captcha and prevent SPAM comments -->
      <adapter controlType="CommunityServer.Blogs.Controls.WeblogPostCommentForm" adapterType="NunoGomes.CommunityServer.Components.WeblogPostCommentFormCaptchaAdapter, NunoGomes.CommunityServer" />
    </controlAdapters>
  </browser>
</browsers>

e configurar o provider de geração de Captcha:

<configuration>
  <configSections>
    <!-- New section for Captcha providers configuration -->
    <section name="communityServer.Captcha" type="NunoGomes.CommunityServer.Captcha.Configuration.CaptchaSection" />
  </configSections>
  <!-- Configuring a simple Captcha provider -->
  <communityServer.Captcha defaultProvider="simpleCaptcha">
    <providers>
      <add name="simpleCaptcha" type="NunoGomes.CommunityServer.Captcha.Providers.SimpleCaptchaProvider, NunoGomes.CommunityServer"
                 imageUrl="captcha.ashx" timeout="00:15:00" />
    </providers>
  </communityServer.Captcha>
  <system.web>
    <httpHandlers>
      <!-- The Captcha Image handler used by the simple Captcha provider -->
      <add verb="GET" path="captcha.ashx" type="NunoGomes.CommunityServer.Captcha.Providers.SimpleCaptchaProviderImageHandler, NunoGomes.CommunityServer" />
    </httpHandlers>
  </system.web>
  <system.webServer>
    <handlers accessPolicy="Read, Write, Script, Execute">
      <!-- The Captcha Image handler used by the simple Captcha provider -->
      <add verb="GET" name="captcha" path="captcha.ashx" type="NunoGomes.CommunityServer.Captcha.Providers.SimpleCaptchaProviderImageHandler, NunoGomes.CommunityServer" />
    </handlers>
  </system.webServer>
</configuration>

Conclusão

A construção de um Adapter pode ser bastante complexa mas a recompensa é a forma como nos permite facilmente, através de configuração, modificar a renderização e o comportamento de uma aplicação.

Podem ver o Adapter em acção aqui (mas têm que estar anónimos).

Reparem que a aplicação original não foi modificada ou recompilada.

Imaginem o que se pode fazer com isto … ;)

Agora juntem-lhe TagMapping … mas isso fica para um futuro próximo.

Posted: 5-3-2010 0:26 por Nuno Gomes | with 2 comment(s)
Filed under: ,

Comments

João Filipe Rocha said:

Viva Nuno acho o excelente caso pratico para o Control Adapter, no entanto eu quando fiz um captca utilizei um ASP.NET Handler que disponibilizava uma imagem dos caracteres que lhe eram passados, um bocado menos complexo que o control adapter.

Mas no teu exemplo dá para ver a potencialidade do Control Adapter.

# Março 5, 2010 12:01

Nuno Gomes said:

Olá João, o foco deste post foi o ControlAdapter e por isso não aprofundei a geração do Captcha.


O adapter é usado para injectar os controlos adicionais, entre os quais um controlo Image cujo ImageUrl é precisamente um HttpHandler. Alias, o handler até está no exemplo do web.config.


No que diz respeito à criação do handler ou à geração do Captcha o grande desafio foi garantir a coexistência de multiplos Captchas em simultaneo numa mesma página.


Talvez aprofunde este tema no futuro ;)

# Março 5, 2010 2:03
Leave a Comment

(requerido) 

(requerido) 

(opcional)

 

(requerido) 

If you can't read this number refresh your screen
Enter the numbers above: