Mês: janeiro 2009

Conceitos de Bancos de Dados Relacionais

Objetivo

O objetivo deste artigo é explicar de forma prática os principais conceitos dos bancos de dados relacionais. Tem muitas outras coisas e detalhes sobre os conceitos expostos aqui, mas esse é apenas um ponto de partida.

Se houverem dúvidas ou sugestões, enviem seus comentários (rodapé da página).

Bancos de Dados Relacionais x Bancos de Dados Orientados a Objeto

Nunca trabalhei com um banco de dados orientado a objeto. O único que “ouvi falar” no mercado foi o tal “Caché” da InterSystems, que nunca tive oportunidade de ver operando na prática e com grande volume de transações.

Quando começaram a evoluir as técnicas de projeto orientado a objeto, principalmente por causa da adoção da UML e começaram as discussões sobre persistência de objetos (Hibernate, por exemplo), começou-se a questionar muito se os bancos de dados relacionais (que já estavam vivos a mais ou menos umas 3 décadas) estariam prontos pra essa nova onda.

O fato é que estamos em 2009 e os bancos relacionais são mais usados do que nunca. Os sistemas legados que já começaram sem padrões, processos, metodologias estão todos escritos em BD’s relacionais e a chance de alguém querer reescrever sistemas com 10, 20 anos de vida é muito baixa.

Parece que a prática de mercado ficou em usar bancos de dados relacionais e mapeamento objeto/relacional para projetos com arquiteturas mais definidas.

Essa é a principal razão de eu ainda estar motivado para escrever isso.

Exemplos de Bancos de Dados Relacionais

  • Sybase, SQL Server 2000 e 2005: São praticamente a mesma coisa. O SQL Server é quase uma “evolução” do Sybase, que teve uma parceria com a Microsoft para que surgisse o SQL Server. São muito similares. Geralmente usados por empresas que fazem questão em manter “tudo” da Microsoft, adotados por gente que evolui de bancos de dados locais (como Access). Muitas pessoas optam também pelo SQL Server pela facilidade de administração.
  • Oracle: Considerado mais “enterprise” pela maioria. Possui melhores opções de gerenciamento de crescimento dos data files que o SQL Server, mas um pouco mais complexo de administrar que o primeiro.
  • Firebird/Interbase: Praticamente a mesma coisa também. O Firebird foi originado pela abertura do código do Interbase pela Borland, o que deu uma forte popularizada entre desenvolvedores Delphi. Tem a vantagem de ser open-source e suportar uma carga quase tão grande (senão algumas vezes maior) que o próprio SQL Server. Pessoas que evitam a adoção do Firebird geralmente são pessoas que fogem de ferramentas Open Source.
  • PostgreSQL: Open source também, disponivel praticamente desde sempre junto com algumas distros de Linux. Já tive oportunidade de mexer um pouco com esse banco e gostei muito. Parece-me até um pouco mais parrudo que o Firebird, porém roda muito melhor em linux do que Windows e tem uma grande dificuldade em drivers de qualidade para outras plataformas, pelo menos na época em que tive mais contato com ele (por volta de 2004). Não sei como está hoje.
  • MySQL: Muito popular entre usuários PHP, é muito simples, muito rápido e muito pequeno. Nas primeiras versões ele tinha uma grande performance principalmente porque não fazia muita coisa (não tinha nem integridade referencial) hoje ele implementa esse e outros conceitos, porém não sei se é tão performático. Muito usado em pequenas aplicações. Também gratuíto. Não sei se a licensa dele é open-source.

Principais Conceitos de Bancos de Dados Relacionais

Tabelas ou Relações

Nos livros “acadêmicos” é comum chamar tabelas de “Relações”. Vou chamar de tabelas mesmo que é como é popularmente conhecido (já que esse artigo está muito, mas muito longe de ser acadêmico).

As tabelas são onde os dados são armazenados, dá pra comparar diretamente com uma planilha Excel, que tem colunas e linhas. Cada coluna possui o seu tipo de dados (serão detalhados posteriormente).

Geralmente para nomear tabelas usa-se palavras no singular. Exemplo: Cliente, Endereco, Pedido, ItemPedido. Não é muito bom nomear tabelas no plural, pois principalmente em nomes compostos, gera-se uma confusão enorme. Ex.: ItensPedido ou ItensPedidos?

Chave Primária (Primary Key)

É a principal forma (não a única) de se localizar dados numa tabela. Vamos pensar numa tabela de “Cliente” que possua as colunas Nome, Telefone, RazaoSocial. Podemos identificar o cliente unicamente através de um campo Codigo, ClienteID ou mesmo CPF/CPNJ (apesar de eu não gostar).

Não vou discutir aqui técnicas sobre como modelar uma tabela/chave primária. Isso fica mais pra frente.

