Por Dennes Torres
dennes@bufaloinfo.com.br
Dennes Torres possui as certificações MCAD, MCSD,MCSE, MCDBA e MCT.
Atualmente atua como diretor da Búfalo Informática, líder
do grupo de usuários DevASPNet, co-lider dos grupos devSQL e getWindows no Rio de Janeiro, podendo sempre ser encontrado na lista de discussão do grupo DevASPNet
(devaspnet-subscribe@yahoogrupos.com.br) bem como nas reuniões do grupo. Possui também um blog em http://cidadaocarioca.blogspot.com

Truques de (bom) Tratamento de Erros no ASP.NET



Erros em aplicações web podem surgir em diversos pontos, com diversas origens diferentes. Por isso o ASP.NET permite a interceptação dos erros em vários níveis, por controles, por código, por página, pelo web.config e pelo global.asax.

Porém este não é o objetivo deste artigo. Sobre esses itens acima já existem diversos outros artigos na web. Neste artigo vou falar de alguns truques bem específicos com tratamento de erros. Vou demonstra-los em situações bem específicas, mas vocês poderão adapta-los para outros casos que certamente nem imaginei.

São duas situações distintas que irei demonstrar : Primeiramente, o tratamento de erros retornados por execuções feitas diretamente pelos objetos datasource, o que é extremamente comum e que com certeza todos vocês precisarão utilizar. Em seguida, o tratamento de erros causados pelo ValidateRequest, o que é um caso muito específico.

Tratando erros vindos do banco de dados

Tanto o sqlDataSource como os objetos de dados (gridview, formview, detailsview, etc), possuem eventos terminados em ING e eventos terminados em ED. Os eventos terminados em ING indicam algo que está acontecendo, enquanto que os eventos terminado em ED indicam algo que já aconteceu.

Nos eventos posteriores a ida a banco, terminados em ED, podemos realizar o tratamento de erros vindos do banco de dados. Nos parâmetros do evento recebemos a indicação se ocorreu ou não um erro vindo do banco de dados.

Desta forma, programando os eventos ED podemos realizar o tratamento dos erros vindos do banco. A questão fica sendo como fazer a exibição da mensagem de erro para o usuário de uma forma padronizada, integrada, por exemplo, com os validadores. Para isso podemos utilizar alguns truques com um CustomValidator.

Vamos fazer um exemplo, já considerando a criação de uma camada de negócios e regras de negócios que irão gerar erros.

1) Crie um novo projeto web (testeErros)

2) Adicione na solução um projeto class library (prjProdutosErros), utilizando file->add->new project

No projeto class Library

3) Apague a classe existente

4) Adicione um dataset (dsProdutos) utilizando add new item

5) Da toolbox, pegue um table adapter e crie um tableAdapter ligado ao banco Northwind, contendo os seguintes campos da tabela products : ProductID, Productname, UnitPrice, UnitsInStock

6) Na última tela do wizard do table adapter desmarque a opção GenerateDbDirectMethods

7) Clique com o botão direito no table adapter e crie os métodos para AlterarProduto, DeletarProduto e InserirProduto. No AlterarProduto e DeletarProduto, altere a clausula where sugerida para que fique apenas com o id do produto

Na aplicação Web

8) Adicione uma referência para o projeto prjProdutosErros

9) Faça um rebuild de toda a solução

10) Na página default.aspx, adicione um objectDataSource

11) Configure o objectDataSource para apontar para o tableAdapter do projeto prjProdutosErros e para os métodos getData, AlterarProduto, DeletarProduto e InserirProduto

(se o tableAdapter não estiver visivel, confira o references e faça novamente o rebuild)

12) Adicione uma gridview na página

13) Ligue a gridview com o objectdatasource

14) Ative a paginação, ordenação, deleção e edição na gridview


Para embelezamento

15) Realize um autoFormat na gridview

16) Entre em "Edit Columns..."

17) Mova o command field para o final

18) Na coluna UnitPrice, configure o dataformatstring como "{0:C}"

19) Na coluna UnitPrice, altere para false a propriedade htmlencode

20) Na coluna unitprice, configure o itemstyle.horizontalalign como right

21) Na coluna unitsinstock configure o itemstyle.horizontalalign como right

