Evitando a re-execução de tarefas devido ao refresh de páginas

A versatilidade da web acaba causando alguns problemas para aplicações criadas para ambiente web. Um dos problemas mais conhecidos é a simplicidade com a qual o usuário, apenas pressionando F5, consegue refazer operações que não poderiam ser feitas novamente, tal como deleção e inserção de dados.

Com o .NET temos novas e bem interessantes formas de resolver este problema da re-execução de páginas. Podemos criar uma classe e, utilizando herança, fazer com que seja utilizada como base para todas as páginas web.

Mas como reconhecer se a requisição é uma requisição original ou um refresh de página ?

Para isso podemos utilizar um algorítimo simples : Para cada requisição realizada por um usuário damos um ticket da requisição, um valor numérico incremental. Esse valor fica oculto na página, em um campo hidden e também guardado em sessão.

A cada nova requisição comparamos o valor do ticket com o valor contido em sessão. Se em algum momento o valor do ticket for menor que o valor contido em sessão então é porque ocorreu um refresh da página. Podemos então sinalizar em uma propriedade da página que a chamada trata-se de um refresh, permitindo a nosso código controlar a realização de operações críticas.

Vamos então criar uma classLibrary para realizar estas tarefas. Vou chamar esta classLibrary de libNotRefresh. Precisaremos adicionar nesta classLibrary uma referência para System.Web, já que isso não é default.

Vamos inicialmente criar uma classe para encapsular o acesso aos valores do ticket em sessão, vou chama-la de clsTicket. Veja como fica o código, bem simples :

Imports System.Web.HttpContext

Public Class clsTicket

 

    Public Shared Function NextTicket() As Integer

        If IsNothing(Current.Session("LastTicket")) Then

            Current.Session("LastTicket") = 1

        Else

            Current.Session("LastTicket") += 1

        End If

        Return (Current.Session("LastTicket"))

    End Function

 

    Public Shared Function LastTicket() As Integer

        If Not IsNothing(Current.Session("LastTicket")) Then

            Return (Current.Session("LastTicket"))

        Else

            Current.Session("LastTicket") = 0

            Return (0)

        End If

    End Function

 

End Class

 

Nesta classe podemos observar uma característica interessante que nem todos conhecem : É possível acessar os objetos do ASP.NET a partir de um componente.

Toda requisição web tem um contexto, o contexto http da requisição. Então utilizando System.Web.HttpContext.Current temos acesso aos objetos do ASP.NET com os valores do contexto atual da requisição, mesmo dentro de um componente.

Temos nesta classe dois métodos : LastTicket para fornecer o valor do último ticket que foi criado e NextTicket, para criar um novo ticket e fornecer seu valor. Em ambos os métodos foi tomado o cuidado de verificar se o valor em sessão está realmente preenchido. Ambos estão também definidos como shared, para simplificar o acesso a eles.

O próximo passo é garantirmos que todas as páginas gerem o ticket na forma de um campo hidden. Para fazermos isso podemos criar uma classe filha de System.web.Ui.Page e fazer com que todas as páginas de nosso site herdem as características desta classe.

Veja como fica :

Public Class paginaBase

    Inherits System.Web.UI.Page

 

    Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)

        Me.RegisterHiddenField("__Ticket", clsTicket.NextTicket)

        MyBase.OnPreRender(e)

    End Sub

 

End Class

É importante lembrarmos neste ponto de um padrão utilizado pela Microsoft nas classes do framework .NET . Para cada evento existente nestas classe existe um método equivalente com o mesmo nome do evento acrescido de "On" e definido como protected.

Este método é disparado dentro das classes no momento imediatamente anterior a ocorrencia do evento e é este método que é o responsável por disparar o evento.

Isso ocorre para que ao criarmos uma herança da classe possamos fazer uso do evento substituindo este método, ao invés de utilizar o evento diretamente. Isso além de simplificar o uso do evento mantém o evento disponível para as aplicações que serão simples usuárias da classe.

Assim sendo, em nossa classe paginaBase fiz overrides do método onPreRender, que ocorre imediatamente antes do evento PreRender. É o momento ideal de incluir algo na resposta, no caso o campo hidden __Ticket.

Para incluir este campo hidden utilizei o método RegisterHiddenField. Este método simplifica muito o trabalho pois já verifica se já existe ou não um campo hidden com este nome. Caso já exista apenas troca o valor, ao invés de duplicar o campo. Outra opção, até mais discreta, seria fazer uso do viewState.

