Desenvolvimento - C#

OPEN XML – Um ponto de vista de desenvolvedor

Visão geral da aplicabilidade do padrão OpenXML no desenvolvimento .NET.

por Mauro Zamaro



O que é o OpenXML ?

O OpenXML é um padrão aberto (ISO) de arquivos para documentos, planilhas, apresentações baseado em XML que pode ser implementado por diversas aplicações em diversas plataformas. O propósito do padrão OpenXML é desvincular os documentos (e as informações contidas neles) das suas aplicações de origem para promover a interoperabilidade entre os diversos sistemas existentes nas mais diversas plataformas. SEM PERDA DE INFORMAÇÃO.

A estrutura de um arquivo OpenXml

Um arquivo OpenXML nada mais é do que um arquivo ZIP criado a partir de uma determinada estrutura de pastas e arquivos (XML + binários de imagens e demais objetos incorporados a um documento). É basicamente construído de partes (elementos) e seus (inter)relacionamentos. Como o formato Zip permite o acesso aleatório ao conteúdo do arquivo, é possível obter parte de seu conteúdo sem a necessidade de realizar a leitura INTEGRAL do documento.

As partes em um pacote OpenXml são criadas como Marcas XML. O fato de o XML ser estruturado em texto simples, é possível acessar seu conteúdo usando editores de texto convencionais e, além disso, garantir uma taxa de compressão muito alta desse conteúdo (no arquivo ZIP) gerando assim arquivos com o mínimo tamanho final possível.

Da mesma maneira, é possível manipular estas partes XML (via código) por meio de DOM (System.Xml.XmlDocument) ou XPath (System.Xml.XPath.XPathDocument).