22) Teste a aplicação, fazendo inclusive uma alteração de preço e confira se tudo está funcionando adequadamente.

No projeto prjProdutosErros

(Estaremos utilizando a técnica de regras de negócio com partial classes mostradas neste video)

23) Adicione uma classe chamada ProductsTableAdapter

24) Crie um namespace dsProdutosTableAdapters

Para testar se funcionou, crie uma sub qualquer dentro da classe, digite "me.", se o intellissense mostrar métodos e propriedades do table adapter, está funcionando, do contrário algo foi escrito errado.

25) No designer do dataset, selecione o método AtualizarProduto e altere, na janela de propriedades, o modifier para private e o name para privAtualizarProduto

26) Monte na classe ProductsTableAdapter o método AtualizarProduto, recebendo os mesmos parâmetros de privAtualizarProduto e chamando este internamente, mas implementando uma regra de negócio.

Namespace dsProdutosTableAdapters
    Public Class ProductsTableAdapter
        Public Sub AtualizarProduto(ByVal Productname As String, ByVal Unitprice As Decimal, ByVal UnitsinStock As Integer, ByVal Original_productId As Integer)
 
            If Unitprice > 300 Then
                Throw New ApplicationException("O produto está caro demais")
            End If
 
            Me.privAtualizarProduto(Productname, Unitprice, UnitsinStock, Original_productId)
 
        End Sub
    End Class
End Namespace

27) Faça um rebuild da solução, clicando com o botão direito nesta.

28) Teste a página. Desta vez existe uma regra de negócio que, se for desrespeitada, irá causar um erro.

29) Para tratar o erro, vamos programar o evento rowUpdated da gridView :

    Protected Sub GridView1_RowUpdated(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewUpdatedEventArgs) Handles GridView1.RowUpdated
        If Not IsNothing(e.Exception) Then
            Me.ClientScript.RegisterStartupScript(Me.GetType(), "xx", "alert(""ocorreu um erro na atualização dos dados"")", True)
            e.KeepInEditMode = True
            e.ExceptionHandled = True
        End If
    End Sub

O evento recebe como parâmetro a exception, se houver ocorrido. Então é necessário testar se existe uma exception.

É emitida uma mensagem simples em javascript

A propriedade keepInEditMode garante que o modo de edição da gridview será mantido, para que o erro seja corrigido

A propriedade ExceptionHandled é uma indicação de que a excessão já foi tratada e por isso o ASP.NET não deve mostrar sua mensagem padrão.

Foi utilizado o rowupdated da gridview ao invés do updated do datasource para que tivessemos controle sobre o KeepInEditMode

30) Teste a aplicação. A mensagem de erro ficou mais amigável, mas ela não é versátil, não se integra com o sistema de validação do ASP.NET.

31) Insira um validationSummary no topo da página

32) Insira um customValidator na página (qualquer posição) e configure a propriedade display para none

33) No código, crie uma variável boolean, ocorreuerro

Dim ocorreuerro As Boolean = False

34) Programe o serverValidate do customValidator

O customvalidator vai utilizar esse flag para saber que houve erro e exibir a mensagem de erro, da forma mais simples possível :

    Protected Sub CustomValidator1_ServerValidate(ByVal source As Object, ByVal args As System.Web.UI.WebControls.ServerValidateEventArgs) Handles CustomValidator1.ServerValidate
 
        args.IsValid = Not ocorreuerro
    End Sub

Observe que o customValidator não está de fato validando nada, mas a ligação dele com a variável ocorreuerro fará com que a mensagem do customValidator seja exibida sempre que ocorrer um erro. Da mesma forma o isValid da página ficará como false e nosso código, se bem organizado, saberá que não deve prosseguir.

35) Altere o evento rowUpdated para utilizar a variável ocorreuerro e o customValidator

    Protected Sub GridView1_RowUpdated(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewUpdatedEventArgs) Handles GridView1.RowUpdated
        If Not IsNothing(e.Exception) Then
            cvErroServidor.ErrorMessage = e.Exception.InnerException.Message
            ocorreuerro = True
            cvErroServidor.Validate()
            e.KeepInEditMode = True
            e.ExceptionHandled = True
        End If
    End Sub