Chave alternativa (Alternate Key, Unique Index)

É uma forma secundária de se localizar unicamente um registro. Ex.: Nossa tabela Cliente tem como chave primária um campo ClienteID, poderíamos definir uma chave alternativa como CPF/CNPJ, além de não podermos duplicar CPF e CNPJ, é uma outra forma de localizar um cliente.

A maioria dos bancos de dados relacionais implementa esse conceito através de “UNIQUE” indexes (create unique index), porém, é muito comum ver nas ferramentas de modelagem (ERWin, Embarcadero ERStudio) o termo “alternate key”.

Chave estrangeira (Foreign Key) / Integridade Referencial

Integridade referencial é um conceito que determina que para que um determinado dado exista numa tabela, deve haver um correspondente em outra. Ex.: Para que possa ser inserido um ClienteID na tabela Pedido, é necessário que exista um registro com o ClienteID informado na tabela Cliente.

Esse conceito é implementado através de chaves estrangeiras (foreign keys). As chaves estrangeiras nada mais são do que essas definições.

É muito importante definir as chaves estrangeiras no modelo de dados, é uma forma de tornar o banco íntegro e evitar uma série de falhas nas aplicações que acessam esse banco. É por aqui que algumas consistências simples entram no modelo, por exemplo:

  • Faz sentido um pedido para um cliente que não existe? Aqui cabe uma FK.
  • Faz sentido um registro na movimentação de conta corrente para uma conta que não existe? Aqui cabe outra FK.
  • Faz sentido um endereço para um cliente que não existe?
  • E assim sucessivamente.

Check Constraint

Alguns bancos implementam esse conceito. É uma forma de determinar quais são as informações possíveis numa coluna da tabela. Ex.: Tem um cliente com um campo Status, char(1). Nesse campo somente podem entrar os valores ‘A’ (ativo) e ‘I’ (inativo). É pela check constraint que impede-se de qualquer outra informação entrar nessa coluna.

Trigger

A maioria dos bancos de dados relacionais hoje implementam uma linguagem de programação no banco de dados. No caso do Oracle, o PL/SQL, do SQL server, o Transact SQL e outras.

A Trigger é uma forma de se escrever um código no banco de dados que execute de acordo com um evento. Ex.: Sempre que inserir um registro na tabela cliente, executa uma trigger. Sempre que excluir um registro de uma tabela, executa outra trigger.

Como usar uma trigger é uma discussão à parte que merece um artigo só pra ela. Não é muito difícil cair em armadilhas ao se criar triggers. Futuramente pretendo escrever um artigo só sobre isso.

Stored Procedure (ou somente Procedure)

É um pedaço de código, um programa que executa do lado do banco de dados (consumindo os recursos de processamento e memória do banco de dados).

Há defensores das procedures e gente que não quer elas de jeito nenhum em seus projetos. Essa é uma discussão muito mais complexa.

Geralmente, usa-se procedures por questões de performance, ou seja, invés de trazer todos os dados para a aplicação e processar lá, faz-se tudo no banco. Nem sempre isso é performatico. Existe uma série de poréns. O principal é saber qual é o conceito neste momento.

User Defined Functions (UDF’s) ou somente Functions

São funções, escritas na linguagem de programação dos bancos de dados, que geralmente permitem que as mesmas sejam usadas direto de statements SQL.

Ex.: Cria-se uma função HoraUtil, que recebe um datetime como parâmetro e de dentro da query, pode-se fazer chamadas a elas, exemplo:

select
  Data,
  HoraUtil(DataHora)
from
  MinhaTabela

Em alguns casos, UDF’s economizam muito, mas muito código redundante em queries, porém, também podem causar vários problemas de performance.

View

View é um recurso que permite que determinada expressão SQL seja armazenada no banco de dados e novas consultas podem ser feitas sobre ela.

É uma forma principalmente de “restringir” ou criar visões mais complexas de tabelas (inclusive joins) e permitir consultas futuras. Ex.: Imagine uma tabela cliente. Pode-se derivar dessa tabela uma view “ClientesAtivos”, de forma que a view liste somente os clientes ativos.

Geralmente é bom usar Views por questões de administração de banco. Pela aplicação geralmente é uma perda desnecessária de performance (pois pode gerar planos de execução de queries bem complexos).

Auto-Relacionamentos em Bancos de Dados Relacionais

A idéia dos relacionamentos está em criar referências entre duas ou mais tabelas. Quando uma tabela tem uma foreign key (FK) para outra, existe um relacionamento entre as tabelas. Ex.: Relacionamento entre a tabela Pedido e ItemPedido, de forma a criar o conceito de integridade referencial entre elas.

Algumas vezes as pessoas se confundem quando aparece um relacionamento para a própria tabela (auto-relacionamento), mas segue o mesmo conceito de relacionamento entre duas tabelas. Por exemplo:

  • Tabela Empresa com os campos
    • EmpresaID (int, not null)
    • RazaoSocial (varchar(100) not null)
    • NomeFantasia (varchar(50) not null)
    • EmpresaPaiID (int, null)
  • FK Definida de EmpresaPaiID para EmpresaID.

Sempre que eu vou inserir um registro numa tabela que tem uma FK, a FK só é validada caso todos os elementos que compõem a FK (no caso dela ser composta) tenham valores diferentes de nulo. No exemplo acima, se eu inserir um registro nessa tabela sem EmpresaPaiID, a FK não vai ser validada.

Caso exista valor, o auto-relacionamento indica que o ID informado em EmpresaPaiID deve existir na tabela Empresa, campo EmpresaID (conforme definido na FK). Na prática, implementa a regra “só podem ser cadastradas empresas filhas caso a pai esteja cadastrada).

Mas no final das contas, pra que serve um auto-relacionamento? A resposta mais simples é, principalmente para criar estruturas hierárquicas na base de dados. Usando o mesmo exemplo acima, para representar uma estrutura:

  • EmpresaID1, “Teste”, “Teste”
    • EmpresaID2, “Teste2”, “Teste2”
      • EmpresaID3, “Teste3”, “Teste3”

A forma mais elegante de representar essa estrutura, empresa 3 filha da empresa 2 que é filha da 1, seria com auto-relacionamento na tabela empresa e os dados representados da seguinte forma:

EmpresaID RazaoSocial NomeFantasia EmpresaPaiID
1 Teste Teste null
2 Teste2 Teste2 1
3 Teste3 Teste3 2

Criação de Controles Dinâmicos em páginas ASP.Net

Objetivo

O objetivo deste artigo é descrever como criar controles dinâmicos no ASP.Net. Em teoria, seria uma tarefa muito fácil. Instancia-se o controle manualmente e adiciona em qualquer outro controle no aspx.

Mas, devido a uma série de complicômetros do ciclo de vida da página (detalhado no artigo Ciclo de Vida da Página no ASP.Net), ela se torna um pouco mais complexa, para que os controles não percam o estado entre requests e seja “possível” realizar a criação dos controles dinamicamente.

Esse artigo pode ser muito útil também para quem está escrevendo “Composite Controls”, pois dependem de instanciar outros controles na mão e persistir estado entre requests.

O exemplo

O exercício que vamos acompanhar nesse artigo é relativamente simples. Vamos criar uma página aspx que possui somente um panel (panel1):

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="ControlesDinamicos._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
      <panel id="panel1" runat="server" />
    </div>
    </form>
</body>
</html>

Dentro deste panel, iremos, em tempo de execução criar um textbox em que nosso usuário deve informar um número e um botão. Quando o usuário clicar nesse botão, vamos criar a quantidade de DropDownList informada pelo usuário e mais um botão. Quando o usuário clicar nesse botão, vamos exibir um label com as informações concatenadas.

Passo 1: Criando o textbox e o botão e pegando seus valores de volta no PostBack

Para simplificar o processo vamos criar um método separado “criarComponentesDinamicos” que deve conter a lógica para se criar os componentes dinâmicos, todos eles. Este por sua vez vai chamar “criarComponentesQtdeDropDownList”, que representa a criação dos componentes necessários nessa primeira etapa. Os controles txtQtdeDropDownList e btnQtdeDropDownList serão definidos como membros private, para que após sua criação, sua instância possa ser acessado por outros métodos (para pegar valores no momento do click, por exemplo, como veremos adiante).

    private TextBox txtQtdeDropDownList;
    private Button btnQtdeDropDownList;

    private void criarComponentesDinamicos(){
      criarComponentesQtdeDropDownList();
    }

    private void criarComponentesQtdeDropDownList(){
      txtQtdeDropDownList = new TextBox();
      panel1.Controls.Add(txtQtdeDropDownList);

      btnQtdeDropDownList = new Button();
      btnQtdeDropDownList.Text = "Criar Drop Down List";
      btnQtdeDropDownList.Click += AoClicarNoBotaoCriarDropDownList; //Hookando o evento Click, para ter ação no momento do click
      panel1.Controls.Add(btnQtdeDropDownList);
    }

    private void AoClicarNoBotaoCriarDropDownList(object sender, EventArgs e){

    }

A princípio, vamos colocar a chamada do método criarComponentesDinamicos, no “Load” da página. Vamos colocar no load porque segundo as “regras” do ciclo de vida da página, para que as propriedades desses componentes não percam seu estado, precisamos que os controles sejam criados até o PreRender.

E para que possamos acessar os valores desses componentes, logo após o “LoadPostData” (após o OnLoad), teremos os valores do request anterior carregados no controle. Dúvidas? Consultar o artigo: Ciclo de Vida da Página no ASP.Net.

    protected void Page_Load(object sender, EventArgs e) {
      criarComponentesDinamicos();
    }

Para fins didáticos, vamos colocar a seguinte implementação no método AoClicarNoBotaoCriarDropDownList:

    private void AoClicarNoBotaoCriarDropDownList(object sender, EventArgs e){
      btnQtdeDropDownList.Text = "Peguei valor do textBox: " + txtQtdeDropDownList.Text;
    }

Agora, se executarmos a aplicação, vamos ter o seguinte resultado:

Ao clicar no botão, teremos o seguinte resultado:

O que aconteceu no passo 1?

O primeiro request foi servido e no momento do OnLoad, já temos o panel1 instanciado pelo próprio “motor” do ASP.Net.

No OnLoad, criamos os controles txtQtdeDropDownList e btnQtdeDropDownList, e ainda atribuimos um evento ao btnQtdeDropDownList.

Logo após o PreRender, o ASP.Net pegou o Control Tree, serializou as propriedades que mantem estado e gravou no ViewState.

Quando preenchemos o valor do edit e clicamos no botão, até o OnLoad ainda não temos os valores de txtQtdeDropDownList e btnQtdeDropDownList. No OnLoad, instanciamos esses componentes e colocamos exatamente na mesma posição no control tree.

Após o método OnLoad (método LoadPostData), o motor do asp.net, pegou novamente os valores que estão no Control Tree e jogou-os na mesma ordem de volta para a instância dos componentes.

Quando o asp.net cai no método AoClicarNoBotaoCriarDropDownList, os valores já foram jogados do ViewState para as instâncias dos controles existentes em txtQtdeDropDownList e btnQtdeDropDownList. Dessa forma, podemos pegar o valor atribuído no TextBox e jogar como texto do botão.

Passo 2: Os controles não são serializados no ViewState, as propriedades deles sim.

É uma confusão comum achar que os controles são inteiros instanciados no ViewState. Para provarmos que não, é simples. Colocamos um “if” no nosso “criarComponentesDinamicos” de forma que no postback, não vai cair nesse método e veremos o comportamento da página.

    protected void Page_Load(object sender, EventArgs e) {
      if (!Page.IsPostBack){
        criarComponentesDinamicos();
      }
    }

Ao executar novamente a página e clicar no botão, observamos que os controles dinâmicos “somem”. Isso prova que a cada request, todos os controles dinâmicos precisam ser instanciados novamente. O pior de tudo, na “hora certa”, senão perdem os valores.

Vamos voltar o código do jeito que era antes, pra podermos seguir adiante:

    protected void Page_Load(object sender, EventArgs e) {
      criarComponentesDinamicos();
    }

Passo 3: Criando os DropDownList

Para criar os DropDownList, vamos alterar a implementação do método AoClicarNoBotaoCriarDropDownList para:

    private void AoClicarNoBotaoCriarDropDownList(object sender, EventArgs e){
      int qtde = Convert.ToInt32(txtQtdeDropDownList.Text);
      criarDropDownList(qtde);
    }

    private void criarDropDownList(int qtde){
      lblResultadoDropDownList = new Label();
      for (int i = 0; i < qtde; i++) {
        DropDownList ddl = new DropDownList();
        ddl.Items.Add(new ListItem("Item " + i.ToString()));
        panel1.Controls.Add(ddl);
      }
      btnExibirInfoDropDownList = new Button();
      btnExibirInfoDropDownList.Text = "Mostrar valores Drop Down List";
      btnExibirInfoDropDownList.Click += AoClicarExibirInfoDropDownList;
      panel1.Controls.Add(btnExibirInfoDropDownList);
    }

    private void AoClicarExibirInfoDropDownList(object sender, EventArgs e) {
      foreach(Control c in panel1.Controls){
        if (!(c is DropDownList))
          continue;
        lblResultadoDropDownList.Text += ((DropDownList)c).SelectedValue;
      }
    }

Vamos também incluir a definição de btnExibirInfoDropDownList:

    private Button btnExibirInfoDropDownList;
    private Label lblResultadoDropDownList;

Agora se executarmos o código novamente, perceberemos outro comportamento interessante. Quando clicamos no botão “Mostrar valores Drop Down List” recém criado, percebemos que os componentes simplesmente somem!

Por que isso acontece? Novamente, pq as instâncias dos componentes não foram recriadas. Quando clicamos no botão, ocorreu um postback, e nesse momento em lugar nenhum passa-se de novo pelo método criarDropDownList().

