domingo, 14 de junho de 2009

Manipulando XML no Flex com E4X

O Action Script 3 possui um conjunto de classes e funcionalidades para manipular documentos XML, baseados na especificação E4X (ECMAScript for XML) - mais precisamente na "ECMA-357 edition 2". Com isso, fica muito fácil trabalhar com XML, além do código ficar mais legível e fácil de manter.

As duas principais classes utilizadas são XML e XMLList - para utilizá-las, não é necessário importar nenhum pacote, já que elas ficam no core do framework.

Obs.: No ActionScript 2 também existia uma classe chamada XML, que foi renomeada para XMLDocument. As classes legadas do AS2 - XMLDocument, XMLNode e XMLNodeType - ficaram no pacote flash.xml.


Bom, mas em vez de explicar todas as funcionalidades do E4X, vamos diretamente a um exemplo prático comentado.

Vamos supor que temos o seguinte documento XML (XML literal) atribuído à variável "xmlProdutos":
var xmlProdutos: XML = 
 <produtos>
  <produto id="001">
   <nome>Creme Dental Happy</nome>
   <descricao>Tubo de 40g</descricao>
   <preco>3.50<preco>
   </produto>
  <produto id="002">
   <nome>Fio Dental Happy</nome>
   <descricao>50 metros</descricao>
   <preco>4.75<preco>
  </produto>
  <produto id="003">
   <nome>Escova Dental Happy</nome>
   <descricao>Escova de cerdas macias</descricao>
   <preco>3.45<preco>
  </produto>
 </produtos>;

1-) Acessando elementos do XML

Para acessar elementos ou nodos de um XML, é usada a notação "." - assim como se acessa as propriedades de um objeto qualquer. No exemplo dado, o documento "xmlProdutos" - que tem como elemento raiz "produtos" - possui uma lista de elementos filhos chamados "produto". Para retornar a lista de todos os produtos, na forma de XMLList, basta fazer da seguinte forma:
var listaDeProdutos: XMLList = xmlProdutos.produto;

Para obter um elemento específico, é só informar o seu índice (como se acessa um elemento de um array). Por exemplo, para pegar o primeiro "produto" do xmlProdutos:
var produto: XML = xmlProdutos.produto[0];

No XML de exemplo, um elemento "produto" possui três elementos filhos: "nome", "descricao" e "preco". Para obter a descrição do segundo produto, basta fazer:
var descricao: String = xmlProdutos.produto[1].descricao;

E para pegar o "id" de um determinado produto, deve-se usar um "@" - já que o id é um atributo do elemento "produto", e não um elemento filho, como no caso anterior.
var idProduto: String = xmlProdutos.produto[0].@id;

Já para obter a "descrição" de todos os produtos, basta não informar o índice do elemento "produto". O exemplo abaixo imprime no console a descrição de todos os produtos na forma de XMLList.
trace(xmlProdutos.produto.descricao);
No console, sairia assim:
Tubo de 40g
50 metros
Escova de cerdas macias

Para imprimir só o nome dos produtos, basta usar o método "text()":
trace(xmlProdutos.produto.descricao.text());
Obs.: Quando retorna só um elemento, como no caso de "trace(xmlProdutos.produto[0].descricao)", não é necessário utilizar o método "text()".


Outra forma de se obter os elementos de um XML é través da utilização dos métodos child() e attribute() - para obter, respectivamente, um elemento filho ou um atributo. Seguem exemplos com o uso dessa sintaxe:
txtResult.text = xmlProdutos.produto.child("nome");
xmlProdutos.produto.attribute("id");
Obs.: Uma das vantagens de utilizar os métodos child() e attribute(), é que é possível passar para eles uma variável do tipo String como argumento - dessa forma, a variável irá armazenar o nome do elemento ou do atributo e o código poderá ficar mais dinâmico.


Pode-se também buscar determinado elemento em qualquer nível do XML, sem conhecer seus nodos pais, ou quando o elemento tem pais diferentes, com o uso de ".." ou do método descendants(). Por exemplo, para obter a lista dos nomes de todos os produtos, poderia ser feito dessa forma:
var nomes: XMLList = xmlProdutos.produto.nome;
Ou simplesmente assim:
var nomes: XMLList = xmlProdutos..nome;
Ou ainda assim:
xmlProdutos.descendants("nome");

E se for utilizada a sintaxe "*", como no exemplo abaixo, "nome" poderia ter um elemento pai com qualquer nome, mas teria que estar necessariamente no segundo nível (com a sintaxe "..", ou utilizando o método descendants(), o elemento poderia estar em qualquer nível e em diferentes hierarquias dentro do XML).
var nomes: XMLList = xmlProdutos.*.nome;

Pode-se também iterar sobre os elementos de um XML. Se quisermos listar no console, por exemplo, o nome e o preço de todos os produtos, pode-se fazer assim:
for each(var xmlNode: XML in xmlProdutos.produto){
 trace(xmlNode.nome + " - R$" + xmlNode.preco);
}
Nesse caso, a variável xmlNode vai representar o elemento "produto" de cada iteração.

