ADO , ListView e mais...


Neste artigo vou abordar uma pequena aplicação que acessa uma base de dados mdb via ADO e exibe os dados de mais de uma tabela em um controle ListView. Meu objetivo é tentar esclarecer algumas dúvidas mais frequentes sobre conexão ADO e outras cositas mas...

Introdução

Vou usar o banco de dados Northwind.mdb (ele vem junto com o Access) e exibir informações das tabelas Produtos , Categorias e Fornecedores em um controle ListView.

Estrutura da tabela Produtos Estrutura da tabela Fornecedores Estrutura tabela Categorias

O relacionamento entre as tabelas é mostrado a seguir:

As informações que eu quero extrair destas tabelas são:
  • CódigoDoProduto(Produtos)
  • NomeDoProduto(Produtos)
  • Descrição (Categoria)
  • NomeDaEmpresa(Fornecedores)
  • NomeDoContato(Fornecedores)
  • Telefone(Fornecedores)

Obs: No destaque , em azul , o nome da tabela a qual o campo pertence.

Uma das primeiras dúvidas que eu gostaria de esclarecer é aquela famosa pergunta de quem esta começando a trabalhar com banco de dados relacionais:

Como faço para exibir informações que estão distribuidas em mais de uma tabela ??

Elementar meu caro Watson !!! Agrupe as informações e as exiba.

A maneira mais fácil de fazer isto é usar uma instrução SQL com a claúsula SELECT. Quando você têm um vínculo entre as tabelas poderá usar a palavra chave INNER JOIN na cláusula FROM de uma instrução SELECT para criar um conjunto de registros com campos das tabelas. Para selecionar campos de várias tabelas , você deve informar basicamente o seguinte:

A sintaxe básica para o JOIN é :

tabela 1 [INNER | LEFT | RIGHT ] JOIN tabela 2 ON tabela1.chave1=tabela2.chave2

Temos 3 opções de cláusulas usadas com JOIN e, o comportamento na maneira de retornar os registros difere em cada caso:
Tipos de JOIN Registros da Tabela da Esquerda Registros da Tabela da Direita
INNER Somente registros com um registro correspondente na tabela da direita Somente registros com um registro correspondente na tabela da esquerda
LEFT Todos os Registros Somente registros com um registro correspondente na tabela da esquerda
RIGHT Somente registros com um registro correspondente na tabela da direita Todos os Registros
Para o nosso caso temos que as informações serão extraidas de 3 tabelas diferentes. Teremos algo parecido com :
SELECT Produtos.CódigoDoProduto, Produtos.NomeDoProduto, Categorias.Descrição, Fornecedores.NomeDaEmpresa, Fornecedores.NomeDoContato, Fornecedores.Telefone
FROM Fornecedores
INNER JOIN (Categorias INNER JOIN Produtos ON Categorias.CódigoDaCategoria = Produtos.CódigoDaCategoria) ON Fornecedores.CódigoDoFornecedor = Produtos.CódigoDoFornecedor;
Vamos montar a nossa instrução SQL usando um variável string. Definindo esta variável como strSQL teremos:
strSQL = SELECT Produtos.CódigoDoProduto, Produtos.NomeDoProduto, Categorias.Descrição, Fornecedores.NomeDaEmpresa, Fornecedores.NomeDoContato, Fornecedores.Telefone
strSQL = strSQL & " FROM Fornecedores"
strSQL = strSQL & " INNER JOIN (Categorias INNER JOIN Produtos ON Categorias.CódigoDaCategoria = Produtos.CódigoDaCategoria) ON Fornecedores.CódigoDoFornecedor = Produtos.CódigoDoFornecedor;"

Assim nossa instrução estará contida na variável string sql. A próxima etapa é gerar o recordset com os campos definidos na instrução SQL. Fazemos isto usando o método Execute da ADO.

Supondo que estamos usando uma conexão ADO - conProdutos - criada anteriormente , e um recordset ADO - rsProdutos - através do qual iremos gerenciar as informações. O comando para é:

rsProdutos.Open strSQL , conProdutos , adOpenForwardOnly , adLockReadOnly , adCmdText

Chegamos agora a outra dúvida muito frequente :

Como eu estabeleço uma conexão com o banco de dados ? Abro ao iniciar o programa e fecho ao sair ou abro e fecho a conexão com o banco de dados ao carregar e descarregar o formulário ?