Em caso de erro, define-se a mensagem do erro, a variável ocorreuErro e chama-se o validate do customValidator que, por causa da variável ocorreuErro, irá gerar o erro.

36) Teste a aplicação

Agora a mensagem será exibida no validationSummary. Pouca diferença ? A forma de exibição no validation summary pode ser facilmente personalizada, recebendo CSS, sendo transformada em uma menssagebox ou não, enfim, fazendo com que agora a mensagem seja mais versátil.


Personalizando a Gridview

Por fim, para facilitar ainda mais o trabalho, podemos criar uma gridview personalizada. Nossa gridview personalizada pode conter uma propriedade que defina se ela vai ou não interceptar erros vindos da base de dados.

1) Adicione na solução um novo projeto, prjNovaGrid, do tipo class library

2) Apague a classe que é inserida por default

3) Faça referência ao namespace System.Web

4) Adicione uma nova classe chamada NovaGrid

5) Faça com que herde as características da gridview

Inherits System.Web.UI.WebControls.GridView

6) Crie uma nova propriedade, conforme abaixo :

Podemos chamar a propriedade de InterceptarErros, seria de um tipo Enum podendo conter 3 valores : Não, Mensagem Simples, Validador

    Private vInterceptarErros As eInterceptar
 
    Public Enum eInterceptar
        Nao
        MensagemSimples
        Validador
    End Enum
 
    Public Property InterceptarErros() As eInterceptar
        Get
            Return (vInterceptarErros)
        End Get
        Set(ByVal value As eInterceptar)
            vInterceptarErros = value
        End Set
    End Property

7) Criar o validador

A geração de mensagens é até bem simples, mas o trabalho com validador, nem tanto. O objeto customValidator precisa ser criado bem cedo na sequencia de eventos da página, para garantir que ele participe do processo de validação, que ocorre cedo.

Porém a gridview possui apenas os eventos Init e Load. Esses eventos não permitem a inserção de objetos na coleção de controls da página. A solução fica sendo no evento init adicionar um tratador de evento para o InitComplete e neste tratador de evento sim, criar o validador. Veja como fica :

    Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
        MyBase.OnInit(e)
        AddHandler Me.Page.InitComplete, AddressOf CriarValidador
    End Sub
 
    Protected Sub CriarValidador(ByVal sender As Object, ByVal e As EventArgs)
        If InterceptarErros = eInterceptar.Validador Then
            cv = New CustomValidator
            AddHandler cv.ServerValidate, AddressOf TrataErro
            cv.Display = ValidatorDisplay.None
            Me.Page.Form.Controls.Add(cv)
        End If
    End Sub

Observe que :

O validador apenas é criado se a propriedade InterceptarErros assim determinar

É adicionado um tratador de eventos para o ServerValidate

O validador recebe display como none, para que a mensagem apareça apenas em um summary

O validador é adicionado na coleção de objetos da página

8) Crie a sub TrataErro

    Private Sub TrataErro(ByVal source As Object, ByVal args As System.Web.UI.WebControls.ServerValidateEventArgs)
        args.IsValid = Not ocorreuerro
    End Sub

Como antes, a sub trataErro nada faz, apenas usa a variável ocorreuErro para identificar se um erro aconteceu ou não

9) Faça um overrides no método onRowUpdated

A gridview possui um evento RowUpdated. Faz parte dos padrões do .NET que para cada evento existe um método equivalente, com o mesmo nome do evento mas iniciando-se com "on", neste caso "onRowUpdated".
Os métodos "on" são protected, por isso apenas visiveis durante um processo de herança, e são responsáveis por disparar o evento a que estão ligados.