Pode-se ainda iterar sobre os elementos de um XML sem conhecer seus nomes, através do método children(), que retorna um XMLList com os elementos filhos, ou do método attributes(), que retorna um XMLList com os atributos do elemento. No exemplo abaixo, em que foi utilizado o método children() para retornar todos os elementos filhos de cada elemento "produto", a variável "element" vai representar cada um desses filhos em cada iteração - a seguir, é verificado se esse filho é do tipo "nome" ou "preco", através do método localname(), e, em caso positivo, é feito a impressão do valor deles no console.
for each(var element: XML in xmlProdutos.produto.children()){
 if(element.localName() == "nome")
  trace("Nome: " + element);
 else if(element.localName() == "preco")
  trace("Preço: " + element);
}

2-) Pesquisando elementos do XML

Pode-se pesquisar determinados elementos no XML. Por exemplo, para obter o elemento "produto" com o atributo "id" = "001", basta fazer:
var xmlProduto001: XML = xmlProdutos.produto.(@id=="001");

E para obter o "nome" do "produto" com atributo "id" = "002", pode-se fazer:
var nomeProduto002: String = xmlProdutos.produto.(@id=="002").nome;

Já para obter a descricao do "produto" com "nome" = "Creme Dental Happy", basta fazer:
var nomeProduto002: String = xmlProdutos.produto.(nome=="Creme Dental Happy").descricao;

Pode-se também utilizar filtros múltiplos. Por exemplo, para obter os produtos com "nome" = "Creme Dental Happy" e "descricao" = "Tubo de 40g", pode-se fazer assim:
var produto: XML = xmlProdutos.produto.(nome=="Creme Dental Happy" && descricao=="Tubo de 40g");

Também é possível utilizar operadores relacionais. Por exemplo, para obter todos os produtos com o preço menor que R$3,00, pode-se fazer assim:
var xmlProdutosFiltrados: XMLList = xmlProdutos.produto.(preco<3);
E para pegar todos os produtos que contenham a palavra "Dental", pode-se usar a função nativa search. Essa função irá retornar a posição inicial do padrão informado (no caso uma String) - se retornar "-1", significa que o padrão não foi encontrado:
var xmlProdutosFiltrados: XMLList = xmlProdutos.produto.(String(nome).search("Dental")>-1);
Atenção: Se tiver mais de um elemento retornado na pesquisa, vai voltar um XMLList e não um XML (se você não tiver certeza se vai voltar um ou mais elementos, use sempre um XMLList). Pode-se então iterar sobre os elementos do XMLList, assim como se faz com um XML. Exemplo de iteração em um XMLList:
var xmlList: XMLList = xmlProdutos.produto;
for each(var xmlNode:XML in xmlList)
 trace(xmlNode.nome);


3-) Alterando, deletando e incluindo elementos no XML

a) Para alterar ou atribuir um valor:
xmlProdutos.produto[0].descricao = "nova descrição";
b) Para deletar um ou mais elementos:
delete xmlProdutos.produto[0];
c) Para incluir um novo elemento: Supondo que deseja-se incluir um novo "produto", representado pelo XML abaixo:
var novoProduto: XML = 
  <produto id="534">
   <nome>Anti-séptico bucal Happy</nome>
   <descricao>Frasco de 150mL</descricao>
   <preco>6.35</preco>
  </produto>;
Para inserir um elemento no final do documento XML, pode-se utilizar o método appendChild:
xmlProdutos.appendChild(novoProduto);
Para inserir um elemento no final do início do XML, pode-se utilizar o método prependChild:
xmlProdutos.prependChild(novoProduto);
Pode-se ainda passar como parâmetro dos métodos appendChild ou prependChild um XMLList ou até mesmo um XML literal, como no exemplo abaixo:
xmlProdutos.appendChild(<produto id="534"><nome>Anti-séptico Happy</nome><descricao>Anti-séptico bucal Happy - frasco de 150mL</descricao><preco>6.35</preco></produto>);
Na utilização de um XML literal, pode-se também usar binding, através do uso de "{" e "}":
var id: String = "534";
var nome: String = "Anti-séptico Happy";
var descricao: String = "Anti-séptico bucal Happy - frasco de 150mL";
var nome: String = "Anti-séptico Happy";
var preco: Number = 6.35;

xmlProdutos.appendChild(<produto id={id}><nome>{nome}</nome><descricao>{descricao}</descricao><preco>{preco}</preco></produto>);
Pode-se ainda inserir o elemento em uma posição específica do XML. Por exemplo, para inserir o novo produto antes do produto que está na posição 2, pode-se usar o método insertChildBefore:
xmlProdutos.insertChildBefore(xmlProdutos.produto[2], novoProduto);
E para inserir depois do produto que está na posição 1, pode-se usar o método insertChildAfter:
xmlProdutos.insertChildAfter(xmlProdutos.produto[1], novoProduto);

4-) Carregando um arquivo XML Para carregar um arquivo XML externo, pode-se utilizar as classes URLLoader e URLRequest, conforme o exemplo abaixo:
import flash.net.URLLoader
   
private var xmlProdutos: XML;
private var urlLoader: URLLoader;
   
