Estado de Visualização em ASP.NET

Antes de entrar em pormenores sobre estado de visualização é necessário compreender o que é um controlo e como este funciona em ASP.NET. De certa forma o ASP.NET é uma arquitectura baseada em controlos, já que uma página é um controlo e qualquer controlo pode conter controlos filhos.

A arquitectura é semelhante à arquitectura de janelas do Windows, onde o próprio ambiente de trabalho é uma janela, que pode conter janelas filhas. Cada janela é apresentada, apresentando primeiro o seu conteúdo e depois apresentando o conteúdo das janelas filhas. O mesmo se passa em ASP.NET em que cada controlo é apresentado, apresentando primeiro o seu conteúdo e depois apresentando o conteúdo dos seus filhos. A apresentação de uma janela em Windows envolve o desenho de pixeis no ecrã, enquanto que a apresentação de um controlo ASP.NET envolve a geração de HTML para preencher uma parte da resposta a um pedido HTTP.

Uma página serve como controlo raiz e tem três controlos filhos imediatos: um controlo literal para gerar o texto de início da página, um controlo do lado do servidor HtmlForm para representar o formulário e todos os seus controlos filho, e por fim outro controlo literal para gerar o texto de fim da página. Todos os controlos adicionados a uma página estarão dentro do formulário e portanto serão filhos do HtmlForm, em que a ordem pela qual estão definidos dentro desse controlo será a ordem pela qual serão apresentados. Cada um destes controlos tem o seu próprio estado de visualização. Todos os controlos que correspondem a elementos de formulário têm a sua manutenção de estado suportada através do envio automático do valor dos elementos quando o formulário é submetido (post back). Todos os outros terão que definir o seu mecanismo de persistência de estado e será sobre este assunto que iremos conversar.

Em ASP.NET, o estado de visualização entre post backs é mantido através de uma colecção de pares nome/valor acessíveis por qualquer controlo a partir da propriedade ViewState. Quase todo o estado do controlo, se não todo, fica guardado nesta colecção. Esta propriedade retorna uma instância do tipo System.Web.UI.StateBag, que é muito semelhante a uma tabela de hash mas tem a capacidade de registar alterações (fazer tracking), ou seja, permite que sempre que um valor seja alterado na colecção seja também marcado como “dirty”.

A função de registo de alterações pode estar ligada ou desligada, mas uma vez ligada não pode ser desligada. De modo a activar a função use o método TrackViewState(). Se a função de tracking estiver ligada qualquer alteração a um objecto fará com que esse objecto fique marcado como “dirty”. Podemos consultar se o objecto está marcado ou não através do método IsItemDirty(string chave) ou forçar a marcação através do método SetItemDirty(string chave). O leitor tenha em atenção um pormenor, após a activação de TrackViewState() qualquer alteração será marcada, mesmo que o objecto seja alterado para o mesmo estado, como por exemplo:

stateBag["nome"] = "valor";
stateBag.IsItemDirty("nome"); // falso
stateBag.TrackViewState();
stateBag["nome"] = "valor";
stateBag.IsItemDirty("nome"); // verdadeiro

A colecção ViewState, como já foi referido, guarda pares nome/valor, os pares podem ser indexados por string e ter qualquer object como valor. Exemplo:

ViewState["ViewStateVariableName"] = 1;

Para ter acesso á variável guardada basta fazer a indexação com a chave e a respectiva conversão.

int number = (int) ViewState["ViewStateVariable"];

Esta colecção também permite guardar tipos os nossos próprios tipos quase tão facilmente como os tipos básicos. Para tal basta apenas o tipo ser serializável, ou seja, consegue-se converter uma instância desse tipo para uma sequência de bytes e posteriormente fazer a sua recuperação. Vai compreender a necessidade de o tipo ser serializável mais á frente neste artigo quando falarmos de um campo de input oculto chamado __VIEWSTATE.

[Serializable]
public class Pessoa
{
  public string _nome;
  public int _idade;
 
  public Pessoa(string nome, int idade)
  {
    _nome = nome;
    _idade = idade;
  }
}

Como a classe Pessoa está marcada como serializável pode ser guardada em ViewState:

Pessoa p = new Pessoa("Vitor", 25);
ViewState["Cliente"] = p;

Lembre-se que terá de efectuar a respectiva conversão quando necessitar de obter o valor guardado.

Pessoa p = (Pessoa) ViewState["Cliente"];