O problema que temos aqui é que para criarmos os dropdownlist de novo, precisamos saber “quantos”. por isso vamos armazenar a informação de “qtde” no ViewState (senão no próximo request perde o valor) e adicionar a chamada do criarDropDownList dentro do nosso método criarControlesDinamicos (por sua vez chamados no OnLoad, ou seja, na “hora certa”).

O problema que isso vai gerar é que alguns componentes vão ser criados duas vezes! No caso falo do botão btnExibirInfoDropDownList e do label lblResultadoDropDownList. Para evitar isso, temos que fazer o nosso código que “cria” os controles (método criarDropDownList) “limpar” antes as instâncias criadas. Por isso, vamos alterar o aspx para criar um “panel2” para facilitar esse processo e fazer as instâncias do DropDownList serem criadas dentro do panel2.

]
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="ControlesDinamicos._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
      <panel id="panel1" runat="server" />
      <panel id="panel2" runat="server" />
    </div>
    </form>
</body>
</html>

Primeiro vamos adicionar como propriedade:

    private int qtdeDropDownList{
      get { return ViewState["qtde"] == null ? 0 : (int)ViewState["qtde"];}
      set { ViewState["qtde"] = value;}
    }

Depois vamos alterar o método AoClicarNoBotaoCriarDropDownList.

    private void AoClicarNoBotaoCriarDropDownList(object sender, EventArgs e){
      qtdeDropDownList = Convert.ToInt32(txtQtdeDropDownList.Text);
      criarDropDownList(qtdeDropDownList);
    }

Alteramos o criarComponentesDinamicos

    private void criarComponentesDinamicos(){
      criarComponentesQtdeDropDownList();
      criarDropDownList(qtdeDropDownList);
    }

Alteramos também o criarDropDownList, para limpar e criar as coisas no panel2.

    private void criarDropDownList(int qtde){
      panel2.Controls.Clear();
      lblResultadoDropDownList = new Label();
      panel2.Controls.Add(lblResultadoDropDownList);
      for (int i = 0; i < qtde; i++) {
        DropDownList ddl = new DropDownList();
        ddl.Items.Add(new ListItem("Item " + i.ToString()));
        panel2.Controls.Add(ddl);
      }
      btnExibirInfoDropDownList = new Button();
      btnExibirInfoDropDownList.Text = "Mostrar valores Drop Down List";
      btnExibirInfoDropDownList.Click += AoClicarExibirInfoDropDownList;
      panel2.Controls.Add(btnExibirInfoDropDownList);
    }

Perfeito, né? Agora quando clicarmos no botão “Mostrar valores Drop Down List”, eles não somem e os valores aparecem no label. MENTIRA. Em troca de todo esse trabalho, ganhamos uma exception “System.ArgumentException”: Argumento de postback ou de retorno de chamada inválido. A validação do evento é habilitada com o uso de <pages enableEventValidation=”true”/> na configuração ou <%@ Page EnableEventValidation=”true” %>  em uma página. Por motivos de segurança, esse recurso verifica se os argumentos para eventos de postback ou de retorno de chamada se originam no controle do servidor que originalmente os processou. Se os dados forem válidos e esperados, use o método ClientScriptManager.RegisterForEventValidation para registrar os dados de postback ou de retorno de chamada para validação.

Por que?

No 1o request, criamos “0” drop down list e o botão btnExibirInfoDropDownList, como o 2o controle dentro do panel2. Como não atribuímos nenhum “ID” pra ele, ele magicamente (baseado no comportamento de NamingContainer) recebeu o ID ctl02 (ou algo parecido, não é relevante).

No 2o request, quando criamos os drop down list, ele recebe outro ID. No momento do click, o controle que gerou o evento tinha um ID, e no momento de processar o evento (após a passada de criarDropDownList no OnLoad) gerou outro ID.

Para evitar isso, vamos gerar o ID manualmente, alterando o criarDropDownList.

    private void criarDropDownList(int qtde){
      panel2.Controls.Clear();
      lblResultadoDropDownList = new Label();
      panel2.Controls.Add(lblResultadoDropDownList);
      for (int i = 0; i < qtde; i++) {
        DropDownList ddl = new DropDownList();
        ddl.Items.Add(new ListItem("Item " + i.ToString()));
        panel2.Controls.Add(ddl);
      }
      btnExibirInfoDropDownList = new Button();
      btnExibirInfoDropDownList.Text = "Mostrar valores Drop Down List";
      btnExibirInfoDropDownList.Click += AoClicarExibirInfoDropDownList;
      btnExibirInfoDropDownList.ID = "btnExibirInfoDropDownList"; //Aqui está a mágica.
      panel2.Controls.Add(btnExibirInfoDropDownList);
    }

