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.

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

  1. cara! Fantástico… Explicação mais do que fácil para algo tão complexo…. muito obrigado!!!!

  2. BELA DICA POIS PARA QUE ESTA INICIANDO ENCONTRA MUITAS DIFICULDADES COM O ASP EM RELAÇÃO A CRIAÇÃO DE CONTROLES AUTOMATICOS

  3. Olá Eric,
    Estou iniciando o desenvolvimento em ASP.NET e estava pesquisando sobre criação dinâmica de componentes e encontrei esse post que por sinal está muito bem elaborado. Parabéns!
    Não estava conformado com a perda dos valores pelo efeito dos PostBacks no Page_Load sobre os componentes criados dinamicamente. Fiz alguns testes criando os componentes no evento Page_PreLoad, como segue:
    protected void Page_PreLoad(object sender, EventArgs e)
    {
    TextBox txtDados1 = new TextBox();
    Panel1.Controls.Add(txtDados1);

    DropDownList ddl1 = new DropDownList();
    ddl1.Items.Add(“1”);
    ddl1.Items.Add(“2”);
    ddl1.Items.Add(“3”);
    Panel1.Controls.Add(ddl1);

    }
    Dessa forma as informações digitadas são mantidas entre os postbacks. Isso é seguro em substituição a solução proposta acima? Ou posso ter outros problemas mais adiante?

    Abraços, Carlos.

    1. Carlos,

      Faz muito, muito, muito tempo que não mexo com isso. Inclusive se vc tiver oportunidade de partir pro ASP.NET MVC, acho que terá muito menos dores de cabeça com esse tipo de coisa.

      De qualquer forma, não testei seu exemplo, mas a princípio faz sentido. O conceito só é que até o page load, os controles precisam estar criados na mesma posição do request anterior no control tree para que os valores sejam desserializados nas propriedades. Atendendo este requisito, funciona.

      Abraço,

      Eric

Leave a comment