Translate this page now :



Gostou da Página?
Clique no botão abaixo para indicar esta página para um amigo!



Pesquisa personalizada
Pesquisar Dicas:








Uma cesta de compras em ASP.NET

Com os novos recursos de WebControls, linguagens de alto nível para desenvolvimento web e orientação a objetos (real, com todos seus recursos) o desenvolvimento para Web com ASP.NET mudou radicalmente. Toda a lógica de planejamento de uma aplicação foi alterada, a lógica do desenvolvimento foi alterada.

Essa alteração, porém, é de difícil aprendizagem, pois envolve uma completa mudança na forma de desenvolver e planejar o desenvolvimento de uma aplicação.

Com este artigo pretendo demonstrar, através do exemplo de uma cesta de compras, o quão diferente é o desenvolvimento em ASP.NET quando utilizamos todos os recursos de que dispomos, demonstrando assim como o desenvolvedor necessita estar atento a esta nova forma de desenvolvimento.

Vamos utilizar no exemplo o banco Northwind. Este banco possui uma tabela chamada Products que contém uma lista de produtos com seus respectivos preços.

Teremos então apenas 2 WebForms : Um para listar os produtos e um 2o para mostrar a cesta de compras (chamaremos de products.ASPX e Cesta.ASPX).

Precisaremos criar uma nova tabela para guardar as informações sobre os produtos que o visitante deseja comprar. Vamos chamar esta nova tabela no banco de cesta :

CodUser Varchar(50)
IdProduto Int
Quant Int

A operação de inserção de um registro na tabela cesta não é tão simples, pois teremos que verificar se por acaso o visitante não solicitou o mesmo produto duas ou mais vezes. Se isso ocorreu deveremos somar as quantidades, ao invés de inserirmos um novo registro.

Então para podermos realizar esse processamento de inserção deveremos criar uma stored procedure no servidor de banco. O código da procedure ficará da seguinte forma :

CREATE PROCEDURE dbo.RegistrarCompra
         @Coduser varchar(50),
         @idProduto int,
         @quant int
         AS
         SET NOCOUNT ON
         
         if exists(select 1 from cesta 
         where coduser=@coduser and idproduto=@idproduto)
         begin
            update cesta set quant=quant+@quant
            where coduser=@coduser and idproduto=@idproduto
         end
         else
         begin
            insert into cesta (coduser,idproduto,quant) 
            values (@coduser,@idproduto,@quant)
         end
         
         
         RETURN 

A primeira página, Produtos.ASPX, conterá uma Grid que exibirá o nome do produto e o preço unitário do produto. Montar a grid é até um processo simples, mas está página conterá alguns pequenos truques. Vejamos um pequeno resumo dos passos para montagem desta página :

Inicialmente devemos fazer a montagem básica da grid, ligando a grid com o dataset, preenchendo o dataset a partir de um adapter, este por sua vez ligado a uma conexão. O truque neste passo é evitar que o DataSet gere os 4 commands que normalmente gera, solicitando que o DataSet gere apenas 1 dos commands, o de select, ao invés dos 4. Não deixe de dar uma lida em nosso artigo sobre performance com o ASP.NET, no qual comentamos e comparamos a performance de vários truques como esse.


Vamos então embelezar a grid. Podemos Definir as cores para itens normais e itens alternados, criando a alternancia de cores na grid. Definir as cores do cabeçalho e o alinhamento da coluna preço. Podemos também implementar uma paginação. Vale a pena dar uma lida no artigo sobre a personalização da paginação na grid.

O próximo passo é organizar a codificação e otimização da página. A paginação exige que a Grid seja revinculada com a origem de dados cada vez que o usuário trocar de página. Porém se refizermos a leitura do banco a cada página estaremos sacrificando tanto a performance como a escalabilidade da solução.

Para resolver este problema devemos manter o DataSet em sessão, evitando chamar o banco múltiplas vezes. Assim sendo devemos criar uma sub para carregar o DataSet em sessão e outra para vincular o dataset da sessão com a grid. As duas serão chamadas em diferentes momentos. Veja como fica o código :

 Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
         If Not Page.IsPostBack Then
              If IsNothing(Session("ds")) Then
                 CarregaDados()
              End If
              VincularDados()
         End If