A manipulação do evento em um processo de herança além de ser limitada não nos dá a garantia de que nosso código rodará antes do código inserido pelo próprio usuário no evento. Fazendo um overrides no método "on", temos a certeza de como será a sequencia de execução.

    Protected Overrides Sub OnRowUpdated(ByVal e As System.Web.UI.WebControls.GridViewUpdatedEventArgs)
        If Not IsNothing(e.Exception) AndAlso InterceptarErros <> eInterceptar.Nao Then
            If InterceptarErros = eInterceptar.MensagemSimples Then
                Me.Page.ClientScript.RegisterStartupScript(Me.GetType(), "msgerro", "alert(""Ocorreu um erro na atualização : " & e.Exception.InnerException.Message & """)", True)
            ElseIf InterceptarErros = eInterceptar.Validador Then
                cv.ErrorMessage = e.Exception.InnerException.Message
                ocorreuerro = True
                cv.Validate()
                e.ExceptionHandled = True
                e.KeepInEditMode = True
            End If
        End If
        MyBase.OnRowUpdated(e)
    End Sub

Bem semelhante com o que fizemos anteriormente com a gridview comum, porém agora sendo realizado durante a criação de um novo componente.

10) Faça um rebuild de toda a solução

11) Voltando para o projeto web, apague a gridview

12) Apague o código no code-behind

13) Na toolbox, você verá a NovaGrid, insira ela na página

14) Configure a NovaGrid exatamente como faria com uma grid comum.

15) Altere a propriedade InterceptarErros para "Validador"

16) Teste a aplicação

Tratando o ValidateRequest

O ASP.NET por padrão realiza o procedimento de ValidateRequest em todas as idas ao servidor. Nesse procedimento ele verifica se o usuário está enviando para o servidor alguma informação que se pareça com um ataque de HTML Injection ou Script Injection.

Se o ASP.NET achar que sim, ocorre o erro de ValidateRequest, uma grande tela amarela para o usuário.

Porém isso não quer dizer que o usuário tenha tentado invadir seu site. Se for um formulário de contato, por exemplo, o usuário pode inadvertidamente causar um problema ao utilizar, em meio a sua mensagem, símbolos de < e >

Com isso ficamos com a tarefa de interceptar esse possível erro e fazer um tratamento para exibir uma mensagem adequada ao usuário, ao invés da horrenda tela amarela.

O validateRequest pode ser habilitado ou desabilitado em cada página através do atributo ValidateRequest da tag @Page, ou para o site inteiro através da tag <pages> no web.config, mas não é recomendável que seja desabilitado.

Para tratar o erro seria normal pensarmos em utilizar um RegularExpressionValidator para nossos campos, porém seria por demais trabalhoso fazer isso para cada campo de nossa aplicação.

A solução então é interceptar o erro gerado pelo ValidateRequest. Para isso fazemos um tratamento do evento Error da página.

Vamos fazer um passo-a-passo para iniciarmos.

1) Crie um novo WebSite no Visual Studio.

2) Insira uma TextBox, um botão e um label (txtNome, cmdOk, lblResultado, respectivamente)

3) Programe o evento click do botão cmdOk :

Protected Sub cmdOk_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles cmdOk.Click
lblResultado.Text = "Ola " & txtNome.Text
End Sub

Testando

4) Execute a página (botão direito em design, view in browser)

5) Digite um nome e clique no Ok

6) Digite <img src=""> e clique no Ok

O ASP.NET gera o erro devido ao ValidateRequest

7) Desative o ValidateRequest utilizando o atributo na tag @Page

8) Teste novamente, digitando <img src=""> . Apesar de não aparecer imagem alguma, vemos um quadrado, indicando que o HTML está sendo processado pelo browser, o que caracteriza um ataque de HTML Injection.

Tratando o erro

9) Programe o evento Error da página

    Protected Sub Page_Error(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Error
        Dim ex As Exception
        ex = Server.GetLastError
 
        If TypeOf ex.GetBaseException Is System.Web.HttpRequestValidationException Then
            Response.StatusCode = 200
            Response.Write("Você digitou um caracter inválido")
            Response.End()
        End If
    End Sub

Porém isso não resolve o problema. Não resolve porque quando o erro ocorrer será exibida apenas a mensagem de erro, toda a página e seu layout deixarão de ser exibidos, o que não é agradável.

Ocorre que, quando a execução chega no evento Page_Error é porque o processo de execução já foi interrompido, a sequencia de eventos da página foi interrompida devido a um erro. Não tem como, pelo evento page_error, reiniciar esta sequencia para que a renderização da página possa ser montada.

A solução para o problema é fazermos um overrides na função DeterminePostBackMode, que é a função que inicia o processo que irá fazer o validate request. Fazendo este overrides, podemos capturar o erro antes deste acontecer, ou seja, antes da sequencia de eventos da página ser interrompida, nos permitindo mais flexibilidade no tratamento.

Para agradável exibição do erro, nos aproveitamos dos recursos de validação do próprio ASP.NET, utilizando um customValidator e um validationSummary para exibir a mensagem de erro.

Veja como fica :

    Public deuerro As Boolean
 
    Protected Overrides Function DeterminePostBackMode() As System.Collections.Specialized.NameValueCollection
        Try
            Return MyBase.DeterminePostBackMode()
        Catch ex As HttpRequestValidationException
            Dim col As New NameValueCollection
            col.Add(System.Web.HttpContext.Current.Request.Form)
            col.Add(System.Web.HttpContext.Current.Request.QueryString)
            deuerro = True
            Return col
        End Try
    End Function

De acordo com o processamento normal desta função, a coleção NameValueCollection é retornada com o conteúdo das coleções Request.Form e Request.QueryString. Então tentamos realizar a tarefa normal da função, porém no meio de um tratamento de erro. Se a função retornar um erro de validação de dados (HTTPRequestValidationException) então nós mesmos nos encarregamos de juntar o request.form e request.querystring em uma única namedvalueCollection e devolve-la, mas não sem antes marcarmos um flag indicando que o erro ocorreu.

Ou seja, estamos fazendo com que o ASP.NET ignore os erros de validateRequest (HTTPRequestValidationException), mas nós saberemos quando o erro ocorreu (através do flag, neste exemplo a variável deuerro) e iremos trata-lo.

O customvalidator vai utilizar esse flag para saber que houve erro e exibir a mensagem de erro, da forma mais simples possível :

    Protected Sub CustomValidator1_ServerValidate(ByVal source As Object, ByVal args As System.Web.UI.WebControls.ServerValidateEventArgs) Handles CustomValidator1.ServerValidate
        args.IsValid = Not deuerro
    End Sub

Observe que o customValidator não está de fato validando nada, mas a ligação dele com a variável deuerro fará com que a mensagem do customValidator seja exibida sempre que ocorrer um erro de validateRequest. Da mesma forma o isValid da página ficará como false e nosso código, se bem organizado, saberá que não deve prosseguir.

    Protected Sub cmdOk_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles cmdOk.Click
        If Me.IsValid Then
            lblResultado.Text = "Ola " & txtNome.Text
        End If
    End Sub

Desta forma conseguimos facilmente exibir uma mensagem de erro agradável quando ocorrer o erro de validação da requisição.



Dicas para quem está começando:
Veja os próximos eventos
que você não pode perder :

10/1/2009 Bufalo Cast: Windows Azure - Visao Geral
on-line - RJ
Por : devASPNet


17/1/2009 Bufalo Cast: SQL Server Data Services - Storage no Azure
on-line - RJ
Por : devASPNet


24/1/2009 BufaloCast: Visual Studio 2010: As novidades para o desenvolvimento de softwares
on-line - RJ
Por : devASPNet


31/1/2009 BufaloCast: Dublin: O novo host do Workflow Foundation
on-line - RJ
Por : devASPNet


13/12/2009 BufaloCast : SQL Server 2008 Energy Community Launch
on-line - RJ
Por : devASPNet

Leituras imperdíveis para quem está começando:

º Otimizando a performance no ASP.NET::..
º Criando objetos de paginação personalizados na grid::..
º Uma cesta de compras em ASP.NET::..
º Utilizando o Refresh de parâmetros no .NET::..
º ASP.NET FORMS Authentication::..
º Utilizando propriedades dinâmicas no .NET::..
º Corrigindo problemas de deleção em grid com paginação::..
º Cuidado com os componentes de validação::..
º Otimizando o InitializeComponent::..
º Movendo fonte de aplicações entre máquinas::..
º Agilizando a performance da IDE do VS.NET::..
º Utilizando Short Circuit no VB.NET::..


























  Parceiros:
20% de desconto para os membros do grupo na aquisição de livros e inscrição para eventos

Receba dicas de programação e programação .NET:
E-mail:
Incluir Excluir