Agora que fizemos o código para gerar o campo hidden devemos preparar o código para recebe-lo de volta, testar o ticket e configurar uma nova propriedade na página, isRefresh, para determinar se está ou não ocorrendo um refresh nesta página.

Para fazer isso vamos fazer um overrides no método onInit, responsável pelo evento Init do objeto Page. Veja como fica :

Public Class paginaBase

    Inherits System.Web.UI.Page

    Dim _isRefresh As Boolean = False

 

    Public ReadOnly Property isRefresh() As Boolean

        Get

            Return (_isRefresh)

        End Get

    End Property

 

    Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)

        Me.RegisterHiddenField("__Ticket", clsTicket.NextTicket)

        MyBase.OnPreRender(e)

    End Sub

 

    Protected Overrides Sub OnInit(ByVal e As System.EventArgs)

        Dim TickAtual, UltimoTick As Integer

        _isRefresh = False

 

        UltimoTick = clsTicket.LastTicket

 

        If IsNothing(Request.Form("__Ticket")) Then

            TickAtual = 0

        Else

            TickAtual = Request.Form("__Ticket")

        End If

 

        If TickAtual < UltimoTick Then

            _isRefresh = True

        End If

        MyBase.OnInit(e)

    End Sub

End Class

No método onInit comparamos o valor existente em sessão com o valor recebido via campo hidden. Se o valor no campo hidden for menor que o contido em sessão então ocorreu um refresh.

Vamos então fazer uma página para testar isso. Vamos fazer uma página com uma caixa de texto, um botão e uma listbox. Veja como fica inicialmente o código :

Public Class WebForm1

    Inherits libNotRefresh.paginaBase

 

     Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

 

        ListBox1.Items.Add(TextBox1.Text)

    End Sub

 

 

End Class

Faça um teste inicial com relação ao refresh. Insira algumas palavras, tal como "teste1", "teste2" e "teste3" na listbox e por fim faça refresh várias vezes. Verá que por mais que tente a última palavra não será duplicada na listBox.

Mas e o problema do refresh ? Por que neste exemplo ele não causa a duplicação ?

Quando vemos a palavra "teste3" (por exemplo) dentro da listbox, ao darmos refresh a transmissão para o servidor será do conteúdo anterior, "teste1" e "teste2", sem o teste3. Quando o código é re-executado a palavra "teste3" é re-inserida.

O mesmo acontece com labels, isso porque apesar dos labels não serem transmitidos para o servidor em um POST, eles guardam seu conteúdo no viewState.

Assim sendo com relação a interface visual o problema do refresh tem impacto mínimo. Ele apenas se torna mais grave em tarefas que vão além da interface, como por exemplo a inserção de um registro. Veja o código a seguir :

Public Class WebForm1

    Inherits libNotRefresh.paginaBase

 

     Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

 

        ListBox1.Items.Add(TextBox1.Text)

        cmd.Parameters("texto").Value = TextBox1.Text

        CN.Open()

        cmd.ExecuteNonQuery()

        CN.Close()

    End Sub

 

 

End Class

Criei um command e uma conexão para gravar o conteúdo da textbox em uma tabela de um banco de dados. Se após isso você testar novamente o refresh, observará na tabela do banco que o texto será duplicado de acordo com o número de vezes que você fizer refresh.

Para fazermos com que isso não aconteça agora é bem simples : Basta alterarmos a herança, para que nossa página herde da classe paginaBase e inserirmos no click do botão um if para testar a nova propriedade isRefresh.

Veja como fica :

Public Class WebForm1

    Inherits libNotRefresh.paginaBase

 

     Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

 

        ListBox1.Items.Add(TextBox1.Text)

        If Not isRefresh Then

            cmd.Parameters("texto").Value = TextBox1.Text

            CN.Open()

            cmd.ExecuteNonQuery()

            CN.Close()

        End If

    End Sub

 

 

End Class

Pronto. Testando novamente você observará que a inserção no banco não mais será duplicada. Basta fazermos o mesmo em todas as nossas páginas, trocarmos a herança e testarmos a propriedade isRefresh, para desta forma termos o controle da realização de refreshs nas páginas por parte dos usuários.

Dennes Torres
MCAD,MCSD,MCSE,MCDBA