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.