private function carregaXML(): void{
 urlLoader = new URLLoader();
 urlLoader.addEventListener(Event.COMPLETE, onComplete);
 //carrega o arquivo "produtos.xml" de uma url qualquer 
 urlLoader.load(new URLRequest("http://www.inf.ufsc.br/~feco/blog/data/produtos.xml"));
 //ou carrega o arquivo "produtos.xml" presente no diretório dados do mesmo contexto da aplicação (local dos fontes)
 urlLoader.load(new URLRequest("dados/produtos.xml"));
}
   
private function onComplete(evt:Event):void{
 xmlProdutos = new XML(urlLoader.data);
}
Já no MXML, basta utilizar o atributo source:
<mx:XML id="xmlProdutos" source="dados/produtos.xml"/>
Pode-se também utilizar o HTTPService, em vez do URLRequest. No exemplo abaixo, é utilizado o HTTPService do MXML:
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" initialize="dataService.send()">

 <mx:Script><![CDATA[
  import mx.rpc.events.ResultEvent;
   
  var xmlProdutos: XML;
   
  private function onResult(event: ResultEvent): void{
   xmlProdutos = XML(event.result);
  }
 ]]></mx:Script>
 
 <mx:HTTPService id="dataService" url="http://www.inf.ufsc.br/~feco/blog/data/produtos.xml" resultFormat="e4x" result="onResult(event)"/>

</mx:Application>

Referências:
AS3 E4X Rundown
Adobe Flex 3.3 Language Reference
Flex Quick Starts: Handling data - Accessing XML data
Ways to use E4X to filter data in ActionScript 3
Working with XML Working with XML, E4X and ActionScript 3


7 comentários:

Janderson disse...

muito bom Fernando, esse exemplo me ajudou muito, bem prático e objetivo, vlw Fera e sucesso!

Fernando disse...

Valeu Janderson! Qualquer dúvida estamos aí!

Unknown disse...

Só uma dúvida, eu carreguei o XML, consegui fazer a alteração nos atributos e como é que faço par salvar estas alterações no arquivo XML externo ?

Fernando disse...

Para manipular arquivo, persistir em banco, etc, terias que utilizar alguma tecnologia server-side, implementando algum serviço que seria invocado pela aplicação Flex.

Andre disse...

Olá, estou usando AS3 para tentar alterar um arquivo XML externo local através de um componente DataGrid e mesmo habilitando a edição dentro do componente ele não salva as alterações no xml quando clico em salvar, apenas salva o XML original sem as alterações, o que você acha que estou errando? Segue código:

var curso_xml:XML;

var myItems:Array = new Array("Curso_1","Curso_2","Curso_3","Curso_4");

listar_cursos.addEventListener(MouseEvent.MOUSE_DOWN, listar_cursos_click);
listar_cursos.buttonMode=true;
function listar_cursos_click(event:MouseEvent):void
{
myGrid.dataProvider = new DataProvider(myItems);
myGrid.editable=false;
}

var col_name:DataGridColumn = new DataGridColumn("label");
myGrid.addColumn(col_name);

salvar_xml.addEventListener(MouseEvent.MOUSE_DOWN, salvar_xml_click)
function salvar_xml_click(e:MouseEvent):void
{
var tFR:FileReference = new FileReference();
var tMessage = curso_xml;
tFR.save(tMessage, "curso.xml")
}

abrir_xml.addEventListener(MouseEvent.MOUSE_DOWN, abrir_xml_click);
abrir_xml.buttonMode=true;
function abrir_xml_click(e:MouseEvent):void
{

myGrid.dataProvider = new DataProvider();
myGrid.editable=true;
var xml_loader:URLLoader = new URLLoader();
xml_loader.load(new URLRequest("curso.xml"));
xml_loader.addEventListener(Event.COMPLETE, show_xml);
}

myGrid.addEventListener(Event.CHANGE, item_click)

function item_click(e:Event):void
{
curso_xml.topicos.topico.nome = myGrid.selectedItem.data;
}

function show_xml(evt:Event)
{
curso_xml = new XML(evt.target.data);
for(var i=0;i<curso_xml.topicos.topico.length();i++)
{
myGrid.addItem({label:curso_xml.topicos.topico.nome[i]})
}
}

Daniel disse...

Estou com um problema parecido, essa aplicação não está salvando nada no meu arquivo xml.





[Event(name="change_page_event", type="scripts.ChangePageEvent")]





{n}{u}{p});
Alert.show('Register Successfully Performed');
} else {
Alert.show('Register Failed');
}
}
]]>

Andre disse...

Consegui gravar no XML local a alteração através do:

texto_painel.addEventListener(Event.CHANGE, texto_painel_change)
function texto_painel_change(e:Event):void
{
myGrid.selectedItem.label = texto_painel.htmlText;
xml.telas.tela = texto_painel.htmlText;
}

Só que está alterando todo o XML, ou seja excluindo todo o conteudo e criando somente em cima destas tags "telas.tela" se o XML tem 70 tags ele exclui todas e somente insere o conteudo digitado no texto_painel. GOstaria de somente alterar o node que estou trabalhando é possível...?