ASP.NET, servercontrols e viewstate - "a hora do pesadelo versão 666".
* Este artigo aplica-se àqueles que não gostam de trabalhar com objetos conectados, nem usar as conexões de dados prontas em modo Bound() que a Microsoft insiste em adicionar às suas linguagens, e que acabam limitando a flexibilidade do código. Dentre estes, aqueles que preferem recorrer a reinventar a roda de vez enquando, para que ela seja mais adequada às suas necessidades. Os exemplos e comentários estão sobre um WebControls.Table personalizado, mas serve para qualquer objeto personalizado ou extendido.
Aqui vou abordar três coisas terríveis do asp.net, que não têm documentação organizada, apenas uma colcha de retalhos que os programadores precisam usar de "se-virol" para descobrir a abordagem correta e como tapar todos os furos.
Estas três coisas são: viewstate, servercontrol, e tabelas dinâmicas.
Basicamente estamos falando da situação clássica: estou criando um servercontrol asp.net personalizado, o qual será uma tabela, e eu gostaria que os dados fossem persistidos entre postbacks.
Após vários dias tendo problemas com isso, e tendo uma primeira versão do meu objeto funcionando como "por mágica" e uma versão nova em folha se recusando a persistir dados e vincular eventos, eu finalmente consegui juntar todas as peças do quebra cabeças e resolver a questão.
O problema, que pelo que entendi na net, pode ser resolvido com algumas regrinhas simples.
Desta forma, tentando ajudar outros quase frustrados (como eu estava até 10 minutos atrás), e contrariando artigos complicados e que não dizem nada de mais, resumo abaixo:
1) O primeiro passo é entender que os dados são persistidos da forma mais comum, pelo objeto viewstate.
Como este objeto não guarda todas as propriedades dos controles, e sim apenas algumas, e somente aquilo que possa ser serializado
(convertido em string), é preciso saber:
-
Quando criar o objeto pela primeira vez (no caso da tabela, adicionar as colunas e linhas)
- Quando salvar os dados criados
-
Quando restaurar internamente os dados salvos. Também é primordial entender que tudo isso é manual, ou como dizemos nos pagos do Rio Grande, "no braço". O ASP.NET irá apenas guardar e restaurar o viewstate como um todo. O resto é tarefa sua.
Basicamente, interessam os seguintes eventos, que são chamados na sequência entre cada postback ou carregamento da página:
Load() da página
Load() do seu objeto control
PreRender() do seu objeto control
* Todos os exemplos em .NET, pois não gosto da sintaxe do C#, com milhões de chaves ({}).
No load da página você insere o básico: "if not me.isPostBack then" para verificar se é a primeira abertura da página, e dentro do IF coloque o código que cria a estrutura da tabela. No meu caso, o objeto aqui deve criar as colunas, conforme o modelo definido, e criar o cabeçalho.
Em seguida o ASP vai chamar o Load do controle. Neste ponto seu objeto estará vazio, e acabou de ser criado. Aqui você verifica se tem algo no viewstate, como por exemplo "if not viewstate("tabelaxxx") is nothing then", para saber se existem dados no cache do viewstate. Se o conteúdo existe, você executa o processamento do mesmo de forma bem manual, ou seja, ali você pode ter guardado os dados do postback anterior, e vai recuperá-los, preencher toda a tabela, setar propriedades, etc. Enfim você remonta o controle inteiro, inclusive componentes da coleção Controls() manualmente, usando como base dados que salvou no viewstate (calma, em seguida você vai ver como guardar os dados ali). Caso o conteúdo do viewstate esteja em branco, você ignora esta etapa. Isto normalmente ocorre na primeira abertura da página.
Por fim, o ASP processa o PreRender do controle. Nesta etapa o controle estará completamente preenchido e pronto para ser gravado no viewstate. O que normalente não se comenta, é que este evento também será chamado após o clique de um botão, por exemplo. Digamos que você tenha um botão "adicionar linha" dentro da página, e que o evento deste botão chama o "InsertRow" do seu controle. Quando o controle é alterado logo após o click do botão, ele chama também o PreRender, contendo os dados dinamicos adicionados. Então esta é a hora certa de salvar os dados no viewstate. Isto porque este evento será chamado tanto após a primeira carga, quando os dados iniciais são adicionados, quanto após eventos de botões e objetos associados ao componente de alguma forma (entre postbacks).
Para salvar os dados, basta usar a mesma lógica de antes. Por exemplo, você monta um array com os dados da tabela, e adiciona este array ao viewstate, como exemplo "viewstate("tabelaxxx") = arraytemp.
* Dica: para guardar no viewstate, os objetos precisam ser serializáveis, ou
variáveis primárias (string, integer, double, etc). Para os objetos do tipo
structure, basta colocar a diretiva <Serializable> no começo da linha de
construção da estrutura, exemplo: "<Serializable> structure teste".
Maiores detalhes, sugiro verem em
http://ericlemes.wikidot.com/dotnet-pagelifecycle, um excelente artigo.
2) Maravilha, já teremos nesta altura nosso controle mantendo seu estado, sem precisar adicionar novamente os dados a cada postback, que seria o default do ASP.net. Mas agora faltam algumas manhas:
-
Os objetos contidos no controle (por exemplo, um checkbox dentro de uma célula da tabela), devem ser adicionados na mesma ordem do postback anterior, pois o viewstate usa algo chamado "control tree", hierárquico, para associar o viewstate dos objetos. Se algo volta fora da ordem é ignorado. Se você não se ater a esta e outras dicas abaixo, vai acabar tendo neste exemplo vários checkbox que não guardam estado (check/uncheck no caso) e que não respondem a eventos, ainda que o AddHandler não dê erro.
-
O ID dos objetos dinâmicos deve ser setado via código, para garantir que não mude entre um postback e outro. Aí entra uma coisa que demorei para descobrir, pois o ASP comporta-se neste aspecto como o C tradicional: não gera erro, apenas não funciona! Os caracteres válidos para um ID são letras, números e underline (_). Outros caracteres não geram erro, apenas são substituídos por underline. Perfeito! Mas isso gera, como diz em um artigo da Microsoft "erro de processamnto da página". Leia-se por isso aquele comportamento errado, mas sem indicação para o programador. Se você setar o ID do objeto por exemplo para "$55", verá que sei ID mantém-se como "$55", mas internamente o ASP converteu para "_55", motivo porque tanto os eventos associados quanto o viewstate não funcionam mais! Mais detalhes sobre o ID em: http://msdn.microsoft.com/pt-br/library/system.web.ui.control.id.aspx.
-
Complementando a informação do ID, notei um outro comportamento estranho do ASP. Se você for usar o UniqueID gerado automaticamente pelo ASP.NET para os objetos de nível superior, concatenando com os Parent (exemplo: ObjNivel1.UniqueID & ObjNivel1.parent.UniqueID & ObjNivel1.page.UniqueID, para garantir ID exclusivo no objeto dinâmico e seus subcomponentes), o próprio ASP.NET gera IDs contendo "$". Notei isto especificamente quando você utiliza um masterpage.
Obviamente o erro é o mesmo descrito no tópico anterior. Então o mais simples é ao fazer a concatenação para gerar ID composto, já execute um replace,
trocando "$" por "_" ou outro caracter aceito.
-
Implemente a interface: "Implements INamingContainer", que vai adicionar uma espécie de prefixo aos seus controles, usando o ID do controle principal. Desta forma evita-se que ao adicionar dois dos seus controles à página dê erro de ID duplicado nos objetos internos.
-
Tente tornar o viewstate o menor possível, então ao guardar os dados, tente guardar em um formato pequeno, e fácil de ser recuperado, como um array. Lembre-se que o viewstate é salvo em um hiddenfield na página, e portato vai e volta do servidor para o cliente a cada postback. Um viewstate grande, pode facilmente levar você a carregar uma página dinâmica de alguns "mega bytes" sem saber.
3) Exemplo simples:
Imagine um servercontrol que implemente uma tabela. Esta tabela tem os métodos InsertCol para criar a estrutura dos dados. Na página hospedeira adiciona-se um botão AdicionaLinha, sendo que no seu evento Click será chamado o Tabela.InsertRow, para inserir linhas e dados.
Você adiciona o Load da página:
Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
If Not Me.IsPostBack Then
Tabela.InsertCol("Código")
Tabela.InsertCol("Descrição")
End If
End Sub
Em seguida no servercontrol:
Public Class Tabela
Inherits WebControls.Table
Implements INamingContainer
Private Sub Tabela_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
If Not ViewState("tabela") Is Nothing Then
Dim stemp As String = ViewState("tabela")
... segue processando os dados para recompor a tabela
endif
end sub
Private Sub Tabela_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.PreRender
ViewState("tabela") = xxxxxx 'aqui cria-se o array, texto, ou outro, contendo os dados e estrutura da tabela, e guarda-se no viewstate.
End Sub
end class
* Ao criar as linhas, células e objetos aninhados, lembre-se das regras de sequência e ID, e pronto.
4) Conclusão:
Sem pretender apresentar teorias complexas e uma infinidade de complicações desnecessárias, estas colocações vão direto ao ponto para resolver as dúvidas mais comuns do ASP.net quando o "vivente" decide fazer as coisas "no braço". Se você não entendeu os fundamentos, sugiro mexer em coisas mais simples para pegar o jeito primeiro, depois tente ler novamente.
Para aqueles que porventura achem o artigo simples demais, e carente de maiores explicações, peço desculpas, mas estou cansado de ver blogs e sites (inclusive o outrora excelente MSDN), tratando de questões óbvias com páginas e páginas, e deixando de lado os detalhes que fazem a diferença.
Espero ter ajudado aos navegantes.