Podemos adotar duas estratégias distintas :

  1. Efetuar a conexão com o banco de dados ao iniciar a aplicação e usar esta conexão para todos os formulários do projeto , fechando a conexão somente quando a aplicação for encerrada
  2. Efetuar a conexão com o banco de dados somente quando o formulário que irá tratar os dados for carregado e fechar a conexão quando o formulário for descarregado.

Ambas as opções têm seus prós e contras e a escolha da melhor opção vai depender do tipo da aplicação. Se a aplicação não abrir mais de um formulário de dados ao mesmo tempo não haveria problemas em adotar a segunda opção. A primeira opção seria a mais comoda , e , embora manter uma conexão aberta com o banco de dados tem um custo elevado , creio que para um ambiente multiusuário (não Cliente/Servidor) os ganhos seriam maiores que abrir e fechar a conexão para cada formulário de dados.

Então eu vou usar a primeira opção no projeto exemplo. Para isto eu vou abrir a conexão em um módulo quando a aplicação for iniciada e fechar quando o usuário encerrar a aplicação. O código para abrir e fechar a conexão com o banco de dados Northwind.mdb é o seguinte:

Public conProdutos as ADODB.Connection

Public Sub Conecta_BD()

 Set conProdutos = New ADODB.Connection
 With conProdutos
   .Provider = "Microsoft.Jet.OLEDB.4.0"
   .ConnectionString = "Data Source=" & app.path & "\Northwind.mdb"
   .Open
 End With
End sub

Public Sub Desconecta_BD()
  conProdutos.close
  set conProdutos = Nothing
End Sub

Vou chamar a função Conecta_BD quando a aplicação for iniciada , então vou definir que o primeiro objeto a ser executado é o Sub Main() na opção Project -> Project1 Properties , opção Startup Object

O código que irei colocar em Sub Main deverá chamar a função Conecta_BD e chamar a tela de apresentação da aplicação. Algo assim:

Private Sub Main()
   Conecta_BD
   frmapresentacao.Show  
End Sub

Para criar o formulário de apresentação selecione a opção Project | Add Form e escolha Splash Screen . No formulário padrão inserido faça as alterações conforme o layout abaixo:

Na opção Project Properties na aba Make defina os valores para a versão em Version Number o título da aplicação em Application e as informações sobre a versão em Version Information
O formuláro de apresentação será exibido por dois segundos e em seguida descarregado. Fazemos isto inserindo um controle Timer no formulário e definindo a propriedade Interval como igual a 2000 ( 1000 igual a 1 segundo) e incluindo o seguinte código no evento Timer:
Private Sub Timer1_Timer()
   Unload Me
   frmmenu.Show
End Sub
Aqui estamos descarregando o formulário frmapresentação e exibindo o formulário frmmenu do sistema.

Vamos agora preparar o formulário - frmmenu - que apresentará o menu da aplicação. Eu vou criar um menu usando os controles ToolBar e ImageList. Para ver os detalhes sobre como implementar o menu leia o artigo : Visual Basic - Criando Menus Profissionais. Além disto eu vou a interface MDI , ou seja, o formulário principal - frmmenu - será uma formulário MDI e teremos formulário MDIChild contidos neste formulário.

No menu Project selecione Add MDI Form , o resto do procedimento é igual ap descrito no artigo. Ao final deveremos ter a seguinte aparência do formulário - frmenu :

Obs: Você ja deve saber que a interface MDI - Multiple-Document Interface - permite criar uma aplicação que mantém multiplos formulários dentro de um container - chamado - MDIForm. O Word e o Excel usam esta interface.

Nela você pode exibir vários formulários ao mesmo tempo. Os demais formulários são chamados de MDIChild. Um formulário MDIChild é obtido definindo a propriedade MDIChild como True. (Para isto é necessário que exista um MDIForm). O ícone do formulário MDIForm é

Defina propriedade WindowState do MDIForm para - 2- Maximized.

Agora vamos criar o formulário filho - MDIChild - que irá conter o controle ListView para exibir os dados das tabelas. Insira um novo formulário - frmlistview - e altere sua propriedade MDIChild para True ; em seguida inclua o controle ListView no formulário. O ícone de um formulário MDIChild é .

Para exibir o formulário - frmlistview - centralizado vamos ter que criar uma rotina , pois um formulário MDIChild não permite a utilização da propriedade StartUpPosition. Inclua o seguinte código no módulo do projeto:

