Il blog di Gianni Giaccaglini

Blog su VBA e VSTO
Gianni Giaccaglini

My Links

News

NB - V. anche gli ARTICOLI (in fondo a questa barra)
Solo quesiti validi a: giannigiac@tin.it
Il mio Best seller su VBA
(v. www.hoepli.it)


Il mio ultimo libro su Open XML
(v. www.FAG.it):



La mia nipotina ELISA

Foto con dedica a ME di
Bill Gates giovanissimo
nei mitici anni 80!

Categorie Post

Categorie Articoli

Archivio

Immagini

Blog Stats

Trasferire tabelle Word 2007 in un foglio Excel (introduzione a LINQ per XML applicato a Word)

Caricare una tabella Word 2007 in Excel 2007, con VSTO + LINQ

NB - Il seguito di questo articolo è nel seguente:

http://blog.shareoffice.it/giannigiaccaglini/articles/10080.aspx

Antipasti su LINQ per XML, fruiti nei VSTO

I VSTO (Visual Studio Tools per Office System) si applicano a vari tipi di documento Office, sfruttando tutte le potenzialità di Visual Studio, molte delle quali precluse in ambiente VBA (Visual Basic Application Edition), con le pur popolare macro, insomma. Con la versione 2008 di Visual Studio è stato varato un nuovo, sofisticato paradigma elaborativo, il LINQ (Language INtegrated Query), comprendente diversi “motori” che permettono accesso a ciascuna delle più comuni basi di dati, con una sintassi mutuata da quella del classico SQL. Esempio fugace:

Dim Query = _

  From Clt in Clienti

  Where Clt.Country = "Italy"

   Select Clt

Come è, perlomeno, intuitive tale codice attinge a una tabella Clienti, filtrando quelli la cui nazione è l’Italia. LINQ è unh linguaggio di tipo dichiarativo (definisce i risultati da ottenere), in contrapposizione alle normali istruzioni, di tipo imperativo, che definiscono passo dopo passo cosa si deve fare (così, detto di passaggio, LINQ ambisce a sostituire la tradizionale libreria ADO, imperativa).

Va da sé che tanto ben di dio, fruibile pure nei VSTO, dubito che in futuro possa essere supportata anche in VBA. Di fatto al momento è giocoforza accontentarsi delle DLL relative ad ADO o al DOM (Document Object Model) per gestire i sempre più attuali file XML.

E qui vengo al punto: fornirò un semplice ma significativo esempio di utilizzo del quarto motore (standard) di LINQ, il LINQ To XML. Prima di proseguire comunico che ahimè darò per noti molti concetti e novità sia generali (del Framework 3.0, per intendersi) che relative a LINQ, con un ovvio invito a quanti ancora non sanno nulla di LINQ: procuratevi un buon testo specifico, ad esempio quello di Alessandro Del Sol già recensito in questo blog):

Microsoft LINQ in Visual Basic , ed. FAG

