domingo, 17 de novembro de 2002

Montando uma classe de TreeView com ASP e XML

Uma das tarefas mais "chatas", na parte de layout, é a exibição de dados em treeviews, uma lista de dados hierarquizados. Neste tutorial, mostraremos como criar um classe em VBScript, para adicionar e remover elementos de um treeview. Posteriormente, os dados da treeview, em XML, serão exibidos via XSL, com a rotina de navegação sendo feita via JavaScript.

Construção da Classe TreeView

Um recurso muito pouco usado no VBScript, mas muito poderoso, é a possibilidade de criarmos classes, que é uma boa para encapsularmos trechos de códigos, evitando repetições desnecessárias e aumentando a reutilização do código. Nesta primeira parte, iremos construir uma pequena classe, que será responsável pela adição/remoção de itens do nosso treeview. Começamos pela declaração da classe e das variáveis de instância:

<%

Class TreeView

 Private XMLDoc
 Private XSLDoc
 Private workNode
 Private InsertPoint

 '... inserir as demais rotinas aqui ...'

End Class

%>

Nada de complicado: declamos a classe TreeView e definimos algumas variáveis. Note apenas que, com o prefixo Private, essas variáveis são acessíveis apenas dentro do código da classe.

Private Sub Class_Initialize()
   set XMLDoc = Server.CreateObject( "MSXML2.DOMDocument" )
   XMLDoc.async = false
   XMLDoc.appendChild XMLDoc.CreateElement( "root" )
End Sub

Private Sub Class_Terminate
   set XMLDoc = nothing
   set XSLDoc = nothing
End Sub

Aqui, nos aprofundamos mais um pouco. A rotina Class_Initialize() é chamada sempre que criamos uma nova instância desta classe, e é responsável pela criação do objeto XML que conterá os dados do treeview. Como o objeto inicialmente é vazio, criamos o elemento "root", que armazenará os demais elementos. A rotina Class_Terminate() é executada quando essa classe é destruída, isto é, quando setando o objeto que a armazena como nothing. Isso assegura que a classe irá liberar a memória alocada para os objetos criados internamente à classe e constitui um bom hábito de programação.

Public Sub XSLData( cPath )
   set XSLDoc   = Server.CreateObject("MSXML2.DOMDocument")
   XSLDoc.async = false
   XSLDoc.load ( cPath )>
   If XSLDoc.parseError <> 0 then
      Response.Write XSLDoc.parseError.srcText & "<br />" & _
                     XSLDoc.parseError.reason & " Linha " & _
                     XSLDoc.parseError.line & " Col " & Object.parseError.linepos
      Response.End
   End If
End Sub

Aqui criamos nosso primeiro método, que pode (e será!) chamado externamente. Para quem acompanhou nossos demais tutoriais, provavelmente verá que esta rotina é muito parecida com a LoadXMLData. De fato, é a mesma rotina, onde apenas mudamos o nome da variável que receberá o objeto XML com os dados do arquivo XSL, cujo local no disco que passamos como parâmetro. Esse arquivo XSL será detalhado na próxima parte do tutorial.

Com os métodos básicos criados, passamos então para as rotinas necessárias para a inserção de elementos no objeto XML.

Public Sub AddItem( NodeName, ItemName, Title, Description )
   set workNode = XMLDoc.selectSingleNode( "//node[ @name = " & FormatString( ItemName ) & " ]" )
   if workNode is nothing then
      set workNode = XMLDoc.createElement( "node" ) with workNode
           .setAttribute "name" , ItemName
           .setAttribute "title" , Title
           .setAttribute "description", Description
      end with
      set InsertPoint = XMLDoc.selectSingleNode( "//node[ @name = " & FormatString( NodeName ) & " ]" )
      'Novo node principal ou não encontrou pai
      if len( NodeName & "" ) = 0 or ( InsertPoint is nothing ) then set InsertPoint = XMLDoc.selectSingleNode( "//root" )
      InsertPoint.appendChild workNode
   end if
End Sub


Essa será a rotina responsável por acrescentar elementos na estrutura. O parâmetro ItemName, informa qual será o nome único do node que estamos inserindo. Caso já hava um node com o mesmo nome, não faremos nada, pois ocorreu um erro na lógica da implementação da classe, por parte do programador que está usando a classe. Caso ainda não exista um node com o atributo name, criamos um novo elemento. Resta saber onde inseri-lo.