Public Sub Centraliza_MDIChild(Formulario As Form)
  Formulario.Top = (Screen.Height) / 3 - Formulario.Height / 3
  Formulario.Left = (Screen.Width) / 2 - Formulario.Width / 2
End Sub

Nossa próxima etapa será criar a rotina que irá preencher o controle ListView com os dados da tabela . Vamos lá...

- Quando o formulário frmlistview for carregado no evento Load iremos fazer a chamada das procedures para centralizar o formulário , definir o tamanho e para preencher o controle ListView :

Private Sub Form_Load()
   Centraliza_MDIChild Me
   Me.Width = 10350
   Me.Height = 5175
   Preencher_Listview
End Sub
O código do procedimento Preencher_ListView é o seguinte:
Private Sub Preencher_Listview()
Dim rsProdutos As ADODB.Recordset
Set rsProdutos = New ADODB.Recordset

Dim strSQL As String

s
trSQL = "SELECT Produtos.CódigoDoProduto, Produtos.NomeDoProduto, Categorias.Descrição, Fornecedores.NomeDaEmpresa, Fornecedores.NomeDoContato, Fornecedores.Telefone"
strSQL = strSQL & " FROM Fornecedores"
strSQL = strSQL & " INNER JOIN (Categorias INNER JOIN Produtos ON Categorias.CódigoDaCategoria = Produtos.CódigoDaCategoria) ON Fornecedores.CódigoDoFornecedor = Produtos.CódigoDoFornecedor;"

rsProdutos.Open strSQL, conProdutos, adOpenForwardOnly, adLockReadOnly, adCmdText

'define o item da lista
Dim ItemLst As ListItem

'limpa a lista
ListView1.ListItems.Clear
'cabecalho do listview
listview_cabecalho

While Not rsProdutos.EOF

'insere o item do arquivo de dados
Set ItemLst = ListView1.ListItems.Add(, , rsProdutos!CódigoDoProduto)

'cada item precisa de um subitem para exibir na lista
ItemLst.SubItems(1) = "" & rsProdutos!nomedoproduto
ItemLst.SubItems(2) = "" & rsProdutos!Descrição
ItemLst.SubItems(3) = "" & rsProdutos!NomeDaEmpresa '
ItemLst.SubItems(4) = "" & rsProdutos!NomeDoContato
ItemLst.SubItems(5) = "" & rsProdutos!Telefone

'vai para o proximo registro
rsProdutos.MoveNext
Wend


rsProdutos.Close
ListView1.Refresh
End Sub

Perceba que usamos a procedure Listview_cabecalho para montar o cabecalho do controle.Na rotina definimos o nome de cada coluna e sua largura com base na largura do controle.

Definimos tambem o modo de exibição que o ListView usará para exibir os dados.(lvwReport). A presenca do objeto ColumnHeaders permite a criação de subitems.(Sem eles não conseguimos usar subitems).O código é:

Private Sub listview_cabecalho()
  ListView1.ColumnHeaders. _
    Add , , "Cod.", ListView1.Width / 18
  ListView1.ColumnHeaders. _
    Add , , "Produto", ListView1.Width / 5
  ListView1.ColumnHeaders. _
    Add , , "Descricao", ListView1.Width / 4
  ListView1.ColumnHeaders. _
    Add , , "Empresa", ListView1.Width / 6
  ListView1.ColumnHeaders. _
    Add , , "Contato", ListView1.Width / 6
  ListView1.ColumnHeaders. _
    Add , , "Telefone", ListView1.Width / 8
  'Define a forma de exibição do controle listview para relatorio
  ListView1.View = lvwReport
End Sub
O resultado será os seguinte:

Obs: Definimos as propriedades do controle ListView: FullRowSelect , GridLines e CheckBoxes para True , assim exibimos as linhas de grade , permitimos a seleção da linha inteira do controle e exibimos na primeira coluna caixas de seleção. Poderiamos fazer isto via código assim :

ListView1.Checkboxes = True
ListView1.GridLines = True
ListView1.FullRowSelect = True
Para manter a seleção  após perder o foco você pode fazer : HideSelection = False

Excluindo registros

Para excluir registros vamos permitir que o usuário selecione a linha da grade referente ao registro que deseja excluir e pressione a tecla DEL. O código é dado a seguir:

Private Sub ListView1_KeyUp(KeyCode As Integer, Shift As Integer)
Dim strsql As String

On Error GoTo trata_erro