O protocolo HTTP é stateless, ou seja, cada pedido é executado independentemente e sem conhecimento de pedidos anteriores. Portanto cada pedido feito á arquitectura ASP.NET será servido por uma instância diferente do controlo e por isso não é possível guardar estado de visualização entre post backs em campos de instância. Devido a isso as propriedades em ASP.NET terão um aspecto diferente já que deverão usar a colecção ViewState para guardar qualquer valor:

public int ValorInteiro
{
  get { return (int)ViewState["ValorInteiro"]; }
  set { ViewState["ValorInteiro"] = value; }
}

Nos casos em que é necessário ter um valor por defeito teremos também que ter em conta que estamos a utilizar a colecção ViewState. Tal como uma tabela de hash, uma StateBag irá retornar null se a colecção não contém uma entrada com essa chave. Portanto, se o valor retornado for nulo é porque ainda não foi atribuído, então deve retornar o valor padrão, caso contrário, retornar o valor obtido.

public string Texto 
{
  get { return ViewState["Texto"] == null ? 
"Valor por Defeito" : (string)ViewState["Text"];
}
  set { ViewState["Texto"] = value; }
}

Repare que ao afectar com null uma propriedade definida com este padrão essa propriedade passará a retornar o valor por defeito e não null como acontece com as propriedades ditas normais. Uma alternativa poderá ser afectar a propriedade com String.Empty (um campo que representa um string vazia) em vez de null. De salientar ainda que cada controlo pode aceder á sua colecção ViewState em qualquer momento e por qualquer razão, não apenas através de propriedades.

Deve-se utilizar esta colecção quando as propriedades reflectem directamente tipos primitivos. No caso de controlos que têm pretendem manter um estado mais complexo, usando tipos próprios poderá ser mais complicado utilizar a colecção ViewState. Como alternativa podemos sobrepor dois métodos virtuais definidos na classe base Control sendo eles o SaveViewState() e o LoadViewState(). Estes métodos permitem escrever e ler manualmente o estado do controlo a partir da stream ViewState. Para um objecto poder ser guardado nessa stream tem que ser serializável. O método SaveViewState(), como o nome indica, permite guardar o estado de visualização. Note que este método também é responsável por chamar o método da base e guardar, no objecto a retornar, o resultado dessa chamada.

protected override object SaveViewState()
{
  ArrayList OsMeusDados = new ArrayList();
  object[] vState = new object[2];
  vState[0] = base.SaveViewState();
  vState[1] = OsMeusDados;
  return vState;
}

O método LoadViewState() permite recuperar o estado de visualização. Note que este método também é responsável por chamar o método da base, passando ao mesmo os dados que lhe pertencem.

protected override void LoadViewState(object savedState)
{
  if (savedState != null)
  {
    ArrayList OsMeusDados;
    // Obter o array de objectos guardados em SaveViewState
    object[] vState = (object[])savedState;
 
    if (vState[0] != null)
      base.LoadViewState(vState[0]);
    if (vState[1] != null)
      OsMeusDados = (ArrayList)vState[1]; 
  }
}

O leitor certamente reconhece um padrão de recursividade na chamada a estes métodos e é desta forma que a arquitectura ASP.NET constrói uma estrutura de dados com todo o conteúdo de estado de visualização. Como já foi referenciado, o protocolo HTTP não tem memória. Isso significa que o estado da página terá que ser guardado e posteriormente reposto no pedido seguinte. Os pares nome/valor colocados na colecção ViewState antes da apresentação da página são armazenados num campo de input oculto, __VIEWSTATE, e quando a página é de novo acedida através de um pedido de POST, o conteúdo do campo __VIEWSTATE é analisado e usado para reconstituir a colecção ViewState.

Falta-nos agora perceber como é mantido o estado dos controlos de formulário. Como já foi dito anteriormente, todos os controlos que correspondem a elementos de formulário têm a sua manutenção de estado suportada através do envio automático do valor dos elementos quando o formulário é submetido (post back). Esta manutenção de estado é realizada através da implementação da interface IPostBackDataHandler.

public interface IPostBackDataHandler {
  bool LoadPostBackData(string postDataKey, NameValueCollection postCollection);
  void RaisePostDataChangedEvent();
}

Neste caso a arquitectura faz uma pesquisa no conteúdo do POST de modo a determinar se existem entradas que correspondem a controlos que implementam IPostBackDataHandler e de seguida é invocado o método LoadPostData para todos esses controlos. A string postDataKey, passado como argumento, contém o identificador único associado ao controlo, que pode ser usado para indexar sobre postCollection para localizar o valor corrente do controlo dentro da colecção como se pode verificar no seguinte exemplo:

public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) {
  string valorAntigo = Text;
  string valorNoPOST = postCollection[postDataKey];
 
  if (!valorAntigo.Equals(valorNoPOST)){
    Text = valorNoPOST;
    return true;
  }
  return false;
}

O resultado deste método deve ser true se mudou o valor do estado do controlo, caso contrário, o método deve devolver false. Para todos os controlos que retornam true neste método é chamado o RaisePostDataChangedEvent de cada um desses controlos de modo a poderem ser desencadeados eventos de alteração de estado. O leitor repare que o carregamento de dados de POST ocorre em duas fases, o método LoadPostData ocorre antes do método de Load da página e o método de RaisePostDataChangedEvent ocorre depois do método de Load. Isto permite que na altura em que as notificações são geradas todos os controlos tenham o seu estado reposto.

Note ainda que, para além dos eventos de alteração de estado (eventos changed), como por exemplo alterações do texto de uma caixa de texto ou a alteração do índice seleccionado de uma DropDownList, também os eventos de reacção utilizam este mecanismo. Por exemplo, o evento de click de um botão é detectado e lançado utilizando esta interface. Como se deve ter apercebido, a manutenção de estado de controlos de formulário não usa o campo oculto __VIEWSTATE em nenhuma das suas fases e é completamente independente deste. É por essa razão que, mesmo que o ViewState não esteja activo, todos os controlos de formulário têm o seu estado automaticamente reposto.

No entanto, na fase LoadPostData, é necessário saber o valor antigo de modo a poder compará-lo com o que vem no POST. Uma das formas é usar o ViewState para previamente guardar esse valor, que depois será usado na comparação com o valor recebido no POST. Note que, apesar da sua ligação próxima, são funcionalidades independentes.

Voltando ao ViewState, a estrutura de dados com todo o conteúdo de estado de visualização não é directamente convertida para string e armazenada no campo __VIEWSTATE. Lembra-se de falarmos que uma StateBag permitia fazer tracking? Ora bem, apenas são salvos no campo oculto as entradas da StateBag que estão marcadas como “dirty”, ou seja, apenas as entradas em que os seus estados são diferentes do estado estático ou estado por defeito. Não faz sentido guardar estado que será reposto automaticamente assim que exista um novo pedido e seja criada uma nova instância do controlo para atender esse pedido. Neste momento o leitor poderá estar a perguntar-se “então e os controlos definidos declarativamente?” Podemos definir de forma declarativa um controlo, como por exemplo:

<asp:Label ID="Label1" runat="server" Text="Label">

No momento em que a arquitectura ASP.NET faz parse do formulário, ao encontrar o atributo runat=”server” cria uma instância desse controlo e tenta corresponder todos os atributos definidos a propriedades publicas do controlo. No exemplo anterior olhando para o atributo Text percebemos que a arquitectura vai afectar a propriedade pública Text com o valor Label. Sabendo nós que quase todo o estado do controlo, se não todo, fica guardado em ViewState poderíamos ser levados a pensar que estes dados seriam considerados “dirty” visto que estamos a mudar o seu estado. Tal não acontece porque o TrackViewState() só é chamado no evento Init do ciclo de vida de uma página e todas estas afectações são feitas antes desse momento, ou seja, não serão consideradas “dirty”.

Como referido, quando o ocorre a fase de Init é chamado o TrackViewState(). Após o TrackViewState() ser chamado para todas as StateBags é chamado o LoadViewState(), que faz com que todo o estado dinâmico de visualização guardado seja reposto e, mais importante ainda, visto que neste momento o tracking já está activo todo o estado dinâmico carregado será considerado “dirty”. Esta sequência fará com que o estado dinâmico de visualização seja reposto e fique novamente persistente para futuros post backs, ou seja, faz com que seja novamente seriado para __VIEWSTATE. Através deste artigo espero que o leitor fique com uma ideia mais clara de como manter estado nos seus controlos tendo em atenção que quase todo, se não todo, o estado de visualização de um controlo é guardado em ViewState, mas apenas é persistido entre post backs o estado dinâmico. O estado por omissão que é criado quando é criada uma instância desse controlo (estado estático) não é persistido.

Bibliografia

Publicado na edição 15 (PDF) da Revista PROGRAMAR.