Fazendo isso, não ganhamos a Exception desagradável, e a página se comporta do jeito que esperamos.

E se eu quiser “sumir com alguns controles”

Invés de “não criar” ou matar as instâncias, use Visible true/false após a criação dos mesmos.

Conclusão

A tarefa de criar componentes dinâmicos não é tão simples quanto parece, devido à complexidade do Control Tree mais a arquitetura de PostBack do ASP.Net.

Em resumo, manter o Control Tree do mesmo jeito antes e depois de carregar o ViewState resolve a maioria dos problemas.

Código completo da solução

Baixe o código completo do artigo aqui.

Ciclo de Vida da Página no ASP.Net

Objetivo

O objetivo deste artigo não é novamente citar quais são todos os métodos virtuais que são chamados pelo ASP.Net no momento que está servindo uma página. Já existe uma tonelada desses na internet, no msdn nos forums e tudo o mais.

Quando estava escrevendo meu último server control, deparei com uma série de situações que na prática não tinham solução descrita de uma forma direta na internet. Por isso resolvi escrever esse artigo, simplificar o ciclo de vida da página para “o que interessa”.

Entenda por “o que interessa”, “por que a minha página vive perdendo o estado?”.

O “Control Tree”

Em muitos lugares fala-se sobre a árvore de controles de uma página, mas por que ela é tão importante?

  • A árvore de controles é quem determina como o ViewState vai ser salvo e carregado.
  • A árvore de controles é quem determina a relação de qual página é “dona” do controle (alguns dependem disso para “renderizar-se”)
  • A grande maioria dos problemas ao se escrever uma página, principalmente relacionados a perda de estado estão relacionadas com manipulação incorreta do control tree.

Suponhamos uma página com um Panel (panel1) e dentro desse panel outro panel (panel2) e dois textbox (textbox1 e textbox2). Dentro do panel2 ainda ganhamos um outro textbox (textbox3). A coisa é chamada de “control tree” pq a estrutura da collection “Controls” de cada um dos controles contendo outros controles, forma uma árvore (como um treeview e seus nós, ou um XML como seus nós). Desenhando fica mais fácil de entender:

  • Página
    • panel1
      • panel2
        • textbox3
      • textbox1
      • textbox2

O ciclo de vida da página

Esse artigo do MSDN mostra de uma forma clara como o Runtime do ASP.Net processa o request. É uma boa referência: http://msdn.microsoft.com/en-us/library/aa479328.aspx

Início do processamento

No início o ASP.Net pega o processamento do request e direciona para o aspx equivalente. Uma instância da classe gerada pelo aspx é criada para o request e começa o ciclo de vida.

Nesse momento o ASP.Net determina se o request é um postback ou não e se é um request assíncrono ou não (AJAX).

Em seguida, o aspx é parseado e o Control Tree é criado. Os componentes tem suas propriedades criadas com valor default e em seguida sobrescritas com os valores informados no aspx.

Inicialização

Na etapa de inicialização a primeira coisa que acontece é “OnPreInit”, depois “Init”. Neste momento, ainda não foram carregadas as informações do ViewState.

A única diferença perceptível entre PreInit e Init (caso alguém saiba mais alguma, ficarei feliz em saber também) é que em PreInit ainda não foi carregado o tema da página, ainda não foi aplicada a MasterPage, nem os control skins.

Load

Nessa etapa, é carregado o ViewState, através do método virtual “LoadViewState”. LoadViewState na verdade apenas carrega as informações do view state de um meio persistente (No caso padrão, do campão hidden escondido na página). Ele na verdade não joga esses valores no componente.

ProcessPostData é um método interno do ASP.Net runtime. Não encontrei nenhuma documentação “oficial” da Microsoft sobre o que esse método faz, o fato é que baseado numa série de testes, pude perceber que uma das responsabilidades dele é pegar os valores do ViewState e jogar nos componentes. A confusão é que, isso é feito mais de uma vez.

Após o carregamento das informações, acontece OnPreLoad e OnLoad. OnPreLoad, na documentação do msdn fala sobre “uma segunda chance para carregar informação do ViewState”. Pra mim não faz muito sentido, enfim… Se alguém tiver mais sugestões sobre diferenças de OnPreLoad e OnLoad, por favor, me conte.

O OnLoad na minha opinião é o lugar ideal (talvez o único) para tratar criação de componentes dinâmicos, pegar informações do Request e outros tratamentos, pois logo após o OnLoad é executado novamente o ProcessPostData. Dessa forma, se um componente dinâmico for criado no on-load, ele vai manter estado se na hora de executar ProcessPostData o Control Tree estiver no mesmo estado em que foi salvo.

Processar eventos de Postback

A próxima etapa é cair nos eventos de postback dos controles. Acontece logo após o OnLoad.