Estruturalmente, o OpenXML é um pacote que segue as convenções do OPC (Open Packaging Conventions) e é basicamente uma coleção de partes de documento que seguem uma seqüência de segumentos de paths (caminhos como ‘word/document.xml’.

Temos basicamente 3 “linguagens” (isso não é um termo mais adequado mas não achei outro que ilustrasse melhor o conceito) cada um para manipulação de tipos de documentos OpenXml (que são basicamente 3):

· WordprocessingML: Documentos de TEXTO (como os do Word);

· PresentationML: Arquivos de Apresentação (como os do PowerPoint);

· SpreadsheetML: Arquivos de Planilhas eletrônicas (como as do Excel);

Em .NET é possível criar arquivos OpenXml apenas com o apoio do namespace System.IO.Packaging e do System.Xml desde que todas as “especificações” do formato sejam seguidas (e construídas na unha, como alguns gostam de fazer). Se quiserem seguir este caminho, Boa sorte, boa viagem mas NÃO ME CHAME! eu vou, provavelmente, estar ocupado com algo muito mais divertido junto ao meu filho e minha esposa!

Como alternativa, temos um SDK (GRÁTIS) disponível para ajudar-nos a criar arquivos OpenXml (via código). No exemplo que segue, eu utilizei o OpenXML format SDK 2.0 CTP September 2008.

Como criar um documento (Word) OpenXML usando o SDK?

Cenário

Uma empresa que vende um conjunto específico de produtos através de distribuidores parceiros precisa de uma solução que permita extrair de uma base de dados (cadastro de produtos) uma tabela de preços para envio, por e-mail por exemplo, de forma sistemática sempre no início de cada mês ou durante a vigência de uma determinada promoção.

Vamos considerar que essa empresa tenha um sistema que exponha uma estrutura de dados específica (entidade) e, a partir desta entidade de dados, haja um serviço que utilize esta estrutura como base para gerar um relatório que possa ser anexar num por e-mail para os distribuidores cadastrados (a tabela de preços de produtos). Meu conjunto de distribuidores é composto de pessoas que têm acesso a ferramentas que suportem o formato OpenXML (como o Word 2007) para acessar esses documentos.

Os dados

Minha entidade de dados foi implementada como segue o código abaixo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OpenXMLPOC.Lib.Data
{
/// <summary>
/// Classe DataExample - servirá de Entity para que eu agregue num Documento OpenXML
/// </summary>
[Serializable()]
public class DataExample
{
public DataExample()
{
//para compatibilidade com a serialização/deserialização : Necessário um construtor sem parâmetros
}
/// <summary>
/// Property code
/// </summary>
public int Code { get; set; }
/// <summary>
/// Property Description
/// </summary>

public string Description { get; set; }
/// <summary>
/// Property Price
/// </summary>

public decimal Price { get; set; } //observação: Todos as properties que se deseja serializar precisam ter o get; E set;
/// <summary>
/// Gera a lista de parâmetros e seus valores para a visualização em formato string separado por Pipe.
/// </summary>
/// <returns></returns>

public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (System.Reflection.PropertyInfo pi in this.GetType().GetProperties())
{
sb.AppendFormat("{0} : {1} | ", pi.Name, pi.GetValue(this, null));
}
return sb.ToString();
}
}

Uma coleção dessas entidades (a tabela de preços efetiva) foi implementada como segue (derivado diretamente de List<DataExample> para facilitar a implementação da coleção de entidades:


[Serializable()]
public class Example: List<DataExample>
{
//Herda automaticamente os construtores de List<DataExample> cf:Generics
public string ToXmlString()
{
System.IO.MemoryStream stream = null;
string ret = string.Empty;
try
{
stream = new System.IO.MemoryStream();
System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(this.GetType());
serializer.Serialize(stream, this);
stream.Position = 0;
UTF8Encoding enc = new UTF8Encoding(true);
ret = enc.GetString(stream.ToArray());
}
finally
{
if (stream != null)
{
stream.Flush();
stream.Close();
stream.Dispose();
}
}
return ret;
}
}
}

O “Gerador” do documento OpenXml com os dados

O Gerador do documento, então, foi implementado como segue (a seguir, os comentários específicos sobre as principais partes dessa classe!)

//Referências (extras) usadas por este código:
//WindowsBase.dll
//DocumentFormat.OpenXml.dll //Referencia do OpenXML SDK 2.0 (versão 2.0.3302.0 CTP Setembro/2008)
// maiores informações: //
http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=1647&SiteID=1

using System.IO;
using System.Text;
using System.Xml;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;

namespace OpenXMLPOC.Lib.Business
{
/// <summary>
/// Classe que gera o documento OpenXML a partir de um conjunto de entidades de dados Example
/// <remarks>Padrão de construtor Single-Ton</remarks>
/// </summary>
public class DocManager
{
const string wordmlNamespace = "
http://schemas.openxmlformats.org/wordprocessingml/2006/main";
/// <summary>
/// Construtor
/// </summary>
private DocManager()
{
//Acesso restrito
}
/// <summary>
/// Instância de DocManager
/// </summary>
static DocManager _instance; // guarda a referência da primeira instancia
public static DocManager GetInstance()
{
if (_instance == null)
{
_instance = new DocManager(); // consigo acessar o construtor privado da própria classe.
}
return _instance;
}
// create a new package as a Word document.
public static void CreateNewWordDocument(string document)
{
using (WordprocessingDocument wordDoc = WordprocessingDocument.Create(document, WordprocessingDocumentType.Document))
{
// Set the content of the document so that Word can open it.
MainDocumentPart mainPart = wordDoc.AddMainDocumentPart();
SetMainDocumentContent(mainPart);
}
}
// Set the content of MainDocumentPart.
public static void SetMainDocumentContent(MainDocumentPart MainPart)
{
const string docXml =
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<w:document xmlns:w=""
http://schemas.openxmlformats.org/wordprocessingml/2006/main"">
<w:body>
<w:p>
<w:r>
<w:t></w:t>
</w:r>
</w:p>
</w:body>
</w:document>";
using (Stream stream = MainPart.GetStream())
{
byte[] buf = (new UTF8Encoding()).GetBytes(docXml);
stream.Write(buf, 0, buf.Length);
}
}
/// <summary>
/// Exporta a lista exemplo para dentro de um documento OpenXml (Word 2007)
/// </summary>
/// <param name="pExampleList"></param>
/// <param name="pPath"></param>
public void ExportListToWord(OpenXMLPOC.Lib.Data.Example pExampleList, string pPath)
{
using (WordprocessingDocument wdDoc = WordprocessingDocument.Create(pPath, WordprocessingDocumentType.Document))
{
MainDocumentPart mainPart = wdDoc.AddMainDocumentPart(); //adiciona o MainDocumentPart
SetMainDocumentContent(mainPart); // Inicializa a parte principal do documento com o schema padrão (1 parágrafo em branco).
//GRAVA O XML integral no documento
XmlDocument bodyXml = new XmlDocument();
bodyXml.LoadXml(wdDoc.MainDocumentPart.Document.Body.OuterXml);
XmlNode fistText = bodyXml.GetElementsByTagName("w:t")[0];
fistText.InnerText = pExampleList.ToXmlString();
// Grava cada item da lista num novo parágrafo
foreach (Data.DataExample de in pExampleList)
{
//cria um novo conjunto <w:p><w:r><w:t></w:t></w:r></w:p> sendo que o conteúdo da tag w:t será a versão em string do objeto de dados <DataExample>.
XmlNode NewParagraph = CreateNewParagraph(bodyXml, de.ToString());
//Adiciona o parágrafo no corpo do documento.
bodyXml.DocumentElement.AppendChild(NewParagraph);
}
// garante que o xml para o MainDocumentPart conterá todas as informações agregadas
wdDoc.MainDocumentPart.Document.Body.InnerXml = bodyXml.InnerXml;
//Salva o Documento (word/document.xml) dentro da Package (pacote)
wdDoc.MainDocumentPart.Document.Save();
//fecha o arquivo.
wdDoc.Close();
}
}
/// <summary>
/// Cria um novo parágrafo nos moldes "<w:p><w:r><w:t></w:t></w:r></w:p>" com o texto do parágrafo incluso na tag adequada
/// </summary>
/// <param name="bodyXml"></param>
/// <param name="textoParagrafo"></param>
/// <returns></returns>
private static XmlNode CreateNewParagraph(XmlDocument bodyXml, string textoParagrafo)
{
XmlNode NewParagraph = bodyXml.CreateElement("w:p", wordmlNamespace);
NewParagraph.AppendChild(bodyXml.CreateElement("w:r", wordmlNamespace));
NewParagraph["w:r"].AppendChild(bodyXml.CreateElement("w:t", wordmlNamespace));
NewParagraph["w:r"].LastChild.InnerText = textoParagrafo;
return NewParagraph;
}
}
}

Sobre o DocManager

A primeira coisa a ser considerada é a estrutura de um arquivo OpenXml. Como comentado acima, o arquivo OpenXml é um arquivo Zip (Package) com partes xml (e outros arquivos binários , quando necesário)

O pacote contém, para um arquivo de documento OpenXML (docx), pelo menos os seguintes elementos:

\[Content Types].xml : Arquivo XML com a definição dos conteúdos do documento;

\_rels\.rels.xml : Arquivo XML com os relacionamentos entre as partes do documento;

\word\document.xml : Arquivo com o conteúdo do documento em formato xml.

O arquivo docment.xml tem o seguinte schema básico:

<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<w:document xmlns:w=""
http://schemas.openxmlformats.org/wordprocessingml/2006/main%22%22>
<w:body>
<w:p>
<w:r>
<w:t></w:t>
</w:r>
</w:p>
</w:body>
</w:document>

Há outros elementos (partes) que podem ser agregados a um documento. Para o fim que se destina este post, não vamos tratar dos outros elementos do docuemnto. A documentação do SDK é completa o sufuciente para que os demais aspectos do desenvolvimento com OpenXml sejam aprendidos e utilizados corretamente.

A primeira coisa a ser feita, é a criação do documento OpenXml. Isso é feito a partir do método CreateNewDocument ilustrado abaixo. Esta função adiciona a Parte principal de um documento, isto é, a parte que vai receber o conteúdo do documento. Em seguida, o Schema básico é aplicado à esta parte para que o documento possa ser manipulado (com o uso do DOM ou mesmo XPath:

public static void CreateNewWordDocument(string document)
{
using (WordprocessingDocument wordDoc = WordprocessingDocument.Create(document, WordprocessingDocumentType.Document))
{

MainDocumentPart mainPart = wordDoc.AddMainDocumentPart();
SetMainDocumentContent(mainPart);
}
}

O método SetMainDocumentContent basicamente aplica o conteúdo padrão com base no schema ilustrado acima.

public static void SetMainDocumentContent(MainDocumentPart MainPart)
{
const string docXml =
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<w:document xmlns:w=""
http://schemas.openxmlformats.org/wordprocessingml/2006/main"">
<w:body>
<w:p>
<w:r>
<w:t></w:t>
</w:r>
</w:p>
</w:body>
</w:document>";
using (Stream stream = MainPart.GetStream())
{
byte[] buf = (new UTF8Encoding()).GetBytes(docXml);
stream.Write(buf, 0, buf.Length);
}
}

Utilizando esses conceitos, nossa exportação da lista de preços pode ser codificada como segue:

/// <summary>
/// Exporta a lista exemplo para dentro de um documento OpenXml (Word 2007)
/// </summary>
/// <param name="pExampleList"></param>
/// <param name="pPath"></param>
public void ExportListToWord(OpenXMLPOC.Lib.Data.Example pExampleList, string pPath)
{
using (WordprocessingDocument wdDoc = WordprocessingDocument.Create(pPath, WordprocessingDocumentType.Document))
{
MainDocumentPart mainPart = wdDoc.AddMainDocumentPart(); //adiciona o MainDocumentPart
SetMainDocumentContent(mainPart); // Inicializa a parte principal do documento com o schema padrão (1 parágrafo em branco).

XmlDocument bodyXml = new XmlDocument();
bodyXml.LoadXml(wdDoc.MainDocumentPart.Document.Body.OuterXml);
XmlNode fistText = bodyXml.GetElementsByTagName("w:t")[0];
fistText.InnerText = “TABELA DE PREÇOS”;
// Grava cada item da lista num novo parágrafo
foreach (Data.DataExample de in pExampleList)
{
//cria um novo conjunto <w:p><w:r><w:t></w:t></w:r></w:p> sendo que o conteúdo da tag w:t será a versão em string do objeto de dados <DataExample>.
XmlNode NewParagraph = CreateNewParagraph(bodyXml, de.ToString());
//Adiciona o parágrafo no corpo do documento.
bodyXml.DocumentElement.AppendChild(NewParagraph);
}
// garante que o xml para o MainDocumentPart conterá todas as informações agregadas
wdDoc.MainDocumentPart.Document.Body.InnerXml = bodyXml.InnerXml;
//Salva o Documento (word/document.xml) dentro da Package (pacote)
wdDoc.MainDocumentPart.Document.Save();
//fecha o arquivo.
wdDoc.Close();
}
}

Notem que toda a manipulação do conteúdo é feito com base nos objetos padrão de manipulação de XML. Isso é especialmente interessante para este cenário onde o desenvolvedor tem a total liberdade de aplicar como melhor lhe for conveniente para integrar dados estruturados ao conteúdo que pode ser manipulado até mesmo por usuárois que não têm acesso às consultas em sistemas específicos.

Um teste de utilização desta solução pode ser feito com o seguinte trecho de código:

class Program
{
static void Main(string[] args)
{
Example ex = new Example();
ex.Add(CreateDataExample(1, "teste1",(decimal)4.89));
ex.Add(CreateDataExample(2, "teste2", (decimal)5.89));
ex.Add(CreateDataExample(3, "teste3", (decimal)6.89));
ex.Add(CreateDataExample(4, "teste4", (decimal)7.89));
ex.Add(CreateDataExample(5, "teste5", (decimal)8.89));

ex.Add(CreateDataExample(10, "FINAL", (decimal)9.89));

DocManager dcMngr = DocManager.GetInstance();
dcMngr.ExportListToWord(ex, @"C:\Users\mauro.zamaro\Documents\export2.docx");
Console.Write("Ready");
Console.ReadKey();

}

static DataExample CreateDataExample(int code, string Description, decimal value)
{
DataExample de = new DataExample();
de.Code = code;
de.Description = Description;
de.Price = value;
return de;
}
}

Ao executar o teste, o arquivo gerado quando submetido à ferramenta DocumentReflector (disponível com o OpenXML SDK CTP September 2008) gera o seguinte código automático:

using DocumentFormat.OpenXml.Packaging;

using DocumentFormat.OpenXml.Wordprocessing;

using DocumentFormat.OpenXml;

namespace GeneratedCode

{

public class GeneratedClass

{

public void CreatePackage(string filePath)

{

using(WordprocessingDocument package = WordprocessingDocument.Create(filePath, WordprocessingDocumentType.Document))

{

AddParts(package);

}

}

private static void AddParts(WordprocessingDocument parent)

{

var mainDocumentPart1 = parent.AddMainDocumentPart();

GenerateMainDocumentPart1().Save(mainDocumentPart1);

}

private static Document GenerateMainDocumentPart1()

{

var element =

new Document(

new Body(

OpenXmlUnknownElement.CreateOpenXmlUnknownElement("<w:body xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"
><w:p><w:r><w:t>TABELA DE PREÇOS</w:t></w:r></w:p><w:p><w:r><w:t>Code : 1 | Description : teste1 | Price : 4,89 | </w:t></w:r></w:p><w:p><w:r><w:t>Code : 2 | Description : teste2 | Price : 5,89 | </w:t></w:r></w:p><w:p><w:r><w:t>Code : 3 | Description : teste3 | Price : 6,89 | </w:t></w:r></w:p><w:p><w:r><w:t>Code : 4 | Description : teste4 | Price : 7,89 | </w:t></w:r></w:p><w:p><w:r><w:t>Code : 5 | Description : teste5 | Price : 8,89 | </w:t></w:r></w:p><w:p><w:r><w:t>Code : 10 | Description : FINAL | Price : 9,89 | </w:t></w:r></w:p></w:body>")));

return element;

}

}

}

O conteúdo de word/document.xml gerado é o que segue:

<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main%22>
<w:body>
<w:body>
<w:p>
<w:r>
<w:t>TABELA DE PREÇOS</w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t>Code : 1 | Description : teste1 | Price : 4,89 | </w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t>Code : 2 | Description : teste2 | Price : 5,89 | </w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t>Code : 3 | Description : teste3 | Price : 6,89 | </w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t>Code : 4 | Description : teste4 | Price : 7,89 | </w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t>Code : 5 | Description : teste5 | Price : 8,89 | </w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t>Code : 10 | Description : FINAL | Price : 9,89 | </w:t>
</w:r>
</w:p>
</w:body>
</w:body>
</w:document>

image

Ao abrirmos o documento com o Microsoft © Office Word 2007, temos o seguinte resultado:

image

Conclusão

O uso do OpenXML é especialmente interessante se há necessidade de negócios em democratizar e/ou distribuir informações confiáveis e que sejam acessadas em qualquer ferramenta / plataforma com custo relativamente baixo e alto valor agregado.

O código fonte pode ser obtido no meu blog http://maurozamaro.spaces.live.com ou por solicitação de e-mail para maurozamaro@hotmail.com

Sobre o autor:

Mauro Zamaro foi apresentado à computação antes mesmo de saber qual era a diferença entre uma barata e uma centopéia. Atua hoje como Arquiteto de soluções na Programmer"s Informática, Gold Partner Microsoft, situada em Campinas/SP.

É certificado em VB.NET (Windows application) desde 2003. É certificado também em C#2.0 MCPD - Windows application e MCTS - Web Applications desde 2008.

Tem se dedicado ultimamente ao assunto “Application Lifecycle Management” no blog “Technicians in a square-ball’s world” (http://maurozamaro.spaces.live.com)

Mauro Zamaro

Mauro Zamaro - Mauro Zamaro foi apresentado à computação antes mesmo de saber qual era a diferença entre uma barata e uma centopéia. Atua hoje como Arquiteto de soluções na Programmer's Informática, Gold Partner Microsoft, situada em Campinas/SP.

É certificado em VB.NET (Windows application) desde 2003. É certificado também em C#2.0 MCPD - Windows application e MCTS - Web Applications desde 2008.

Tem se dedicado ultimamente ao assunto "Application Lifecycle Management" no blog "Technicians in a square-ball's world" (
http://maurozamaro.spaces.live.com).