End Sub
 Private Sub CarregaDados()
         Ds1.Products.BeginLoadData()
         DA.Fill(Ds1)
         Ds1.Products.EndLoadData()
         Session("ds") = Ds1
 End Sub
 Private Sub VincularDados()
         DG.DataSource = CType(Session("ds"), DS).Products
         DG.DataBind()
End Sub
 Private Sub DG_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) _ 
                               Handles DG.PageIndexChanged
         DG.CurrentPageIndex = e.NewPageIndex
         VincularDados()
End Sub


Para melhorar um pouco mais a performance devemos personalizar a sub InitializeComponent, conforme comentei em uma dica aqui no site. Veja como fica a personalização :

 Private Sub CarregaDados()
         Inicializar()
         Ds1.Products.BeginLoadData()
         DA.Fill(Ds1)
         Ds1.Products.EndLoadData()
         Session("ds") = Ds1
End Sub
 Sub Inicializar()
         Me.DA = New System.Data.OleDb.OleDbDataAdapter()
         Me.OleDbSelectCommand1 = New System.Data.OleDb.OleDbCommand()
         Me.CN = New System.Data.OleDb.OleDbConnection()
         Me.Ds1 = New CestaCompras.DS()
         CType(Me.Ds1, System.ComponentModel.ISupportInitialize).BeginInit()
         '
         'DA
         '
         Me.DA.SelectCommand = Me.OleDbSelectCommand1
         Me.DA.TableMappings.AddRange(New System.Data.Common.DataTableMapping() _
                             {New System.Data.Common.DataTableMapping("Table", "Products", _
                            New System.Data.Common.DataColumnMapping() _
                            {New System.Data.Common.DataColumnMapping("ProductID", "ProductID"), New _
                            System.Data.Common.DataColumnMapping("ProductName","ProductName"), _
                            New System.Data.Common.DataColumnMapping("UnitPrice","UnitPrice")})})
         '
         'OleDbSelectCommand1
         '
         Me.OleDbSelectCommand1.CommandText = "SELECT ProductID, ProductName, UnitPrice FROM Products"
         Me.OleDbSelectCommand1.Connection = Me.CN
         '
         'CN
         '
         Me.CN.ConnectionString = "Provider=SQLOLEDB.1;Persist Security Info=False;User          ID=sa;Initial Catalog=Northw" & _
         "ind;Data Source=.;Use Procedure for Prepare=1;Auto Translate=True;Packet          Size=40" & _
         "96;Workstation ID=BUFALO;Use Encryption for Data=False;Tag with          column collation" & _
         " when possible=False"
         '
         'Ds1
         '
         Me.Ds1.DataSetName = "DS"
         Me.Ds1.Locale = New System.Globalization.CultureInfo("en-US")
         Me.Ds1.Namespace = "http://www.tempuri.org/DS.xsd"
         CType(Me.Ds1, System.ComponentModel.ISupportInitialize).EndInit()
 End Sub


Desta forma otimizamos todos os postBacks que ocorrerão na página, garantindo que só iremos instanciar e configurar os objetos connection, dataAdapter e dataset quando realmente forem necessários e não a cada paginação realizada.

O próximo passo é permitir que o usuário entre com a quantidade que ele deseja adquirir do produto e solicitar a compra. Precisaremos então adicionar 2 colunas na Grid : Uma coluna de template e um Select Command.

Precisaremos personalizar essas duas colunas :

Na coluna com o Select Command deveremos alterar a propriedade Command Name. A propriedade Command Name é utilizada pela Grid para identificar o Command que foi clicado e direcionar para o evento correto. Personalizando o Command Name a grid irá chamar o evento ItemCommand, que identifica o clique em um Command personalizado. Assim sendo podemos utilizar o Command Name "Comprar" para personalizar este Command.

Na Template Column vamos trocar o título para "Quant." e editar o template, clicando com o botão direito na grid. Vamos inserir uma caixa de texto dentro da template column e chama-la de txtQuant.

Vamos então inserir um Command para realizarmos a gravação da compra solicitada pelo usuário. Durante o criação do command podemos utilizar uma das seguintes opções :

Voltar com o código da sub Inicializar para a sub InitializeComponent para que possamos criar o command. Optando por essa solução deveremos ter cuidado ao voltar o código da InitializeComponent para a inicializar. É claro que podemos voltar todo o código da InitializeComponent para a inicializar e não nos preocuparmos mais com isso, mas se desejarmos ser bem cuidadosos devemos observar que o Command para inserção é utilizado em momentos diferentes dos demais objetos, em um postBack diferente. Assim para mantermos uma otimização ideal do código deveremos não apenas manter a sub Inicializar, mas criar outras duas : InicializarConexao, pois a conexão será utilizada em mais de um processo de postBack, e InicializarAtualizacao para definir o command de insercao da compra.

Outra opção seria definir duas variáveis de conexão distintas, uma para a leitura outra para atualização. Consequentemente apenas duas subs, a Inicializar que já fizemos e uma InicializarAtualizacao. Não teríamos prejuizos em relação a performance mas teríamos alguma redundância de código.

Vamos ver como fica a codificação da 1a opção :

 Sub Inicializar()
         InicializarConexao()
         Me.DA = New System.Data.OleDb.OleDbDataAdapter()
         Me.OleDbSelectCommand1 = New System.Data.OleDb.OleDbCommand()
         Me.Ds1 = New CestaCompras.DS()
         CType(Me.Ds1, System.ComponentModel.ISupportInitialize).BeginInit()
         '
         'DA
         '
         Me.DA.SelectCommand = Me.OleDbSelectCommand1
         Me.DA.TableMappings.AddRange(New System.Data.Common.DataTableMapping() _
                    {New System.Data.Common.DataTableMapping("Table", "Products", _
                      New System.Data.Common.DataColumnMapping() _
                      {New System.Data.Common.DataColumnMapping("ProductID", "ProductID"), _
                      New System.Data.Common.DataColumnMapping("ProductName", "ProductName"),_
                       New System.Data.Common.DataColumnMapping("UnitPrice", "UnitPrice")})})
         '
         'OleDbSelectCommand1
         '
         Me.OleDbSelectCommand1.CommandText = "SELECT ProductID, ProductName,          UnitPrice FROM Products"
         Me.OleDbSelectCommand1.Connection = Me.CN
 '
         'Ds1
         '
         Me.Ds1.DataSetName = "DS"
         Me.Ds1.Locale = New System.Globalization.CultureInfo("en-US")
         Me.Ds1.Namespace = "http://www.tempuri.org/DS.xsd"
         CType(Me.Ds1, System.ComponentModel.ISupportInitialize).EndInit()
 End Sub
 Sub InicilizarConexao()
         Me.CN = New System.Data.OleDb.OleDbConnection()
         '
         'CN
         '
         Me.CN.ConnectionString = "Provider=SQLOLEDB.1;Persist Security Info=False;User          ID=sa;Initial Catalog=Northw" & _
         "ind;Data Source=.;Use Procedure for Prepare=1;Auto Translate=True;Packet          Size=40" & _
         "96;Workstation ID=BUFALO;Use Encryption for Data=False;Tag with          column collation" & _
         " when possible=False"
End Sub
 Sub InicializarAtualizacao()
         InicializarConexao()
         Me.CMD = New System.Data.OleDb.OleDbCommand()
         '
         'CMD
         '
         Me.CMD.CommandText = "[RegistrarCompra]"
         Me.CMD.CommandType = System.Data.CommandType.StoredProcedure
         Me.CMD.Connection = Me.CN
         Me.CMD.Parameters.Add(New System.Data.OleDb.OleDbParameter("RETURN_VALUE",System.Data.OleDb.OleDbType.Integer,_
                  4, System.Data.ParameterDirection.ReturnValue,False,_
                  CType(10, Byte), CType(0, Byte), "", System.Data.DataRowVersion.Current,Nothing))
         Me.CMD.Parameters.Add(New System.Data.OleDb.OleDbParameter("Coduser",System.Data.OleDb.OleDbType.VarChar, 50))
         Me.CMD.Parameters.Add(New System.Data.OleDb.OleDbParameter("idProduto",System.Data.OleDb.OleDbType.Integer,_
                  4, System.Data.ParameterDirection.Input,_
                  False, CType(10, Byte), CType(0, Byte), "", System.Data.DataRowVersion.Current,Nothing))
         Me.CMD.Parameters.Add(New System.Data.OleDb.OleDbParameter("quant",System.Data.OleDb.OleDbType.Integer, 4,_
                   System.Data.ParameterDirection.Input, _
                   False, CType(10, Byte), CType(0, Byte), "", System.Data.DataRowVersion.Current, Nothing))

End Sub

Observem que todo esse código não precisou ser digitado, esse código é gerado automaticamente no designer conforme configuramos os objetos. Apenas foi necessário acertarmos o momento em que o código é executado, através desta separação em subs.

Vamos enfim codificar a execução do Command. Teremos que programar o evento ItemCommand da Grid. Para isso nos aproveitaremos de dois recursos :

O nome que nós demos para a textBox nos permitirá utilizar o método FindControl para recuperarmos a quantidade digitada pelo usuário.

Configurando a propriedade DataKeyField da Grid podemos fazer com que a propria Grid guarde o código do produto de forma mais acessível para nós, facilitando nosso trabalho.

Veja como fica o código :

 Private Sub VincularDados()
         DG.DataSource = CType(Session("ds"), DS).Products
         DG.DataKeyField = "ProductID"
         DG.DataBind()
End Sub

Private Sub DG_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) _
         Handles DG.ItemCommand
         InicializarAtualizacao()
         CMD.Parameters("coduser").Value = Session.SessionID
         CMD.Parameters("idproduto").Value = DG.DataKeys(e.Item.ItemIndex)
         CMD.Parameters("quant").Value = CType(e.Item.FindControl("txtquant"),TextBox).Text
         CN.Open()
         CMD.ExecuteNonQuery()
         CN.Close()
End Sub


Da forma como está as textbox manterão a informação digitada através dos postbacks. Isso quer dizer que conforme o visitante for digitando quantidades e clicando em comprar as quantidades serão mantidas nas textbox, o que não é algo muito interessante.

Para evitar isso podemos utilizar um pequeno truque : editando a template column, podemos fazer um databind para a textbox, utilizando como expressão uma string vazia (""). Para causar o processamento desta expressão devemos utilizar a instrução Page.Databind() na sub VincularDados e fazer a chamada da sub VincularDados ao final do evento ItemCommand.

Assim sendo, com os vínculos realizados todas as textbox permanecem vazias entre os postbacks.

Para finalizar esta primeira página vamos inserir um link no topo da página para a cesta de compras, que chamaremos de cesta.aspx

Na página Cesta.ASPX o Select que faremos será um join entre a tabela cesta e a tabela products, fazendo uma filtragem pelo código do usuário. O Adapter, ao perceber o join, gerará apenas o command de select, não gerará os demais.

Vamos inserir uma Grid e ligar a grid com a tabela do DataSet. Da mesma forma precisaremos de uma Sub CarregaGrid, mas desta vez não guardaremos o DataSet em sessão, pois a cesta de compras do usuário encontra-se constantemente sendo alterada. Também não precisaremos nos preocupar tanto com a montagem de uma sub inicializar, pois em todos os postbacks precisaremos dos objetos de acesso ao banco.

O visitante pode, com certeza, desejar remover itens da cesta de compras. Devemos então possibilitar essa eliminação. Inserir um botão remover ao lado de cada registro para eliminar o registro é tarefa muito básica, vamos então montar uma grid um pouco mais sofisticada.

Vamos inserir uma template column com uma checkbox dentro da grid e, fora da Grid, vamos inserir um botão entitulado "Remover itens selecionados". Desta forma deixaremos que o usuário marque vários registros na grid para depois clicar no botão Remover para que todos os itens marcados sejam eliminados.

Montar o Command para realizar a eliminação do registro é até simples. O mais difícil é como identificar os itens que estão marcados. As checkbox tem todas o mesmo nome, já que estão inseridas em uma template column, portanto tentar acessa-las pelo nome no evento do botão não funcionará corretamente.

Pode-se inicialmente imaginar que a única solução seria vasculhar toda a grid, linha por linha, para identificar as checkbox que encontram-se marcadas. Mas na verdade podemos evitar essa solução utilizando alguns pequenos truques.

As checkbox serão clicadas para marcar ou desmarcar um item. Podemos então programar o evento CheckedChange da textbox para processar o item.