O próximo evento chamado é o “OnPreRender”, segundo o msdn, a última chance de manipular o Control Tree antes de salvar o estado.

Salvar ViewState

O ViewState é salvo, baseado no Control Tree. Isso é muito importante.

Significa que para o estado ser mantido corretamente, no próximo postback, no momento de carregar o ViewState (antes e após o OnLoad, no ProcessPostData) a estrutura do Control Tree precisa ser idêntica à que foi usada para salvar o ViewState.

Aqui moram a maioria dos problemas. Se está perdendo estado, é porque provavelmente a estrutura do Control Tree está diferente da que foi salva na hora de carregar.

Render

Nesse momento a página “desenha-se” chamando recursivamente o Render de todos os componentes filhos.

Nem adianta mais manipular o Control Tree aqui, pois o estado não vai ser salvo mais e provavelmente vai gerar um monte de outros problemas.

O ciclo de vida e o ViewState

Uma coisa muito importante sobre o ViewState que confunde a maioria das pessoas (já me confundiu muito). O ViewState guarda o “estado” de “algumas” propriedades do componente na página e recupera os mesmos valores no próximo postback. O ViewState **não** serializa as instâncias dos controles inteira e recupera no próximo PostBack. São duas coisas completamente diferentes.

Quando o postback acontece, os controles são re-instanciados baseados no aspx e as informações do ViewState carregadas pra dentro. Isso significa q se um componente foi criado dinamicamente, por default, ele não reaparece magicamente no postback, ele precisa ser re-criado.

Se ele precisa ser re-criado, como eu faço pra ele manter estado? A resposta é: “Re-crie o componente na mesma posição em que ele estava na hora que o ViewState foi salvo na hora certa”. A hora certa é: No OnLoad.

Aqui é que entra toda a complicação.

Tem dois pontos CRUCIAIS para se entender o ViewState. Um é o momento que os valores lidos são jogados para dentro do componente, ou seja, ProcessPostData. Isso acontece duas vezes. Uma antes do OnLoad, outra depois.

Outro ponto crucial é quando o ViewState é salvo, ou seja, entre o PreRender e o Render.

No momento de carregar o ViewState, a página respeita **exatamente a estrutura do Control Tree**. O que isso significa na prática? Se eu tinha a estrutura:

  • Página
    • TextBox
    • DropDownList
    • TextBox

No momento de salvar o ViewState e no postback subsequente, no momento de carregar o ViewState eu tenha a estrutura

  • Página
    • TextBox
    • TextBox

O segundo TextBox perde o estado.

Referências

Alguns lugares de onde tirei informações para esse artigo (interessante é que eles são bem diferentes):

Cuidados ao Escrever Server Controls no ASP.Net

Objetivo

O objetivo deste artigo é descrever os principais pontos que devem ser observados ao se escrever server controls. Parece muito simples, mas na prática são muitos detalhes e é muito fácil se perder neles, por causa da velha história do ciclo de vida da página, ViewState e muitos outros detalhes.

De onde herdar?

A classe “base” para um server control não-visual (não consigo pensar num desses, mas é isso mesmo) é um System.Web.UI.Control. Para componentes visuais, System.Web.UI.WebControl, ou alguém mais especializado (System.Web.UI.TextBox, System.Web.UI.Panel, etc).

Quando existem “itens” no componente, Ex. Coluna de um grid, subcontroles de um panel ou qualquer outra situação semelhante, deve-se pensar no seguinte:

  • O componente vai ter propriedades suas mantidas em ViewState? Se a resposta for afirmativa, é interessante derivar eles também de WebControl. Se ele não for derivado de WebControl e fazer suas próprias propriedades guardarem estado, será necessário escrever todo o código para que o controle filho guarde e leia o ViewState (overriding LoadViewState e SaveViewState).
  • Se o item for bobão, sem muitas propriedades e com a lógica de renderização todo no componente principal (Ex.: Form, Filtro) estes podem ser uma classe sem herança alguma. Geralmente, eu prefiro a primeira opção.

Como as tags serão escritas no aspx?

Aqui começa uma confusãozinha razoável.

Supomos o seguinte componente, um componente que seja um Panel com título. Vamos chamá-lo de MeuPanel. Como queremos que o “usuário” do nosso componente escreva as tags no aspx?

Pode ser de duas formas:

<ME:MeuPanel titulo="Título do panel">
  <ME:MeuSubComponente />
  <ME:MeuSubComponente />
  <ME:MeuSubComponente />
</ME:MeuPanel>

No exemplo acima, estamos considerando que o título do panel é uma propriedade que será escrita como “Atributo” e que os subcomponentes serão armazenados numa propriedade chamada “MeusControles”, esta será a propriedade interna default.

Para esse exemplo, a declaração da classe deve ser a seguinte:

[ParseChildren(false, "MeusControles")]
[PersistChildren(true)]
public class MeuPanel{
  //...
}

ParseChildren = false, indica que os filhos não serão tratados como propriedades, ou seja, os nós filhos do ME:MeuPanel no aspx não tentarão ser colocados dentro de uma propriedade chamada “MeuSubComponente” da classe MeuPanel.

A propriedade default “MeusControles” do atributo ParseChildren, indica que os nós filhos declarados serão mapeados para dentro de uma propriedade chamada “MeusControles” por default, ou seja, não precisa colocar o nós <MeusControles> e </MeusControles> no aspx.

O atributo PersistChildren=true indica que o que for informado como filho do nó do aspx são itens de uma collection (filhos) e não propriedades. Se ele não for informado, ele vai tentar colocar MeuSubComponente como uma propriedade de Controls, e não como um item na collection.

A declaração das propriedades na classe, deve ser a seguinte:

[Description("Título do Panel")]
[PersistenceMode(PersistenceMode.Attribute)]
public string titulo{
  ...
}

[Description("Controles filhos")]
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
public List<Control> MeusControles{
  ...
}

Uma outra forma de se informar as propriedades no aspx poderia ser:

<ME:MeuPanel titulo="Título do panel">
  <titulo>Título do Painel</titulo>
  <MeusControles>
    <ME:MeuSubComponente />
    <ME:MeuSubComponente />
    <ME:MeuSubComponente />
  </MeusControles>
</ME:MeuPanel>

Para esse exemplo, as declarações na classe devem ser as seguintes:

[ParseChildren(true)]
public class MeuPanel{
  ...
}

[Description("Título do Panel")]
[PersistenceMode(PersistenceMode.Attribute)]
public string titulo{
  ...
}

[Description("Controles filhos")]
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
public List<Control> MeusControles{
  ...
}

Composite Controls e componentes com collections de filhos (child controls)

Composite controls é a mesma coisa que “Controle composto”, ou seja, um controle que agrega outros subcomponentes. Um bom exemplo disso poderia ser um “dual list box” por exemplo. Desde que ele fosse implementado agregando dois ListBox (Ele também poderia ser implementado inteirinho do zero, baseado em WebControl, com todo o controle em javascript).

Componentes com collections seguem a mesma idéia.

A única observação principal que devemos ter sobre essas “variações” é a “árvore” de Controles dentro do aspx. Ela é importante, pq é quem determina a relação “pai/filho” dos componentes, e quem força a chamada de uma série de métodos virtuais nos componentes. Ex.: Sempre que um grid é renderizado, ele precisa em algum lugar sofrer um “DataBind”, precisa ser chamado “CreateChildControls” e uma série de coisas.

Quem controla essas chamadas é a Página. E não existe nenhuma relação entre a página e o controle se o mesmo não existir na collection de “Controls” na hora certa. A “hora certa” pra fazer isso é “OnInit”. Depois disso a página sai chamando tudo o que precisa nos componentes filhos.

Se o componente q tem child controls mantiver os mesmos numa collection que não seja a de “Controls” é necessário no OnInit jogar os mesmos na collection de controls, senão eles sofrerão os mesmos problemas de “ciclo de vida”.

No caso de “Composite Controls” a melhor hora para adicionar os filhos na collection de controls é no método virtual CreateChildControls.

Muitos problemas de componentes que perdem o estado no postback, estão relacionados à perda da relação com a página na hora certa. Devo escrever um artigo mais detalhado sobre isso em breve.

Ciclo de Vida da Página

Esse é um assunto bastante complexo. A principal questão é que os controles devem fazer o trabalho de renderização no método virtual “Render”. Parece óbvio, mas não é tão simples assim.

No momento da renderização as vezes usamos outros componentes para ajudar no processo. Ex.: Table, TableRow, TableCell e amigos. Quando criamos um componente desse e adicionamos um dos nossos dentro deles (collection Controls), para q estes por sua vez sejam renderizados, automaticamente trocamos o “Pai” do nosso subcomponente. Isso novamente vai fazer com q outros métodos do ciclo de vida da página não sejam chamados no componente.

Se em tempo de render forem criados componentes, Ex.: Table, a table também precisa ser adicionada na collection de Controls, assim qdo outro componente é adicionado na table, ele não perde a relação com a página.

O ciclo de vida em si, é um pouco mais complicado que isso, se considerarmos todos os eventos dele. O artigo a seguir é bom para entender um pouquinho sobre ele, antes de se aventurar a sair escrevendo: http://www.15seconds.com/issue/020102.htm

Tem alguns outros detalhes sobre criação “dinâmica” de componentes, que merecem um artigo à parte.