Esta tarefa é feita pelo trecho em negrito. Caso não encontre o node informado no parâmetro NodeName, ou se o mesmo for vazio, iremos inseri-lo no node principal, do contrário, colocaremos dentro do node informado. Essa rotina é a responsável pela hierarquização dos itens do treeview e é fundamental para o funcionamento da classe.

Public Function ExistItem( ItemName )
   set workNode = XMLDoc.selectSingleNode( "//node[ @name = " & FormatString( ItemName ) & " ]" )
   ExistItem = not ( workNode is nothing )
End Function
Public Sub DelItem( ItemName )
   set workNode = XMLDoc.selectSingleNode( "//node[ @name = " & FormatString( ItemName ) & " ]" )
   if not ( workNode is nothing ) then workNode.parentNode.removeChild workNode
End Sub


Esses dois métodos são responsáveis pela verificação se um determinado item do treeview existe e por excluir um node da árvore, respectivamente. Como são métodos públicos, podem ser acessados externamente ao código da classe.

Public Default Function BuildTree()
   BuildTree = XMLDoc.transformNode( XSLDoc )
End Function
Private Function FormatString( texto )
   FormatString = chr(39) & replace( replace( texto, chr(39), chr(39) & chr(39) ), "\", "\\" ) & chr(39)
End Function

Public Function DumpIt()
   DumpIt = XMLDoc.XML
End Function


Com esses três últimos métodos, terminamos a implementação da classe. O primeiro é responsável pela transformação do XML em HTML e baseia-se no arquivo XSL que iremos discutir na próxima parte deste tutorial. O FormatString apenas repete as aspas simples em um nome de node passado, cuidando para que o caracter "\" não apareça sozinho, pois ele seria interpretado como indicador de uma caracter literal, implicando em problemas, caso fossemos mostrar uma árvore de diretórios, usando o nome e path do arquivo como nome do item (uma vez que é único). O método DumpIt é usado apenas para depurarmos o conteúdo do objeto XML, apenas retornando seu conteúdo.

Assim, criamos a classe VBScript do ASP que irá gerenciar a árvore de itens, para posterior exibição. Ainda falta a rotina em JavaScript que irá funcionar em client-side, e que irá mostrar/esconder os itens do treeview, caso um nó seja selecionado:

<!--
function treeview( obj ){
 var dAll = document.all.tags("LI");
 for( var i=0; i < dAll.length; i++ ){
    if( dAll[ i ].id.substr( 0, obj.id.length + 1 ) == ( obj.id + '.' ) ){
        dAll[ i ].style.display == '' ? dAll[ i ].style.display = "none": dAll[ i ].style.display = '';
    }
 }
 obj.src.indexOf( 'DirOpened.gif' ) > -1 ? obj.src = 'imagens/DirClosed.gif': obj.src = 'imagens/DirOpened.gif';
}
// -->


Este código deve ser salvo em um arquivo chamado treeview.js e será usado na terceira parte do tutorial. Basicamente, ele troca a imagem do node selecionado (de um ícone que representa uma pasta aberta para um representativo de pasta fechada) e altera a propriedade display dos elementos filhos do mesmo.

Acabamos aqui a construção da classe. Segue um pequeno resumo do que vimos:



  • Aprendemos a criar classes em VBScript e a limpar os objetos usados quando a instância da classe é destruída.

  • Criamos um documento XML vazio e adicionamos um node "root" ao mesmo, para receber os demais itens do treeview.

  • Criamos as rotinas responsáveis pela manutenção da árvore de itens que será transformada no treeview.

  • Montamos uma pequena rotina em JavaScript, que o browser client irá executar quando o usuário clicar em um item que possua subitens.
Criando a folha de estilo XSL

Na primeira parte deste tutorial, criamos o código VBScript para implementar a classe TreeView no ASP. Nesta parte, iremos criar a folha de estilo (XSL), que será responsável por transformar o documento XML criado em HTML. Veremos que podemos trabalhar muito bem com estruturas de dados irregulares usando os recursos de recursividade do XSL.

Para criamos o efeito de "árvore", usaremos o elemento <UL> do HTML, para gerarmos a árvore hierarquica de itens. Neste exemplo, mantemos os caracteres indicativos de lista, apenas para visualizarmos melhor os itens. Esses caracteres podem ser retirados facilmente, acessando a propriedade CSS list-style dos elementos <UL> e setando-as para none.

Segue o cabeçalho padrão do XSL e o primeiro template:

<?xml version='1.0' encoding="iso-8859-1" ?>
 sl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="html" omit-xml-declaration="yes"/>

 <xsl:template match="/">
   <ul><xsl:apply-templates /></ul>
 </xsl:template>


Sem mistério: Criamos um template que se aplica ao node principal. Uma vez acessado, ele cria o primeiro elemento <UL> e depois pesquisa por outros templates que dêem conta de processar os demais nodes.

 <xsl:template match="node">

  <xsl:element name="li">

   <xsl:attribute name="id">id<xsl:number level="multiple" /></xsl:attribute>

   <xsl:element name="img">
    <xsl:attribute name="width" >15</xsl:attribute>
    <xsl:attribute name="height">15</xsl:attribute>
    <xsl:attribute name="id">id<xsl:number level="multiple" /></xsl:attribute>
    <xsl:attribute name="alt"><xsl:value-of select="@description" /></xsl:attribute>

    
<xsl:if test="count( ./node ) = 0">
     <xsl:attribute name="src">Imagens/File.gif</xsl:attribute>
    </xsl:if>

    <xsl:if test="count( ./node ) != 0">
     <xsl:attribute name="src">Imagens/DirOpened.gif</xsl:attribute>
     <xsl:attribute name="style">cursor:hand</xsl:attribute>
     <xsl:attribute name="onclick">javascript:treeview( this ) </xsl:attribute>
    </xsl:if>


   </xsl:element>

   <xsl:value-of disable-output-escaping="yes" select="@title"/>

  </xsl:element>

  
<xsl:if test="count( ./node ) != 0">
   <ul><xsl:apply-templates /></ul>
  </xsl:if>


 </xsl:template>

</xsl:stylesheet>

Este template irá ser aplicado aos nodes "node", criando novos elementos <LI> com o conteúdo do atributo title. Mas há algumas sutilezas no código que precisam de explicação:


  • O atributo id, tanto do elemento <LI> quanto do <IMG> são montados de forma a gerarem valores únicos. O elemento xsl:number retorna o nível de profundidade na recursão do template, de forma a gerar valores como 1, 1.1, 1.2, 1.2.1 e assim por diante. Esse aspecto do xsl:number é crucial para o funcionamento a contendo da treeview.

  • Setamos o atributo ALT da tag <IMG> como sendo o valor passado para o atributo description. Mantendo o cursor do mouse sobre a imagem, será aberta uma pequena caixa de texto, exibindo o conteúdo da mesma. Tem apenas valor cosmético, mas pode se tornar um recurso bastante útil, caso seja bem utilizada.

  • Para que o a função treeview (JavaScript) seja usada apenas nos casos em que é necessária e para mostrarmos as imagens corretas, criamos as duas estruturas de ifs em negrito. Caso o elemento atual possua elementos filhos ( count( ./node) != 0 ), criamos o atributo onclick na imagem, de forma a processar o evento do clique do mouse, exibindo/escondendo os elementos subordinados ao item clicado.

  • Atenção especial para o elemento xsl:value-of: O atributo disable-output-escaping="yes" informa ao processador XSLT que o conteúdo do atributo title deve ser mostrado integralmente, sem a substituição de, por exemplo, caracteres "<" por "&lt;", que é comportamento padrão. Isso possibilita passar código HTML neste atributo, que é especialmente útil para a criação de links, formatação de estilo, etc.

  • Para terminar, chamamos o elemento xsl:apply-templates, caso haja elementos subordinados ao que está sendo processado. Inicialmente, esta condição nao precisaria ser colocada, mas optamos por ter um documento HTML mais enxuto, sem tags <UL> vazias. Note que um novo elemento <UL> é criado antes de chamarmos a xsl:apply-templates novamente, o que, visualmente, implica em criarmos um novo grupo de itens, deslocado para a direita.


O código desta parte deve ser salvo no arquivo treeview.xsl e mantido no diretório onde os demais arquivos estão armazenados.


Bom, já aprendemos como criar classes no VBScript e como processar estruturas de dados irregulares em XSL, mediante o uso da recursividade. Agora, iremos exibir a estrutura de diretórios do servidor na classe TreeView e faremos alguns comentários finais.

Uma demo das funcionalidades da TreeViewClass

Até o momento, nossa aplicação já possui três arquivos, a saber:  



  • treeview.asp: que contém as definições da classe TreeView;
     
  • treeview.xsl: responsável pela transformação do documento XML, gerado pela classe TreeView em HTML a ser interpretado pelo browser;
      
  • treeview.js: código JavaScript, responsável por exibir/esconder elementos do treeview;
Com estes arquivos em mãos, podemos passar para a codificação do index.asp:

<% Option Explicit %>
<!--#include file="treeview.asp" -->
<html>
<head>
<title>TreeViewClass Demo #2: Lista de arquivos do servidor</title>
<script type="text/javascript" src="treeview.js"></script>
</head>
<body>


Um pequeno comentário: na linha em negrito, incluimos o arquivo treeview.js, eliminando a necessidade de copiar o código em todos os arquivos que utilizem a classe TreeView.

<%

dim oTreeView, FSO, rootFolder
set oTreeView = New TreeView
set FSO = Server.CreateObject( "Scripting.FileSystemObject" )
rootFolder = Request.ServerVariables( "APPL_PHYSICAL_PATH" )


Aqui fazemos o fundamental: criamos uma instância da classe TreeView e do objeto FSO do ASP, que irá permitir o acesso à árvore de diretórios do servidor. Definimos como o primeiro diretório a ser pesquisado, o diretório físico apontado pelo diretório virtual, criado no PWS/IIS. Note que em alguns provedores, que este diretório pode não ser o mesmo do seu site, o que pode implicar em problemas, caso a estrutura de diretórios seja muito grande. Caso o script apresente problemas, como, por exemplo, timeout, experimente exibir o valor desta variável e certificar-se que a mesma aponta para o diretório correto.

With oTreeView
     .XSLData( server.mappath( "treeview.xsl" ) )
     LoadFolders( rootFolder ) 'Carga recursiva dos arquivos/dirs, a partir do diretório atual
     Response.Write .BuildTree()
End With
set oTreeView = nothing
set FSO = nothing

Este trecho apenas carrega o documento XSL que criamos e inicia a rotina LoadFolders, que carregará, recursivamente, o conteúdo dos diretórios. Ao término da mesma, exibimos o produto da transformação do documento XML em HTML e, terminado o processamento da página ASP, o controle passará ao browser, que chamará a função treeview(), quando esta for necessária.

Note que, após terminarmos as operações com a classe TreeView, nós a destruimos, juntamente com outros objetos não mais necessários. Isso permite que a memória alocada para os objetos envolvidos nas operações com o documento XML seja liberada. Este ponto é especialmente pertinente caso seu servidor tenha um número de acessos simultâneos significativo.

Sub LoadFolders( cPath )
   Dim oFolder, i
   set oFolder = FSO.GetFolder( cPath )
   ' Adiciona os arquivos no path
   for each i in oFolder.Files
       oTreeView.AddItem cPath, i.path & i.name, i.name, i.path
   next
   for each i in oFolder.SubFolders
       ' Inclui SubDirs e processa-os recursivamente
       oTreeView.AddItem cPath, i.path, i.name, i.path
       LoadFolders( i.path )
   next
   set oFolder = nothing
End Sub

%>
</body>
</html>

Esta rotina é a responsável pela navegação entre os subdiretórios. Com o path passado por parâmetro, uma coleção GetFolder do objeto Scripting.FileSystemObject é criada (e destruída ao final - note). No primeiro loop for..next, para cada arquivo da coleção Files, criamos um item na TreeView. Os valores que usamos são:



  • cPath: o path atual, com sendo o node onde os arquivos serão alocados.

  • i.path & i.name: o nome único do node (é impossível estar duplicado a combinação de path e nome de arquivo)

  • i.name: no terceiro parâmetro, informa o que será exibido na tag <LI> do treeview.

  • i.path: será usado como atributo da imagem representativa de arquivo, o caminho completo do mesmo.
O mesmo vale para o segundo loop for..next, mas este é destinado à navegação entre os diretórios. Para cada um deles, é criado um novo node e a rotina LoadFolders é chamada novamente, apontando para o caminho do mesmo. Como dito anteriormente, esta rotina pode ultrapassar o tempo máximo de processamento do script, implicando em término prematuro da mesma, caso o número de arquivos ou diretórios seja muito grande. Mas não há de ser o caso...

Pronto para os testes? Ótimo! Rode o index.asp criado e clicando ícones de pastas, os itens subordinados a esta serão exibidos/escondidos. Mantendo o mouse sobre as imagens, irá aparecer o tooltip com o caminho completo do arquivo. Exatamante como queríamos. :-)

Até a próxima!

Faça o download do material deste artigo.

Esta matéria foi postada originalmente no ASP4Developers por Rubens N. Farias (site), que na época era "pós-graduado em análise de sistemas orientados a objetos, MCP, MCSD, MCAD, MCSD.NET e consultor em TI, além de idealizador do projeto ASP4Developers. Desenvolve sistemas sob medida, focados na satisfação do usuário, com qualidade e custo realista.". Hoje, vai saber...

0 comentários: