Agora é oficial e público. Fui premiado com o prémio MVP em ASP/ASP.NET.
Este prémio acarreta uma enorme responsabilidade mas também indica que o caminho que tenho seguido é um caminho de sucesso e com futuro.
Durante o ano que passou aprendi muitas coisas (algumas partilhei com vocês, outras talvez partilhe no futuro) mas também cometi alguns erros (tão necessários para evoluir como os sucessos).
Para o próximo ano só posso prometer que vou continuar com as mesmas prioridades: aprender, ajudar e partilhar.
Obrigado a todos.
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.
Um dos problemas mais reportados no âmbito do ASP.NET é o ViewState, i.e., os problemas relacionados com o seu uso e os problemas que surgem quando o mesmo é desligado.
É bem verdade que o ViewState pode atingir uma dimensão ridiculamente elevada e afectar o desempenho da aplicação mas não podemos esquecer os seus beneficios.
O ViewState surgiu como resposta ao pedido da comunidade para criar um mecanismo infraestrutural de persistência de estado da aplicação.
Se nos recordarmos do tempo em que faziamos ASP, uma das preocupações mais comuns era a manutenção do estado da aplicação para permitir encadear correctamente as regras de negócio.
Ora o ViewState criou uma abstracção sobre o estado. Se o ViewState estiver activo os controlos Server-Side conseguem reagir à mudança e disparam eventos. Foi esta abordagem Event-Driven que revolucionou a forma de fazer aplicações Web.
Para além disto, uma das preocupações que tinhamos (e ainda temos) era a segurança do estado. O ViewState podendo, “Out-Of-The-Box”, ser encriptado também nos resolve este problema.
Dito isto, parece óbvio que o ViewState não nasceu fruto do acaso e permitiu agilizar o desenvovimento de aplicações Web.
Então qual a razão de ser tão frequentemente usado como desculpa para justificar o não uso do ASP.NET?
Bom, a meu ver, existem vários factores que contribuem para esta situação:
- documentação pobre
- alteração de paradigma
Na verdade, julgo ser a combinação das duas. Quando nos encontramos fora da área de conforto e não conseguimos de uma forma célere respostas às nossas questões acabamos naturalmente por criar uma barreira e evitamos repetir o cenário.
Compreender o ViewState implica conhecer os segredos do ciclo de vida da página e a forma como os controlos estão envolvidos na mesma e este conhecimento não é trivial, está disperso e nem sempre é facil de estabelecer as pontes.
Ainda assim, com empenho e persistência q.b. não é dificil compreender o conceito, a implementação, as suas virtudes e acima de tudo os seus defeitos.
Atingido este ponto é possivel, contrapor e ultrapassar a maioria das limitações apontadas ao ViewState.
Através:
- da alteração ao meio de persistência do ViewState – passar da serialização para HTML para persistência em BD ou outros
- do desligar do ViewState dos controlos Bindable (GridView, DropdownList) sem que o controlo perca funcionalidade
- da redução a quantidade de informação persistida no ViewState por cada Controlo
é possivel reduzir a dimensão total do viewstate para valores razoaveis ou mesmo desprezaveis.
No ASP.NET 2.0 foi introduzido o conceito de ControlState, i.e., o estado minimo que o controlo precisa para poder reagir à mudança. Este estado não é passivel de ser desligado mas é por definição muito menor que o ViewState.
Com esta introdução pensar-se-ia que podiamos desligar o ViewState de qualquer controlo que os eventos de mudança continuariam a funcionar … mas não … o ControlState é apenas usado por um grupo reduzido de controlos e na prática o modelo Event-Driven “Out-Of-The-Box” do ASP.NET continua a depender do uso de ViewState.
Mas não desanimem, é muito facil extender os controlos para usar este conceito e desligar o ViewState sem impactos colaterais. Num próximo post falarei sobre o ControlState e sobre os cenários que descrevi.
É verdade, a Typemock tem vindo a atribuir licenças grátis do novo Isolator 2010. Para se habilitarem basta:
- ter uma conta no Twitter
- seguir a Typemock no Twitter: @Typemock
- fazer twit do seguinte: just entered for a chance to win a free Isolator @typemock RT it to enter the double drawing.
Não percam tempo, o último sorteio é amanhã, 24/02, e serão atribuidas 2 licenças do Isolator.
Boa sorte …
Desde 2002/2003 que não tinha qualquer contacto com o desenvolvimento em WinForms, desde então posso-me intitular um maníaco do ASP.NET, mas o Ano Novo reservou-me um desafio inesperado: construir um pacote Visual Studio 2008 Integration Package (VSIP) com um Editor avançado para uma estrutura de metadados com suporte em xml .
Para aqueles que já trabalham com VSIP ou VS Add-ins não será surpresa o facto de me ter visto “obrigado” a recuperar o conhecimento à muito perdido sobre os controlos WinForms.
Talvez seja apenas fruto da minha pouco estruturada memória ou talvez nunca tenha reparado antes, mas o controlo TabControl quando opera em modo vertical – quer à Esquerda quer à Direita – apresenta uma experiência ao utilizador francamente má (na maioria dos casos nem apresenta o texto do Tab).
Sim, isto não é problema inultrapassavel, na realidade o controlo TabControl possui os pontos de extensibilidade necessários para ultrapassar esta limitação e existe até um artigo na Msdn com a receita para resolver este comportamento : How to: Display Side-Aligned Tabs with TabControl.
Provavelmente sou apenas eu que pensa assim mas na realidade eu esperava que este tipo de comportamento estivesse desde logo disponível e totalmente funcional, tanto mais que este é um comportamento comum e usado na maioria das aplicações WinForms.
A longa espera pelo lançamento da versão oficial do novo .NET Reflector acabou.
Tendo sido “Earlier Adopter” e “Usability Tester” desta nova versão, posso afirmar que a RegGate dedicou especial atenção ao seu desenvolvimento, especialmente ao novo produto .NET Reflector Pro.
Este novo produto (€145) integra a tecnologia do .NET Reflector no Visual Studio e permite fazer debug em referência externas mesmo quando o código fonte.
“Ok … mas para isso não preciso deste produto” - dirão alguns de vocês e têm razão. Mas o que cativa neste produto é a forma como está integrado no ambiente de desenvolvimento (Visual Studio 2005, 2008 ou 2010) e a forma fácil e intuitiva de o usar.
Tal já disse, eu uso este produto desde as versões Beta e estou viciado ;) … Experimenta.
Como sempre as minhas conversas com o Paulo Morgado são longas e frutuosas, e como não podia deixar de ser a mais recente conversa também o foi.
Como bem sabem os membros da comunidade pontoNETpt a segurança dos blogs foi melhorada com a recente introdução de Captcha nas funcionalidades criticas:
- criação de utilizador
- adição de comentários.
Tenho estado a preparar um post precisamente sobre esta intervenção e, como é habitual, ao estruturar o texto e ao preparar o código acabei por fazer alterações e levantar algumas questões. Foi precisamente sobre uma dúvida que surgiu a tal conversa com o Paulo e este post.
Foreach
O código em causa era o seguinte:
private Control MyMethod(T wrapper)
{
Control anchorControl = null;
foreach (string validAnchorId in this.ValidAnchorIds)
{
anchorControl = CSControlUtility.Instance().FindControl(wrapper, validAnchorId);
if (anchorControl != null)
{
break;
}
}
return anchorControl;
}
Como podem ver é um pedaço de código absolutamente comum e que todos nós usamos.
List<string>.Find
A minha questão relacionava-se com a possibilidade de substituir o foreach pelo método List<string>.Find:
private Control MyMethod(T wrapper)
{
Control anchorControl = null;
this.ValidAnchorIds.Find(
delegate(string validAnchorId)
{
anchorControl = CSControlUtility.Instance().FindControl(wrapper, validAnchorId);
return (anchorControl != null);
});
return anchorControl;
}
Como podem ver, não parece mal … continua a ser legível e … naturalmente compila.
Ainda assim, não estava satisfeito … como se costuma dizer … não me “cheirava”. Basicamente eu descartava o resultado do Find() e além disso usava no delegate uma variável do scope do método.
Foi nesta altura que o Paulo entrou em campo e com a ajuda dele e usando o Reflector para mostrar o código em .NET 1.0 obtive o seguinte código esclarecedor:
[System.Runtime.CompilerServices.CompilerGenerated]
private sealed class <>c__DisplayClass1
{
// Fields
public Control anchorControl;
public T wrapper;
// Methods
public bool <MyMethod>b__0(string validAnchorId)
{
this.anchorControl = CSControlUtility.Instance().FindControl(this.wrapper, validAnchorId);
return (this.anchorControl != null);
}
}
private Control MyMethod(T wrapper)
{
<>c__DisplayClass1<T> CS$<>8__locals2 = new <>c__DisplayClass1<T>();
CS$<>8__locals2.wrapper = wrapper;
CS$<>8__locals2.anchorControl = null;
this.ValidAnchorIds.Find(new Predicate<string>(CS$<>8__locals2.<GetAnchorControl>b__0));
return CS$<>8__locals2.anchorControl;
}
Ao olhar para este código, abstraindo a nomenclatura, podemos ver que o uso duma variavel pertencente ao scope do método obriga à criação duma nova classe - <>c__DisplayClass1. É através desta classe que o Predicate<string> tem acesso à variável do método - anchorControl.
Simplificando, o meu delegate é substituido por um Predicate<string> mas o que dava mesmo jeito era uma Func<string,Control>, isto é, o que dava mesmo jeito era que a iteração sobre a List<string> devolve-se uma instância de Control.
LINQ To Objects
Hummm … uma vez mais o Paulo interviu e com os devidos alertas lembrou-me da possibilidade de usar LINQ To Objects. Neste cenário o meu método ficou assim:
private Control MyMethod(T wrapper)
{
var q = from anchorId in this.ValidAnchorIds
let anchorControl = CSControlUtility.Instance().FindControl(wrapper, anchorId)
where anchorControl != null
select anchorControl;
return q.FirstOrDefault();
}
Notar que existem várias forma obter o mesmo resultado usando LINQ To Objects mas isso fica para um próximo post.
Agora já não é um método legível por qualquer programador, requer conhecimentos de LINQ naturalmente, mas ainda assim atingido esse conhecimento não é dificil perceber o que se pretende:
“Iterar sobre os items da lista até que seja encontrado um anchorControl não nulo e caso nenhum seja encontrado devolver o valor por omissão do tipo esperado”
Se olharmos, tal como anteriormente, para a versão .NET 1.0 deste método vemos o seguinte:
private Control MyMethod(T wrapper)
{
<>c__DisplayClass6<T> CS$<>8__locals7 = new <>c__DisplayClass6<T>();
CS$<>8__locals7.wrapper = wrapper;
if (WrappedFormBaseCaptchaAdapter<T>.CS$<>9__CachedAnonymousMethodDelegate4 == null)
{
WrappedFormBaseCaptchaAdapter<T>.CS$<>9__CachedAnonymousMethodDelegate4 = new Func<<>f__AnonymousType0<string, Control>, bool>(WrappedFormBaseCaptchaAdapter<T>.<MyMethod>b__2);
}
if (WrappedFormBaseCaptchaAdapter<T>.CS$<>9__CachedAnonymousMethodDelegate5 == null)
{
WrappedFormBaseCaptchaAdapter<T>.CS$<>9__CachedAnonymousMethodDelegate5 = new Func<<>f__AnonymousType0<string, Control>, Control>(WrappedFormBaseCaptchaAdapter<T>.<MyMethod>b__3);
}
return this.ValidAnchorIds
.Select(new Func<string, <>f__AnonymousType0<string, Control>>(CS$<>8__locals7.<MyMethod>b__1))
.Where(WrappedFormBaseCaptchaAdapter<T>.CS$<>9__CachedAnonymousMethodDelegate4)
.Select(WrappedFormBaseCaptchaAdapter<T>.CS$<>9__CachedAnonymousMethodDelegate5)
.FirstOrDefault<Control>();
}
Como podem observar é bastante mais complexa e a quantidade de código gerado pelo compilador é “assustadora” mas, uma vez mais, para os que conhecem LINQ To Objects não é de todo surpreendente.
Vamos começar pelo principo, i.e., pela classe <>c__DisplayClass6
private sealed class <>c__DisplayClass6
{
// Fields
public T wrapper;
// Methods
public <>f__AnonymousType0<string, Control> <GetAnchorControl>b__1(string anchorId)
{
return new { anchorId = anchorId, anchorControl = CSControlUtility.Instance().FindControl(this.wrapper, anchorId) };
}
}
É instanciada e carregada com o parâmetro wrapper e o único método que possui é usado no método de extensão Select, o mesmo é dizer que a colecção inicial será iterada e para cada item será executado o método em causa. Como resultado de cada iteração resultará um tipo anónimo que contém o anchorName e o anchorControl correspondente e o resultado final do Select será um IEnumerable<T> sendo T o tipo anónimo.
Continuando … vamos executar o método de extensão Where, o qual recebe como argumento uma Func<TSource, bool>, isto é, um método que recebe um TSource e devolve um booleano. O método Where filtra o IEnumerable<TSource> inicial por forma a conter apenas os items para os quais a execução da Func<TSource, bool> devolve ‘true’.
No caso concreto, o TSource é o tipo anónimo criado no Select e o código é:
private static bool <GetAnchorControl>b__2(<>f__AnonymousType0<string, Control> <>h__TransparentIdentifier0)
{
return (<>h__TransparentIdentifier0.anchorControl != null);
}
É facil identificar o que este método está a realizar e o resultado da execução do Where será um IEnumerable<T> no qual todos os items possuem anchorControl != null.
A próxima execução será outro Select, que desta feita executa o seguinte método para cada item:
private static Control <GetAnchorControl>b__3(<>f__AnonymousType0<string, Control> <>h__TransparentIdentifier0)
{
return <>h__TransparentIdentifier0.anchorControl;
}
Uma vez que o método devolve apenas o Control concluimos que o Select está a construir uma IEnumerable<Control>, isto é, uma lista de controlos não nulos e cujos Ids estavam definidos na lista original.
Para terminar é executado o método de extensão FirstOrDefault que como o nome indica é apenas responsável por devolver o primeiro Control da lista ou caso a mesma esteja vazia devolve ‘null’ pois este é o valor por omissão dos tipos por referência.
Das 3 abordagens esta parece de facto a mais complexa e potencialmente menos performante, mas será mesmo?
Conclusão
Resolvi então efectuar uma bateria de testes e avaliar a performance nos seguintes cenários:
A tabela anterior permite apurar o seguinte:
- o cenário foreach é o mais performante quando a operação a realizar para cada item for muito rápida (ex: comparações)
- para operações lentas não são visiveis diferenças significativas entre os vários cenários
- a degradação da performance não apresenta uma relacão directa com o aumento da dimensão da lista
O mesmo é dizer que para cenários comuns devemos usar o foreach ou mesmo o List<>.Find, ficando a decisão ao critério de cada um de nós.
Já o cenário LINQ To Objects deve ser apenas usado apenas nas situações em que a performance não é um factor crítico ou as operações envolvidas são intrinsicamente pouco performantes.
E pronto … como é muito comum em Software a resposta é … depende … quando tiverem dúvidas façam uma aplicação e testem o vosso caso. Os preguiçosos podem usar a aplicação que eu usei para efectuar os meus testes.
Embora um pouco tarde, aqui fica o meu testemunho sobre este evento.
Em relação à grande maioria dos participantes, eu cheguei ao evento com uma grande vantagem.
A minha vantagem foi a participação no PreRemix, no qual foram ensaiadas as sessões apresentadas pelo Luis A. Martins, Pedro Félix e pelo José António Silva.
Com esta vantagem na bagagem concentrei-me fundamentalmente em aproveitar a presença em Portugal do Simon Guest e do Brad Abrams.
O meu percurso passou por todas as “tracks”:
As sessões do Simon não desapontaram, antes pelo contrário, com bom ritmo e bastantes interessantes. A primeira sessão do Simon está disponivel aqui.
O mesmo não posso dizer em relação ao Brad Abrams, esperava um sessão de nivel superior mas acabou por ser uma sessão de introdução. Estou certo que foi util para a maioria da audiência que me pareceu fundamentalmente ser constituida por estudantes. Seja como for o conteúdo está disponivel aqui.
Foi na ultima sessão do dia que a escolha foi mais dificil, por um lado podia assistir à apresentação sobre MVC do Nuno Silva por outro podia aproveitar a presença do Los Reyes.
Acabei por optar por assistir à sessão do Los Reyes e acabou por ser aquilo que eu pensava – exotérica - mas ainda assim proveitosa, pois novas perspectivas sobre a tecnologia são sempre bem vindas.
E pronto, já estou em contagem decrescente para o Remix 10.
Antes de mais, convém salientar que esta não é uma questão nova, aliás, podemos ler aqui que a Microsoft considera esta questão um comportamento “por desenho” e não um problema.
O problema, eu considero um problema, ocorre quando simultaneamente se usa uma GridView com um SQLDataSource e uma SortExpression que contém uma “SortDirection”, isto é, ‘ASC’ ou ‘DESC’ (ex: ‘Coluna1 DESC’).
Nesta condição, se realizarmos a mesma ordenação duas vezes consecutivas, a segunda execução origina um erro/excepção do tipo
“System.IndexOutOfRangeException: Cannot find column Coluna1 DESC.”
Na ligação que referi anteriormente é sugerida uma resolução mas, embora simples, requer sempre o registo do evento Sorting da GridView e o ajuste da alteração em função das colunas em questão. Ora, falando de mim, este tipo de resolução é sempre critica pois por norma acabo por me esquecer de a realizar até o erro surgir.
Assim, resolvi adicionar a correcção para este comportamento à minha GridView base e, embora seja uma alteração ligeiramente mais complicada, vai permite-me eliminar esta preocupação sempre que lido com a GridView.
Redefini o método OnSorting da GridView por forma a alterar o valor da propriedade SortDirection da instância de GridViewEventArgs.
Para tornar esta solução genérica reproduzi parcialmente o método privado ParseSortString da DataTable por forma a conseguir avaliar se o valor da propriedade SortExpression contém algum dos termos ‘ASC’ ou ‘DESC’.
Aqui está o código resultante:
public class GridView : global::System.Web.UI.WebControls.GridView
{
protected override void OnSorting(GridViewSortEventArgs e)
{
if (!string.IsNullOrEmpty(this.SortExpression))
{
if (this.SortExpression.Equals(this.SortExpression))
{
bool isMultipleSortExpression;
SortDirection? sortDirection = GetSortDirection(this.SortExpression, out isMultipleSortExpression);
if (sortDirection.HasValue)
{
// To undo bug in GridView.HandleSort(string sortExpression) and then in GridView.CreateDataSourceSelectArguments()
e.SortDirection = SortDirection.Ascending;
}
}
}
base.OnSorting(e);
}
private SortDirection? GetSortDirection(string sortExpression, out bool isMultipleSortExpression)
{
SortDirection? sortDirection = null;
isMultipleSortExpression = false;
string[] strArray = sortExpression.Split(new char[] { ',' });
for (int i = 0; i < strArray.Length; i++)
{
string strA = strArray[i].Trim();
int length = strA.Length;
if ((length >= 5) && (string.Compare(strA, length - 4, " ASC", 0, 4, StringComparison.OrdinalIgnoreCase) == 0))
{
sortDirection = SortDirection.Ascending;
}
else if ((length >= 6) && (string.Compare(strA, length - 5, " DESC", 0, 5, StringComparison.OrdinalIgnoreCase) == 0))
{
sortDirection = SortDirection.Descending;
}
if (!sortDirection.HasValue)
{
break;
}
}
if (sortDirection.HasValue)
{
if (strArray.Length > 1)
{
isMultipleSortExpression = true;
}
}
return sortDirection;
}
}
Com esta implementação a excepção não ocorre mas, tal como seria de esperar, também não ocorre a alternância entre ordenação ascendente e descendente. Neste caso a ordenação está fixa e é definida no valor da SortExpression.
Usem e abusem!
No dia 2 de Outubro realiza-se em Portugal o ReMIX 09.
O ReMIX 09 é um evento satélite do MIX 09 e pretende apresentar o melhor do que por lá se passou.
Eu vou lá estar … e tu?
Se é membro da nossa comunidade beneficias de 20 % de desconto. Para saberes mais consulta este post do Paulo Morgado.
Espero encontrar-te lá.
Já não é a primeira vez que escrevo aqui da Typemock e dos seus produtos e é sobre um novo produto que eu agora vou escrever.
Recentemente a Typemock lançou o Racer, um produto cuja principal finalidade é detectar os potenciais ‘Deadlock’ em ambientes ‘Multi-Thread’.
Notem que embora os ambientes ‘Multi-Thread’ sejam vulgarmente associados a cenários ‘Multi-Core’, a verdade é que se considerarmos que a maioria das aplicações ASP.NET que são servidas por um IIS correm numa ‘Thread Pool’ com múltiplas Threads (normalmente algumas dezenas), então os ambientes ‘Multi-Thread’ são mais comuns do que é vulgarmente assumido.
Assim, é válido assumir que numa aplicação ASP.NET podemos ter problemas de ‘DeadLock’. Se considerarmos que em aplicações mais complexas é necessário muitas vezes implementar mecanismos próprios de Cache e muitas vezes também de Session este cenário começa a emergir e a ser um preocupação válida e consciente.
Ora o Typemock Racer era a ferramenta que faltava. Agora podemos compor testes de concorrência com a mesma facilidade com que compomos testes unitários, bastando atributar o método com [ParallelInspection].
Não há agora motivos para justificar o porquê dos problemas de concorrência serem apenas identificados praticamente no fim do ciclo de desenvolvimento de uma aplicação:
- durante os testes de carga
- ou, no pior cenário, em ambiente de produção
Com o toolkit completo da Typemock eu posso agora:
- criar mais facilmente e mais rapidamente testes unitários – usando o Isolator
- criar testes unitários dedicados para ambiente ASP.NET (simular o ciclo de vida da página, dos controlos, simular eventos …) – usando o add-in Ivonna para o Isolator
- criar testes de concorrência para detectar deadlocks – usando o Racer
Isto não significa que o meu código é isento de erros e problemas … mas eu estou muito mais confortável com a qualidade do código que produzo.
Se quiserem saber um pouco mais podem visitar este post do Roy Osherove (contem um pequeno video).
Antes de avançar mais, vamos só refrescar um pouco a memória e recuperar o que é uma String. Optei por escrever String pois a nomenclatura correcta em português é tão óbvia que deixa transparecer de imediato a definição básica: uma String é uma cadeia/sequência de caracteres.
O tipo String tem as seguintes caracteristicas:
- é um tipo por referência
- é imutável
- pode ser nulo
- redefine o operador ‘==‘
Literal
Todas as Strings que estão definidas no código são denominadas de Literais e podem ser de dois tipos: regulares ou “verbatim”. As Strings “verbatim” são todas aquelas que estão prefixadas com o caracter @ e que por isso podem conter quase todas combinações de caracteres. As Strings regulares são todas as restantes.
Nas entranhas do .NET - “Intern Pool”
O .NET possui um conceito que está pouco divulgado e é designado por “Intern Pool”. Este conceito não é mais do que um conjunto de strings que podem ser referenciadas multiplas vezes.
Todos os Literais são automaticamente adicionados a este conjunto e por isso, sempre que se referência o mesmo Literal é-nos devolvida uma referência para a mesma string, i.e., neste conjunto de strings não há duplicados todos os literais iguais referenciam a mesma string neste conjunto.
O CLR usa este mecanismo para minimizar as necessidades de armazenamento de strings.
Adicionalmente é ainda possivel adicionar a este conjunto, outras strings criadas dinâmicamente durante a execução do programa. Para tal usamos o método String.Intern() que para além de adicionar a string ao conjunto (caso não exista) devolve uma referência para a string desejada.
Face ao que foi dito, percebe-se de imediato que o uso de “Intern Pool” também tem alguns “Se”:
- A memória alocada para a Intern Pool só é libertada quando o CLR terminar
- O uso do metodo String.Intern() obriga à criação da string, pelo que será alocada memória para a nova string (embora o GC a vá libertar mais tarde)
Beneficio
Ora então como é que o uso de String.Intern() nos pode trazer beneficios?
Simples … como já referi o tipo String redefine o operador ‘==’, vamos dar uma olhadela:
public static bool Equals(string a, string b)
{
return ((a == b) || (((a != null) && (b != null)) && EqualsHelper(a, b)));
}
Como podemos observar, o primeiro teste realizado é verificar se as referências são iguais.
Como também já foi dito, o método String.Intern() devolve uma referência para uma String e podemos assim forçar que a comparação de Strings seja feita por referência.
Para tal basta que ambos os operandos da operação estejam na “Intern Pool”.
Outro dado que podemos já extrair é que só temos beneficio se ambos os operandos tiverem a mesma referência e a comparação terminar de imediato na primeira condição.
A questão que se coloca de imediato é: Será que ganhamos mesmo algo com isto?
Nada a que uma bateria de testes não responda.
Testes
Para o teste considerei um cenário que todos nós usamos com frequência: Importação de uma string do ficheiro de configuração e uso da mesma numa regra de negócio.
Para manter as coisas simples a minha regra de negócio é apenas a comparação com um literal (que conveniente ;-)):
static void Main()
{
// Get runtime string.
string s1 = ConfigurationManager.AppSettings["mykey"];
// Get string pool reference to string.
string s2 = string.Intern(s1);
// Three loops:
// - Repeat benchmark 10 times
// - Repeat inner loop 10000 times
// - Repeat 2 tests 10000 times
int m = 10000;
int totalIterations = 10;
long totalInternTime = 0;
long totalStandardTime = 0;
for (int v = 0; v < totalIterations; v++)
{
int d = 0;
long t1 = Environment.TickCount;
// Test regular string.
for (int i = 0; i < m; i++)
{
for (int a = 0; a < m; a++)
{
if (s1 == "my value")
{
d++; // true
}
}
}
long t2 = Environment.TickCount;
// Test interned string.
for (int i = 0; i < m; i++)
{
for (int a = 0; a < m; a++)
{
if (s2 == "my value")
{
d++; // true
}
}
}
long t3 = Environment.TickCount;
totalStandardTime += (t2 - t1);
totalInternTime += (t3 - t2);
Console.WriteLine(string.Format("{0},{1}", (t2 - t1), (t3 - t2)));
}
Console.WriteLine("=======Media========");
Console.WriteLine(string.Format("{0},{1}", totalStandardTime / totalIterations, totalInternTime / totalIterations));
Console.WriteLine("=======Ratio========");
Console.WriteLine(string.Format("{0}", (totalStandardTime / totalInternTime)));
Console.WriteLine("Press any key to end");
}
Os resultados são explicitos:
Usando o metodo String.Intern() o código executa quase 4x mais rápido … mas atenção … neste teste só consideramos o melhor cenário: a comparação é sempre verdadeira.
Notar ainda que a diferença acentua-se se a dimensão da String aumentar (isto porque se as referências forem diferentes a comparação é realizada caracter a caracter).
Se alterarmos ligeiramente o teste e considerarmos um cenário misto de comparações com sucesso e insucesso o resultado é:
… 2x mais rápido ….
Conclusão
O uso do método String.Intern() oferece de facto ganhos de performance. Deve no entanto ser usado com cuidado pois o seu uso indiscriminado pode originar problemas relacionados com o GC e a gestão da memória. Assim, deve ser usado:
- apenas quando a comparação é feita com um literal – casso contrário não trás qualquer benificio, antes pelo contrário
- quando a performance é um factor critico de sucesso e a aplicação manipula dezenas de milhares de comparações - caso contrário a melhoria pode não ser mensurável.
Esta é provávelmente a métrica complexa mais usada em engenharia de software e continua a ser uma tema recorrente de discussão. Ainda assim continua a ser um assunto obscuro para muitos programadores, coordenadores e gestores...
Na maioria dos exemplos sobre a simulação de tipos estáticos, o tipo é público assim como os seus métodos.
Normalmente os programadores expôem todos os membros que vão ser alvo de teste unitário. Pessoalmente não concordo com esta prática.
Eu tento sempre produzir código por forma a manter a complexidade ciclomática baixa, e quando consigo acabo por ter código que é facilmente testável.
Nunca exponho código só para ser facilmente testável, o código deve sempre ter a minima visibilidade que é efectivamente necessária.
No fim, eu acabo por depender das ferramentas para testar o que for necessário, quer sejam membros privados, protegidos ou publicos.
Actualmente uso o Typemock Isolator e devo dizer que estou absolutamente satisfeito.
Aqui fica então como é que simulo uma instância de um tipo estático interno e também altero o comportamento do método GetString para permitir testar o valor do primeiro parâmetro:
Type globalizationHelperType = Type.GetType("NG.Helper, NG", true);
Isolate.Fake.StaticMethods(globalizationHelperType);
Isolate.WhenCalled(() => globalizationHelperType.GetMethod("GetString", new Type[] { typeof(String) }).Invoke(null, new object[] { null })).DoInstead((callcontext) => { return callcontext.Parameters[0]; });
Este toolkit pretende melhorar a performance das aplicações Web através da redução do tamanho total das páginas geradas. Esta melhoria é obtida através da redução do tamanho do valor associado à propriedade ClientID dos controlos.
À cerca de um ano atrás este toolkit foi disponibilizado no code.msdn.microsoft.com mas agora decidi migrá-lo para o CodePlex.
Experimentem !!!
Mais Entradas
Página seguinte »