If KeyCode = vbKeyDelete Then
  strsql = "DELETE FROM Produtos WHERE CódigoDoProduto = " & CLng(Me.Tag)
  If MsgBox("confirma a exclusão do registro referente ao produto => " _
      & ListView1.SelectedItem.SubItems(1), vbYesNo, "Excluir") = vbYes Then
      conProdutos.Execute strsqlEnd If
  End If
endif
  Exit Sub

trata_erro:
   MsgBox Err.Description
End Sub
- Após o usuário selecionar a linha , verificamos se a tecla DEL foi pressionada :- If KeyCode = vbKeyDelete Then

-A seguir montamos a instrução SQL para excluir os produtos cujos código sejam iguais a Me.Tag. Onde Me refere-se ao formulário e Tag a sua propriedade Tag. Nesta propriedade eu armazeno o valor do código do produto (Item.Text) quando o usuário clica no ListView. O código é :

Private Sub ListView1_ItemClick(ByVal Item As MSComctlLib.ListItem)
  Me.Tag = Item.Text
End Sub
Se você tentar excluir um registro irá receber a seguinte mensagem:

Sabe porque ? Bem , Como a tabela - Detalhes do Pedido - possui registros relacionados ao produto que você esta tentando excluir , a exclusão , se realizada , iria deixar registros na tabela - Detalhes do Pedido - sem o correspondente Produto na tabela Produtos .

Isto ocorre devido ao relacionamento entre os campos CódigoDoProduto da tabela Produtos e da tabela Detalhes do Pedido e não há jeito de evitar sem alterar os relacionamentos entre as tabelas. Então por que eu estou falando sobre isto ? Para que você fique esperto e preveja estas situações !!!

Você pode prever este tipo de erro e interceptá-lo gerando uma mensagem mais amigável ao usuário. Chamamos isto de tratamento de erros.(Uma boa aplicação sempre possui um tratamento de erros).Vamos melhorar o tratamento de erro usado no evento ListView1_KeyUp , o novo código para o tratamento de erros ficará assim :

trata_erro:
Select Case Err.Number
  Case -2147467259
    MsgBox "O registro não pode ser excluido pois existem registros " & vbCrLf & _
    " relacionados na tabela - Detalhes do Pedido - Verifique !!", vbCritical, " ERRO "
  Case Else
    MsgBox Err.Number & vbCrLf & " - " & Err.Description, vbCritical, "ERRO"
End Select
Exit Sub

Obs: Para uma relação dos erros relacionados com os provedores OLEDB leia o artigo : Relação de Erros ADO / OLE-DB

Ordenando os registros

Para terminar este artigo vou mostrar como podemos ordernar os dados exibidos no controle ListView. Vamos implantar a seguinte funcionalidade : quando o usuário clicar em uma coluna qualquer os dados serão ordenados de form ascendentes conforme aquela coluna.

A primeira coisa a fazer é inserir o código que realiza a ordenação no módulo do projeto . O código é :

Public Sub OrdenaListView(ByVal lvw As MSComctlLib.ListView, ByVal Coluna_Cabecalho As MSComctlLib.ColumnHeader)

 lvw.SortKey = Coluna_Cabecalho.Index - 1
 lvw.Sorted = True
 lvw.SortOrder = lvwAscending
End Sub

Iremos chamar esta rotina quando o usuário clicar na coluna do controle ListView , para usaremos o evento - ListView1_ColumnClick. O código é o seguinte:

Private Sub ListView1_ColumnClick(ByVal ColumnHeader As MSComctlLib.ColumnHeader)
  OrdenaListView ListView1, ColumnHeader
End Sub

Estamos chamando a rotina passando como parâmetros o controle ListView e a coluna clicada. Se você testar vai ver que a coisa realmente funciona.

Não esqueça de de fechar a conexão ao sair. Para isto use o evento QueryUnload do formulário MDI ; é só chamar a função criada no módulo para fechar a conexão. Lembra ?

Obs : O evento QueryUnload ocorre antes de um formulário ou aplicação fechar. Quando um objeto do tipo MDIForm fecha , o evento QueryUnload ocorre antes para o formulário MDIForm (MDI Pai) e depois para todos os os MDIChild ( MDI filhos).

Private Sub MDIForm_QueryUnload(Cancel As Integer, UnloadMode As Integer)
  Desconecta_BD
End Sub
Faça o download do projeto clicando aqui =  ADOListView.zip ( 14,0 KB )
Pronto !!! Acabei. Até o próximo artigo.....  

José Carlos Macoratti