( http://www.fag.it/scheda.aspx?ID=28759 )

Per approfondire l’argomento segnalo poi la seguente Bibbia specifica. di due autori italiani Paolo Pialorsi e Marco Russo (che l’hanno pubblicata anche in inglese, per Microsoft Press):

Programmare Microsoft LINQ, Mondadori Informatica

( http://education.mondadori.it/Libri/SchedaLibro.asp?IdLibro=88-6114-160-9 )

Infine fornisco il link della mia recensione al testo LINQ in Action dell’americana Manning Publications:

http://blog.shareoffice.it/giannigiaccaglini/articles/9845.aspx

Purtroppo pure le nozioni base relative al formato XML in genere e in particolare a quello aperto di Office 2007 – OOXML (Office Open XML Format) – sono un ineludibile prerequisito! Al riguardo cito di nuovo il manualino del qui presente Gianni Giaccaglini: Open XML guida alla sviluppo, ed. FAG. I (numerosi) esempi anche applicativi per VBA e VSTO sono in DOM, non in LINQ (che quando scrissi era in fasce e le nozioni di fatto possedute quasi solo da Microsoft MVP).

Perché non riciclarli in LINQ? Ci sto lavorando, attualmente con scarso tempo disponibile. Questo è un primo esempio.

Il nostro documento Word 2007 con tabella da portare in un foglio Excel

Veniamo al sodo proponendo un sem’plicissimo documento Word 2007, avente il semplice contenuto seguente (chiedendo venia per lo squallido umorismo delle etichette):

Tabella

Abba

Rabà

ciccì

10

20

30

23,40

123,40

12,50

1.200

1.234

1.234,50

 

Il proposito espresso dal chilometrico titolo parla chiaro e, in pratica, significa realizzare codice Visual Basic 2008 in salsa VSTO 2008 relativo a una cartella di lavoro Excel 2007, atto a trasferire la predetta tabella nella cella corrente del suo Foglio1.

Nota – Per estrema semplicità assoceremo la specifica routine all’evento Click di un oggetto Button (equivalente a un CommandButton1 del VBA).

La main part Document.xml

Come è noto, il file .docx è in realtà un archivio ZIP, composto di cartelle e di varie parti, per lo più di tipo XML. In particolare la parte principale (main part) è un Document.xml, posta in una directory interna Word . La main part può essere estratta in vari modi, da ultimo mediante la vers. 2.0 di un apposito SDK. Qui va detto che converrebbe utilizzare la precedente versione 1.0 (la dll corrispondente si chiama OpenXml) (***) anzi per estrema semplicità che tale Document.xml venga estratto a manina o meglio con il codice indicato in un altro post (***), in una directory ad hoc del nostro PC, sia essa \PartiXML. Su questa agiremo, però sfruttando lo specifico motore LINQ ossia LINQ to XML.

Ma ecco come si presenta, il sospirato Document.xml, brutalmente copiato con Ctrl+C:

<w:document xmlns:ve="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml">

- <w:body>

- <w:p w:rsidR="00EF0E44" w:rsidRDefault="002413A7">

- <w:r>

    <w:t>Tabella:t>

  :r>

  :p>

- <w:tbl>

- <w:tblPr>

  <w:tblStyle w:val="Grigliatabella" />

  <w:tblW w:w="0" w:type="auto" />

  <w:tblLook w:val="04A0" />

  :tblPr>

- <w:tblGrid>

  <w:gridCol w:w="2444" />

  <w:gridCol w:w="2444" />

  <w:gridCol w:w="2445" />

  :tblGrid>

- <w:tr w:rsidR="002413A7" w:rsidTr="002413A7">

-   <w:tc>

-     <w:tcPr>

        <w:tcW w:w="2444" w:type="dxa" />

      :tcPr>

-    <w:p w:rsidR="002413A7" w:rsidRDefault="002413A7">

        <w:proofErr w:type="spellStart" />

-       <w:r>

           <w:t>abba:t>

        :r>

  <w:proofErr w:type="spellEnd" />

  :p>

  :tc>

- <w:tc>

- <w:tcPr>

  <w:tcW w:w="2444" w:type="dxa" />

  :tcPr>

- <w:p w:rsidR="002413A7" w:rsidRDefault="00DC326A">

  <w:proofErr w:type="spellStart" />

- <w:r>

  <w:t>R:t>

  :r>

- <w:r w:rsidR="002413A7">

  <w:t>abà:t>

  :r>

  <w:proofErr w:type="spellEnd" />

  :p>

  :tc>

- <w:tc>

- <w:tcPr>

  <w:tcW w:w="2445" w:type="dxa" />

  :tcPr>

- <w:p w:rsidR="002413A7" w:rsidRDefault="002413A7">

  <w:proofErr w:type="spellStart" />

- <w:r>

  <w:t>ciccì:t>

:r>

.  O M I S S I S

- <w:r>

. . . O M I S S I S . . .

  <w:t>1:t>

  :r>

- <w:r w:rsidR="00DC326A">

  <w:t>0:t>

  :r>

  :p>

  :tc>

- <w:tc>

 . . . O M I S S I S . . .

. . . O M I S S I S

  :tbl>

  <w:p w:rsidR="002413A7" w:rsidRDefault="002413A7" />

- <w:sectPr w:rsidR="002413A7" w:rsidSect="00EF0E44">

  <w:pgSz w:w="11906" w:h="16838" />

  <w:pgMar w:top="1417" w:right="1134" w:bottom="1134" w:left="1134" w:header="708" w:footer="708" w:gutter="0" />

  <w:cols w:space="708" />

  <w:docGrid w:linePitch="360" />

  :sectPr>

  :body>

  :document>

Il precedente guazzabuglio è mal indentato e con molti omissis che possono aver “mangiato” qualche tag di chiusura. Comunque, con un po’ di pazienza, si deducono i nodi salienti che ci interessano. Per cominciare, il nodo radice in alto contiene, come attributi xmlns , diverse dichiarazioni di namespace, tra cui ripeto la seguente:

xmlns:w=http://schemas.openxmlformats.org/wordprocessingml/2006/main

che, tra l’altro, definisce come w il prefisso di tale spazio nomi, che si ritrova sistematicamente in tutti i tag: <w:document>, <w:body> ecc.

Nel nostro esempio ci interessano i contenuti, pertanto in ultima analisi, tralasciando i tag diciamo così formali come e seguenti palesemente relativi alla griglia (Grid) della tabella, procedendo di padre in figlio abbiamo (Il corpo del documento>, e sotto l’albero iniziale tra e relativo all’unico paragrafo contenente “tabella”, seguono che (sempre ignorando i nodi formali) racchiude dei comprendenti a loro volta diversi . Si tratta delle righe e delle celle di ogni riga della nostra tabellina. Ogni cella poi contiene nodi con dei “run” che infine racchiudono nodi di testo . Tale sequela è identica a quella del paragrafo normale, però i nodi sono interni a .

Tale situazione è chiara e “regolare”, però se si va più avanti si noteranno testi spezzati in due “run” come nel caso seguente:

- <w:r>

. . . O M I S S I S . . .

    <w:t>1w:t>

  w:r>

- <w:r w:rsidR="00DC326A">

    <w:t>0w:t>

  w:r>

Mentre il testo originario era 10! La cosa è giustificata quando una stessa parola è, putacaso, “mammamia” ossia con una parte in grassetto, la sottile distinzione tra run e t serve per questo. Purtroppo Word, per misteriosi motivi, a volte compie spezzettamenti arbitrari, nel nostro piccolo documento c’è pure “rabà” suddiviso dio sa perché in “r” e “abà”!

Nota E questo non solo nelle tabelle. Personalmente credo sia un bug. Peraltro diversi blogger esperti di LINQ ignorano la cosa, per cui di fatto sono errati certi loro snippet che ricostruisono testi Word supponendo che siano sempre “separati”.

Tutto ciò, come si comprende, crea terrificanti complicanze se si vogliono ricostruzioni esatte.... Per fortuna il rimedio, per chi ha a cuore solo i contenuti c’è, ed è esprimibile con la seguente, basilare

Regola Il testo associato a nodi di livello più alto è la concatenazione dei contenuti di tutti i loro figli, nipoti ecc.

Ricordo che in DOM è la proprietà Text di un nodo IXMLDOMNode che compie tali “giunzioni”, cui in LINQ corrisponde la proprietà Value di un XElement (anche di ciò trova scarse tracce negli snippet testé citati).

Qui però mi fermo coi richiami. Questo non è un trattato! e d’ora in poi darò per note le nozioni su LINQ e farò solo considerazioni essenziali, specie nei punti un po’ oscuri e/o mal documentati.

Chiarimento di un dubbio atroce

È tempo di entrare in medias res, con un mio primo tentativo. Per brevità ne riporto solo l’incipit, tanto alla fine il codice, opportunamente sanato, sarà lo stesso:

Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click

  Dim MiaDir As String = _

  "C:\LINQ\Tabella\Parti XML\"

  Dim Doc As XDocument = _

  XDocument.Load(MiaDir & "Word\Document.xml")

  Dim Tabella As XElement = Doc.Descendants("w:tbl").First

  Dim Nr As Integer = Tabella...<w:tr>.Count - 1

  'Numero colonne, dedotto dalla PRIMA riga di tabella-run

  Dim Nc As Integer = Tabella.Element("w:tr").<w:tc>.Count - 1

Nella mia ingenuità (e ignoranza) ritenevo che i nomi dei nodi con tanto di prefisso w: fossero accettati, però e erano rigettati dal compilatore (che evidenziava come anomalia il w), mentre sintassi come Descendants("w:tbl) o Element(“w:tr”) davano errore a run-time.

Che fare? Non trovando esempi analoghi nel pur ottimo testo di Del Sole che stavo usando e preoccupato di verificare comunque il mio algoritmo sono ricorso a un trucco sudicio assai, sostituendo i vari w:tbl, w:tr ecc. con tbl, tr ecc.:

Così tutto era accettato e funzionava a dovere.

  Dim Tabella As XElement = Doc.Descendants("tbl").First

Eccetera

Successivamente sono riuscito a trovare la soluzione, grazie anche alla delucidazione di Paolo Pialorsi, che qui pubblicamente rigrazio:

Per lavorare coi namespace in LINQ to XML occorre usare gli XName. Ecco come creare un nodo qualificato con il namespace:

Ns As XNamespace = "http://schemas.devleap.com/Customer"

Dim customer As XElement = new XElement(ns + "customer", _

   new XAttribute("id", "C01"),_

   new XElement(ns + "firstName", "Paolo"), _

  new XElement(ns + "lastName", "Pialorsi"))

 

customer.Save(Console.Out)

Console.ReadLine()

Personalmente trovo un po’ strana questa particolarità, in quanto il concatenamento di un XNamespace con una stringa produce il giusto risultato in virtù di una precisa, particolare convenzione di LINQ to XML Sia come sia, invito a prenderne accurata nota i principianti in LINQ che provengono da DOM, ove stringhe dotate di prefisso sono tranquillamente accettate.

Nota Un ultimo mistero resta in piedi: come la mettiamo coi vari “assi” o ? Ovvio che qui non ci sono concatenamenti con namespace che tengano... Che addirittura ne sia, semplicemente, vietato l’uso?

Il codice VB 9, finalmente

Ecco dunque il listato VSTO + LINQ, presente nel Foglio1 di una cartella di lavoro Excel:

Imports System.Xml.Linq

Public Class Foglio1

'. . . omissis . . .

Private Function FormatoInglese(ByVal strDatoIngl As String) As String

  If IsNumeric(strDatoIngl) Then

    strDatoIngl = Replace(strDatoIngl, ".", "*")

    strDatoIngl = Replace(strDatoIngl, ",", ".")

    strDatoIngl = Replace(strDatoIngl, "*", ",")

  End If

  Return strDatoIngl

End Function

 

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

  Dim MiaDir As String = _

  "C:\LINQ\Tabella\Parti XML\"

  Dim Doc As XDocument = _

  XDocument.Load(MiaDir & "Word\Document.xml")

  Dim Ns_w As XNamespace = _

  "http://schemas.openxmlformats.org/wordprocessingml/2006/main"

  Dim Tabella As XElement = _

  Doc.Descendants(Ns_w + "tbl").First() 'NON Da'ERRORE a run-time! _

  e .First occorre anche in presenza di una sola tabella

  'Numero righe

  Dim Nr As Integer = Tabella.Descendants(Ns_w + "tr").Count - 1 'OK!!!

  'Numero colonne, dedotto dalla PRIMA riga di run

  Dim Nc As Integer = _

  Tabella.Element(Ns_w + "tr").Descendants(Ns_w + "tc").Count – 1

  Dim Matr(0, 0) As String

  ReDim Matr(Nr, Nc)

  Dim i As Integer = 0

  Dim j As Integer = 0

  'Caricamento valori nella Matr

  For Each Riga In Tabella.Elements(Ns_w + "tr")

    For Each Cella In Riga.Elements(Ns_w + "tc")

      Matr(i, j) = Col.Value

      Matr(i, j) = FormatoInglese(Matr(i, j))

      j = j + 1

    Next Cella

    i = i + 1

    j = 0

  Next Riga

  Copia Matr nell'intervallo "attorno" a cella attiva

  With Application.ActiveCell

    .CurrentRegion.ClearContents()

    For i = 0 To Nr

      For j = 0 To Nc

       .Cells(i + 1, j + 1) = Matr(i, j)

      Next

    Next

  End With

End Sub

End Class

Commenti stringatissimi. La Function iniziale traduce una stringa numerica invertendone punti e virgole, operazione necessaria in quanto la copia della matrice ricavata dalla tabella Word deposita nelle celle Excel formati inglesi. Segue il caricamento in Doc del Document.xml supposto giacente nella cartella ...\PartiXML, sottocartella Word. Quindi l’acquisizione del sospirato namespace in Ns_w permette la corretta sintassi per i vari oggetti Descendants, Element ed Elements. Ottenuta la tabella nell’omonima variabile Tabella di tipo XElement (il tipo base di LINQ To XML) se ne ricavanocon due loop annidati, i nodi-Riga e, per ciascuno di questi, i rispettivi nodi Cella, inseriti sistematicamente nella matrice Matr di dimensioni Nr e Nc pari a quelle della tabella.

Un’ultima osservazione spicciola Gli assi definiti con un punto (come in MioXElem.) o tre punti (come in MioXElem... oltre, probabilmente, non esser leciti in presenza di prefissi, coprono solo le sintassi Elements e Descendants che danno elementi multipli, ma sono orfani di Element che dà il singolo nodo.  Per questo un’istruzione del tipo seguente, ammesso che siano lecite sintassi tipo non può fare a meno di Element:

Dim Nc As Integer = _

Tabella.Element(Ns_w + "tr").<w:tc>.Count – 1

L’analisi del successivo ciclo che copia Matr nella cella attiva del foglio è lasciata per esercizio.

Nota In VBA questa operazione si può esprimere sinteticamente con MiaZona = Matr, con MiaZona delle dimensioni Nr ed Nc, cosa che i VSTO non accettano...

Imperativo sarà lei? Perché no?

Chi mastica l’essenziale del nuovo verbo LINQ si stupirà per non aver trovato, dopo il Load del Document.xml, una dichiarazione del tipo seguente:

Dim Tabella As XElement = _

  From Tab As XElement In Doc.Descendants(Ns_w + "tbl”) _

  Select Tab

 

Il fatto è che, a differenza (credo), degli altri mondi LINQ, il motore LINQ To XML ha per così dire una duplice anima, dichiarativa e imperativa, cui il manuale Link In Action dedica esplicito capitolo. In altri termini supporta istruzioni che manipolano gli assi tipici della struttura arborescente dell’XML. Ne ho dedotto che quando, come nel nostro caso, è assente una clausola filtrante Where è più semplice e corto adottare l’equivalente imperativo, che per comodità del lettore ripeto qui sotto:

Dim Tabella As XElement = _

  Doc.Descendants(Ns_w + "tbl").First()

Trovo infine interessante far notare che, in stile imperativo, LINQ To XML supporta anche iterazioni For Each come quelli da me adottati, anche se gli XElement non sono ti tipo Enumerable ma di tipo IEnumerable, cosa che implica che i documenti XML non sono caricati per intero nella memoria interna, come avviene in DOM. Con risparmio di RAM e un certo rallentamento negli accessi. Altra contropartita, gli XElement non sono indicizzabili.

Variante semplificata! (ci avete pensato?)

La precedente routine utilizza una matrice come deposito intermedio, ma a ben pensarci se ne potrebbe fare a meno caricando direttamente le celle della tabella Word su quelle “attorno” alla cella attiva del foglio Excel. Senza ripudiare il procedimento di cui sopra, che può servire quando una matrice conviene (da sola o per successive elaborazioni) ecco allora la variante semplificata:

Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click

        Dim MiaDir As String = _

        "C:\LINQ\Tabella\Parti XML\"

        Dim Doc As XDocument = XDocument.Load(MiaDir & "Word\Document.xml")

        Dim Ns_w As XNamespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"

        Dim Tabella As XElement = _

        Doc.Descendants(Ns_w + "tbl").First()

        'CARICAMENTO DIKRETTO SUL FOGLIO

        Dim CellaAttiva As Excel.Range = Application.ActiveCell

        Dim Righe = Tabella.Elements(Ns_w + "tr")

       Dim Riga As XElement

        i = 1 : j = 1

        For Each Riga In Righe 'Tabella.Elements(Ns_w + "tr")

            For Each CellaTab In Riga.Elements(Ns_w + "tc")

                CellaAttiva.Cells(i, j) = FormatoInglese(CellaTab.Value)

                j = j + 1

            Next CellaTab

            i = i + 1

            j = 1

        Next Riga

    End Sub

 

Nessun commento, salvo precisare che una definizione completa del tipo della variabile Righe richiederebbe a rigore la lunghissima istruzione seguente:

Dim Righe As System.Collections.Generic.IEnumerable(Of System.Xml.Linq.XElement) = _

Tabella.Elements(Ns_w + "tr") 

 

Ne approfitto per citare una novità interessante di Visual Studio 2008, il riconoscimento automatico del tipo (che viene riconosciuto dal contesto). Vale solo per le variabili locali (cioè per la maggior parte dei casi pratici) e permette di evitare lungaggini, così come abbiamo fatto in entrambi gli esempi.

Altri possibili sviluppi

Sempre relativi al problema qui delineato. Il primo si rivolge ai vari guru LINQ, invitati a escogitare una variante, magari più “dichiarativa” con opportuna query atta a ricavare la matrice oppure oggetti direttamente trasferibili in un controllo griglia.

Ancora più importante sarebbe l’estrazione programmatica della/e parte/i di un documento OOXML, prima di caricarlo. I più esperti se la cavano con il supporto per OPC (formato Open Packaging Convention), nativo in Visual Studio, che consente tali operazioni su qualsiasi file ZIP. Meglio ancora, si può ricorrere a una SDK fatta apposta per OOXML, Open XML Formats SDK.. Ne fornisco i riferimenti per scaricarla (dal sito openxmldeveloper.org) e leggerne documentazione e snippet:

http://openxmldeveloper.org/archive/2008/06/11/3342.aspx

Link (principali) letti da tale pagina:

OSSEVAZIONE IMPORTANTE per chi volesse proseguire con la libreria Open XML Format: la si deve referenziare ESATTAMENTE nel modo seguente:

Imports DocumentFormat.OpenXml.Packaging

(nella prima versione occorreva anteporre Microsoft.Office. per cui un mio precedente lavoretto ora non funzionava più con la dizione "lunga"!)

Nota – Si ricorda poi che il download della libreria SDK 1.0 non basta. Occorre anche attivare, nel singolo progetto, il comando Progetto > Aggiungi riferimento..., eventualmente con clic su scheda Sfoglia ecc., altrimenti la direttiva Imports DocumentFormat.OpennXML.Packaging non viene accettata.

 

Comunque io per ora mi fermo qui, ritenendo di aver raggiunto lo scopo di fornire nozioni introduttive. Forse ne parlerò in seguito... Pigrizia o, magari, ignoranza? Liberi di malignare, tuttavia faccio infine notare che è ormai disponibile una versione 2 di tale SDA, basata su oggetti “fortemente tipizzati”. Insomma una sintassi più potente e chiara. Dunque conviene – almeno per OOXML – studiarsi la nuova bestiolina prima di procedere.

NB - Il seguito di questo articolo è nel seguente:

http://blog.shareoffice.it/giannigiaccaglini/articles/10080.aspx

?>

?>

?>

?>

posted on venerdì 6 marzo 2009 15.44