Construção de uma mini-framework de validação em ASP.NET
Apesar da plataforma ASP.NET fornecer alguns controlos capazes de efectuar a maior parte das validações necessárias, a verdade é que, normalmente, esses controlos não são suficientes para garatirem a correcção dos dados atribuídos às propriedades expostas pelos objectos de negócios em todos os instantes. Aliás, basta pensarmos que o nosso modelo de objectos pode ser simultaneamente usado por uma aplicação web e exposto através de um conjunto de web services. Nestas situações, as regras de negócio têm de ser obrigatoriamente "impostas" pelos próprios objectos de forma a garantir o correcto funcionamento do modelo (aliás, na minha opinião, estas regras devem ser sempre "impostas" pelos próprios objectos).
Por outro lado, se tentarmos analisar a situação do ponto de vista do programador ASP.NET, também não é justo efectuarmos apenas a validação dos valores nos objectos e não utilizar todas as funcionalidades disponibilizadas pelos validators! Bem, o que dava realmente jeito era conseguir reutilizar as regras de validação pelos validators e pelos próprios objectos. A utilização desta estratégia garante-nos o melhor de dois mundos: os objectos efectuam sempre a validação das propriedades, mas nós, programadores de ASP.NET, continuamos a poder utilizar os nossos validators sem grande duplicação de código.
Mas então o que será necessário para atingirmos este objectivo? Na minha opinião, precisamos de duas coisas: precisamos de conseguir construir um conjunto de rotinas de validação que permitam a fácil composição de regras e necessitamos de garantir que temos validatores capazes de garantir a validação dos dados introduzidos num controlo através da reutilização dessas rotinas. Como vamos ver ao longo deste artigo, a plataforma .NET permite-nos facilmente atingir estes objectivos através da escrita de algumas linhas de código.
Construção de regras de validação
Se pensarmos um pouco, a validação de dados poderá ser facilmente reutilizada se for exposta através de uma classe que desempenha um papel de serviço. Esta classe limitar-se-ia a expor um conjunto de rotinas responsáveis por receber um valor e indicar se esse valor está ou não correcto. Para ilustrarmos estes conceitos, vamos começar por supor que temos uma classe Aluno com duas propriedades: Nome e Morada. Para começar, apenas queremos garantir que a atribuição do nome ao aluno nunca aceita o valor nulo. Para começar, podemos implementar a propriedade Nome através de um get e set simples
public string Nome
{
get
{
return _nome;
}
set
{
ServicoValidacao.ValidaNome( value );
_nome = value;
}
}
O serviço de validação seria responsável por validar o nome, garantindo que este não está vazio nem nulo:
public static class ServicoValidacao
{
public static void ValidaNome( string nome )
{
if (string.IsNullOrEmpty( nome ))
{
throw new ArgumentException( "Nome não pode ser nulo ou vazio" );
}
}
}
Uma vez que o nosso objecto Aluno será, em último caso, persistido numa base de dados, optamos também por obrigar a que o número de carácteres do nome não ultrapasse o número definido na coluna onde esse valor será persistido (neste caso, podemos optar por considerar 300 o número máximo de carácteres). Logo, basta-nos apenas modificar o serviço de forma a adicionar mais uma condição:
public static class ServicoValidacao
{
public static void ValidaNome( string nome )
{
if (string.IsNullOrEmpty( nome ) || nome.Length > 300 )
{
throw new ArgumentException( "Nome não pode ser nulo ou vazio" );
}
}
}
Apesar de satisfazer minimamente os requisitos, a validação não é feita de uma forma muito elegante e a eventual adição de novas condições deixa o código cada vez menos legível. A solução para este problema passa pelo desenvolvimento de uma livraria que facilita a validação permitindo mesmo a fácil composição de condições. O diagrama seguinte ilustra a estratégia usada:

A ideia é simples: a classe base define apenas um método abstracto designado de IsValid. Todas as restantes classes implementam esse método de forma a satisfazerem um determinado critério. Por exemplo, a classe LARegExpValidation implementa o método tendo em atenção uma expressão regular e uma string que são passadas à classe através do construtor.
namespace LAValidationLib
{
public class LARegExpValidation: LABaseValidator
{
private string _expressionToValidate;
private string _regExpression;
public LARegExpValidation( string expressionToValidate, string regExpression )
{
_expressionToValidate = expressionToValidate;
_regExpression = regExpression;
}
public override bool IsValid()
{
Regex regExp = new Regex( _regExpression );
return regExp.IsMatch( _expressionToValidate );
}
}
}
A utilização desta mini-framework modifica o código de validação, tornando-o mais legível, como podemos comprovar através do excerto seguinte:
public static class ServicoValidacao
{
public static void ValidaNome( string nome )
{
LAValidationLib.LAAndValidator validator = new LAValidationLib.LAAndValidator(
new LAValidationLib.LAStringNotEmptyValidator( nome ),
new LAValidationLib.LAStringLengthValidator( nome, 300, LAValidationLib.LAStringLengthOperator.LessOrEqual ) );
if (!validator.IsValid( ))
{
throw new ArgumentException( "Nome não pode ser nulo ou vazio" );
}
}
}
Como é possível verificar, a utilização da estratégia anterior tornou o código mais elegante e permitiu a fácil composição de várias regras de validação sem perder legibilidade. Agora que já temos a livraria de validação definida, está na hora de nos concentrarmos na sua utilização a partir do ASP.NET. Como a rotina de validação foi toda escrita em C#, é fácil notar que a sua utilização só poderá ser feita no lado servidor. Contudo, a validação dos formulários ASP.NET pode ser feita quer no lado cliente, quer no lado servidor. A forma mais fácil de reutilizarmos o código passa pela utilização de um validator que recorra ao servidor para conseguir efectuar a validação do lado cliente. Felizmente para nós, a integração de call-backs no ciclo de vida da página facilita a construção de um controlo deste tipo.
LACallbackValidator: construção de um validator que efectua validação no lado cliente através de call-backs
A construção de um validator personalizado é razoavelmente simples: temos apenas de herdar da classe BaseValidator e efectuar o override do método EvaluateIsValid (este método é o responsável pela validação efectuada no lado servidor). No lado cliente (isto é, no HTML enviado para o browser), um validator é sempre representado por um SPAN. O funcionamento dos validators no lado cliente resulta da combinação de vários aspectos. A página injecta código responsável por introduzir um array com todos os validators existentes na página. Este array é usado pela framework jscript para efectuar a validação dos controlos associados aos validators. É importante salientar que cada validator pode possuir um conjunto de propriedades com nomes conhecidos. Por exemplo, a propriedade evaluationfunction define o nome da função script responsável por efectuar a validação dos dados no lado cliente (refira-se ainda que, para além desta, existem outras, como por exemplo, validateemptytext, usada para indicar se o validator deve entrar em acção quando não possui texto - esta propriedade é apenas usada por alguns validators).
A construção do validator de call-back é ligeiramente mais complexa do que poderia parecer à primeira vista. O grande problema reside no facto da comunicação efectuada através do call-back com o servidor ser sempre assíncrona de forma a não bloquear o browser. Este tipo de comportamento não é adequado ao validator já que , nestas situações, o método que efectua a validação tem sempre de indicar se o valor existentes no controlo validado é (ou não) válido.
A afirmação do parágrafo anterior pode parecer incorrecta, já que o método GetCallbackEventReference (exposto pela classe ClientScriptManager) recebe um parâmetro do tipo boolean que, segundo a documentação, indica se a operação deve ou não ser executada de forma síncrona. Apesar disso, a verdade é que a comunicação com o servidor é sempre feita de forma assíncrona quando usamos os call-backs tradicionais introduzidos pelo ASP.NET 2.0.
Neste caso, interessa-nos reaproveitar a infraestrutura associada aos call-backs, mas queremos usá-la de forma sincrona. A solução para este problema reside na reutilização de algum do código jscript normalmente usado nestas situações com a alteração do modo de invocação de forma a que a chamada seja efectuada de forma sincrona. Durante um call-back, a página envia para o servidor vários dados: tipicamente, os dados contidos no viewstate e a informação associada ao call-back. Portanto, se quisermos reaproveitar a infraestrutura do lado servidor, temos de, obrigatoriamente, enviar estes dados. Com recurso ao reflector, facilmente obtemos uma função capaz de reproduzir o que normalmente acontece com as chamadas de call-back iniciadas pela página:
function LACallbackValidator_StartCallback(id,args)
{
var postData = __theFormPostData +
"__CALLBACKID=" + WebForm_EncodeCallback(id) +
"&__CALLBACKPARAM=" + WebForm_EncodeCallback(args);
if (theForm["__EVENTVALIDATION"])
{
postData += "&__EVENTVALIDATION=" + WebForm_EncodeCallback(theForm["__EVENTVALIDATION"].value);
}
var xmlRequest;
try
{
xmlRequest = new XMLHttpRequest();
}
catch(e)
{
try
{
xmlRequest = new ActiveXObject("Microsoft.XMLHTTP");
}
catch(e)
{
}
}
xmlRequest.open("POST", theForm.action, false);
xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlRequest.send(postData);
var response = xmlRequest.responseText;
if (response.charAt(0) == "s")
{
return( response.substring(1) == "true" );
}
else if( response.charAt(0) == "e")
{
//error during callback!
alert (response.substring(1));
return false;
}
else
{
var separatorIndex = response.indexOf("|");
if (separatorIndex != -1)
{
var validationFieldLength = parseInt(response.substring(0, separatorIndex));
if (!isNaN(validationFieldLength))
{
var validationField = response.substring(separatorIndex + 1, separatorIndex + validationFieldLength + 1);
if (validationField != "")
{
var validationFieldElement = theForm["__EVENTVALIDATION"];
if (!validationFieldElement)
{
validationFieldElement = document.createElement("INPUT");
validationFieldElement.type = "hidden";
validationFieldElement.name = "__EVENTVALIDATION";
theForm.appendChild(validationFieldElement);
}
validationFieldElement.value = validationField;
}
return(response.substring(separatorIndex + validationFieldLength + 1) == "true");
}
}
}
}
Apesar de extenso, o método não é muito complexo. Em primeiro lugar, temos de obter os dados enviados no call-back. Como é possível verificar, recorremos à variável theFormPostData, introduzida automaticamente pela plataforma, para obtermos os dados mantidos nos controlos existentes no interior do formulário. Note-se que esta variável é inicializada automaticamente pela página através da execução do método WebForm_InitCallback (injectado na página sempre que algum dos controlos aí existentes invoca o método GetCallbackEventReference exposto pela classe ClientScriptManager). Após obtermos os dados a enviar, instanciamos o objecto XMLHttpRequest e efectuamos o pedido ao servidor de forma síncrona (note-se que o valor false é passado como último parâmetro ao método open). A instanciação do objecto XMLHttpRequest é mantida no interior de um bloco try/catch de forma a garantir a sua correcta instanciação no IE 6.X (nas versões anteriores ao IE 7, este objecto é implementado através de um controlo ActiveX).
A resposta enviada pelo servidor é obtida a partir da propriedade responseText do objecto XMLHttpRequest. Como no servidor reaproveitámos toda a infraestrutura introduzida pelo ASP.NET 2.0, então temos de tratar a resposta como se estivéssemos perante um call-back executado pela plataforma (daí a quantidade de validações efectuadas). Neste caso, o método executado no lado servidor retornará apenas true ou false, já que o objectivo desse método é validar o valor enviado a partir do cliente.
Resumindo, a função anterior recebe o id do controlo que deve tratar o call-back no lado servidor e o valor que deve ser passado como argumento a esse método. Após efectuar a operação de call-back de forma síncrona, o método retorna o valor true ou false de forma a reflectir os cálculos efectuados no lado servidor.
Apresentada que está a parte cliente, falta apenas falar acerca da implementação do código servidro do controlo. A estratégia usada é muito semelhante ao que acontece com o CustomValidator, isto é, a validação no lado servidor é efectuada através do tratamento de um evento (designado de ServerValidate). De forma a permitir a personalização dos valores enviados a partir do cliente, o controlo permite a definição de uma função javascript que recebe, como único argumento, o valor do controlo validado. O valor retornado por esta função é enviado para o lado servidor onde será realmente validado. Vamos então analisar os principais métodos do controlo:
protected override bool EvaluateIsValid()
{
string valueToValidate = "";
string controlToValidate = this.ControlToValidate;
if ( !string.IsNullOrEmpty( controlToValidate ) )
{
valueToValidate = this.GetControlValidationValue( controlToValidate );
if ( string.IsNullOrEmpty( valueToValidate ) && !this.ValidateEmptyText )
{
return true;
}
}
return PerformValueEvaluation( valueToValidate );
}
O método EvaluateIsValid é invocado no lado servidor de forma a garantir que o controlo validado possui valores válidos. Tal como tínhamos referido, este método recorre a um evento para efectuar a validação dos dados (o método PerformValueEvaluation limita-se a gerar o evento responsável pela obtenção da validação do valor recebido - uma nota adicional para referir que este método também é executado durante um call-back).
protected override void AddAttributesToRender( System.Web.UI.HtmlTextWriter writer )
{
base.AddAttributesToRender( writer );
if ( this.RenderUplevel )
{
this.Page.ClientScript.RegisterExpandoAttribute( this.ClientID, "evaluationfunction",
"LACallbackCustomerValidatorEvaluateIsValid", true );
if ( !string.IsNullOrEmpty( ClientStartUpFunction ) )
{
this.Page.ClientScript.RegisterExpandoAttribute( this.ClientID, "startupvalidationfunction", this.ClientStartUpFunction, true );
}
if ( this.ValidateEmptyText )
{
this.Page.ClientScript.RegisterExpandoAttribute( this.ClientID, "validateemptytext", "true", true );
}
this.Page.ClientScript.RegisterExpandoAttribute( this.ClientID, "uniqueId", this.UniqueID, true );
}
}
O método AddAttributesToRender é responsável por adicionar os atributos que serão usados pela framework de validação javascript no lado cliente. Neste caso, o método LACallbackCustomerValidatorEvaluateIsValid é usado sempre que o validator for chamado a dar o seu parecer sobre a validade do valor existente no controlo validado (na prática, este será o método responsável por iniciar o call-back através da invocação do método javascript apresentado na secção anterior). Para além do atributo evaluationfunction, o controlo de validação regista ainda os atributos validateemptytext, uniqueId e startupvalidationfunction. O primeiro(validateemptytext) é usado para indicar se a validação deve ser efectuada quando o controlo validado não possui texto; o segundo (uniqueId) é usado para manter o Id do controlo de forma a identificá-lo durante uma operação de call-back (note-se que é necessário recorrer à propriedade UniqueId, já que pretendemos identificar o controlo no lado servidor e não no lado cliente); finalmente, o atributo startupvalidationfunction identifica uma eventual função responsável por modificar os dados enviados durante a operação de call-back (como referimos anteriormente, a possibilidade de definir uma função deste género permite-nos modificar ou adicionar informação ao valor que será enviado para o lado servidor call-back).
protected override void OnPreRender( EventArgs e )
{
base.OnPreRender( e );
this.Page.ClientScript.RegisterClientScriptResource( this.GetType( ), "LACallbackValidator.js" );
this.Page.ClientScript.GetCallbackEventReference( this, "", "", "" );
}
O método OnPreRender serve apenas dois propósitos: introduzir o ficheiro script que contém os métodos javascript usados pelo controlo e invocar o método GetCallbackEventReference. Aliás, repare-se que neste caso, o método só é invocado porque este, internamente, procede ao registo da função WebForm_InitCallback que, como vimos, é responsável pela correcta inicialização da variável javascript theFormPostData. Para terminarmos a análise do controlo, falta apenas apresentarmos o método LACallbackCustomerValidatorEvaluateIsValid: ele é responsável por decidir (no lado cliente) se o valor é ou não válido:
function LACallbackCustomerValidatorEvaluateIsValid( val )
{
var value = "";
if (typeof(val.controltovalidate) == "string")
{
value = ValidatorGetValue( val.controltovalidate );
if ((ValidatorTrim(value).length == 0) &&
((typeof(val.validateemptytext) != "string") || (val.validateemptytext != "true")))
{
return true;
}
}
var args = { Value:value, IsValid:true };
var initialcb = value;
try
{
if (typeof(val.startupvalidationfunction) == "string")
{
var auxFun = val.startupvalidationfunction + "('" + initialcb + "') ;"
initialcb = eval( auxFun );
}
}
catch(err)
{
alert( err.description );
}
//var ret = LACallbackValidator_StartCallback( val.id, initialcb );//eval( val.callbackValidatorMethod + "(" + initialcb +");" );
var fun = val.callbackValidatorMethod + "('" + val.uniqueId + "','" + initialcb + "');";
var ret = eval( fun );
val.IsValid = ret;
return val.IsValid;
}
Mais uma vez, reaproveitamos a API cliente fornecida pela plataforma para obter o valor do controlo a validar. Note-se como antes de iniciarmos o call-back (através da invocação do método LACallbackValidator_StartCallback apresentado previamente), tentamos executar um eventual método cliente definido através da propriedade servidor ClientStartUpFunction (que, como vimos, tem por objectivo permitir a modificação dos valores enviados para o servidor).
Conclusões finais
E com poucas linhas de código desenvolvemos uma mini-plataforma de validação que inclui um conjunto de rotinas genéricas de validação que permitem a fácil composição de regras e um controlo de validação capaz de efectuar esse tipo de operações através de call-backs executados de forma sincrona. O sample que acompanha este artigo apresenta uma página que ilustra a utilização desta mini-plataforma e está disponível na área de downloads do site.
Por favor enviem-me as vossas opiniões/sugestões/críticas/correcções
para progC@netmadeira.com.
Fiquem bem e boa programação! Até à próxima.
Leiam o meu blog em: http://weblogs.pontonetpt.com/luisabreu