Porém, como a checkbox encontra-se dentro de um template, teremos que forçar o vínculo do evento. O primeiro passo será criar uma sub que tenha a mesma assinatura que o evento CheckedChange da Checkbox. Um truque para fazer isso é inserirmos uma checkbox qualquer dentro da página, gerarmos o evento e depois eliminarmos a checkbox. Podemos então alterar o nome da sub de evento (vamos utilizar alterouMarcacao) e devemos eliminar o Handles ao final da declaração da sub e alterar a visibilidade da sub, que estará como private, para Protected.

Para fazermos o vinculo das checkbox com o evento precisaremos fazer uma edição diretamente no código HTML, inserindo a chamada do evento na checkbox, veja :

                          OnCheckedChanged="AlterouMarcacao"

Cada checkbox precisará também guardar um valor indicando a que produto se refere. Então vamos inserir uma propriedade Value na checkbox, veja :

                         Value='<%# Container.DataItem("Productid")          %>'

Você notará que o VS reclamará do Value, deixará ele sublinhado em vermelho. Realmente Value não é uma propriedade do objeto checkbox no servidor, mas quando o servidor não reconhece uma propriedade no código HTML ele simplesmente passa a propriedade para o client, então não precisamos nos preocupar com isso.

Mas com certeza você deve estar se perguntando : Mas afinal, como será o processamento deste evento, teremos que ligar o autopostback ?

Não, não precisaremos. Vamos nos aproveitar da sequencia de processamento de eventos no servidor web. Quando um postBack ocorre, os eventos são processados na seguinte sequencia (estou resumindo) : Page_Load, Eventos change e equivalentes, eventos de ação, click e equivalentes.

Assim sendo, com o autoPostBack desligado, os cliques que ocorrerem nas checkbox gerarão execuções da sub AlterouMarcação, clique por clique. Só depois disso o evento clique do botão remover será executado, sendo que tudo isso ocorrerá no mesmo postback.

Podemos então nos aproveitar disso e utilizarmos um truque : no evento AlterouMarcacao vamos montar uma collection contendo o ProductID das checkbox marcados e no evento click causaremos a execução do command para eliminar os itens da collection.

Então certamente você se perguntará : Mas como saberemos o productID, se não temos acesso a checkbox pelo nome ? Simples, podemos utilizar o Sender, recebido como parâmetro na sub Alterou marcação e que contém a checkbox que disparou o evento. Como Value não é realmente uma propriedade do objeto checkbox, utilizaremos a propriedade Attributes para recuperar o value.

Veja como fica o código da página Cesta.aspx :

 Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
         'Put user code to initialize the page here
         If Not Page.IsPostBack Then
             CarregaDados()
         Else
             col = New Collection()
         End If
End Sub
 Sub CarregaDados()
         DsCesta1.cesta.BeginLoadData()
         DA.SelectCommand.Parameters("coduser").Value = Session.SessionID
         DA.Fill(DsCesta1)
         DsCesta1.cesta.EndLoadData()
         DG.DataBind()
End Sub

Protected Sub AlterouMarcacao(ByVal sender As System.Object, ByVal e As System.EventArgs)
         If CType(sender, CheckBox).Checked Then
              col.Add(CType(sender, CheckBox).Attributes("Value").ToString)
         End If
End Sub
 Private Sub cmdRemover_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdRemover.Click
         Dim s As String
         CN.Open()
         For Each s In col
                CMD.Parameters("idproduto").Value = s
                CMD.Parameters("coduser").Value = Session.SessionID
                CMD.ExecuteNonQuery()
         Next
         CN.Close()
         CarregaDados()
End Sub


Como você certamente observou durante a montagem deste exemplo, são muitos os truques em relação a sequencia de eventos e organização de subs, truques bem diferentes do que utilizavamos no ASP 3.0. Então esteja atento para aproveitar bem os novos recursos do ASP.NET.


DENNES TORRES
MCSD,MCSE,MCDBA


Quer saber mais?
Faça um curso na Búfalo Informática, Treinamento e Consultoria e
Prepare-se para o Mercado!
Veja o que a Búfalo tem para você.


� Búfalo Informática, Treinamento e Consultoria - Rua Álvaro Alvim, 37 Sala 920 - Cinelândia - Rio de Janeiro / RJ
Tel.: (21)2262-1368 (21) 9240-5134 (21) 9240-7281 e-Mail:
contato@bufaloinfo.com.br