Generování Word dokumentu pomoci Open XML SDK, část 2

Jan Holan       20. 2. 2012       Office       6550 zobrazení

Minule jsme se seznámili s Microsoft Office Open XML SDK 2.0 a vytvořili jednoduchý Word dokument. Asi nejběžnější postup na vytváření dokumentu v aplikacích ale většinou není ten, že bychom vytvářeli kompletně celý dokument, ale použijeme již připravený dokument jako šablonu, a dokument vytvoříme pomoci ní.

Nejprve si takovou šablonu připravíme. Vyjdeme z obyčejného Word dokumentu, kde si vyznačíme potřebná místa, do kterých budeme v kódu pomoci Open XML SDK knihovny vkládat konkrétní obsah. K tomu použijeme Word Content Controls. Postup je následující:

  • Nejprve musíme ve Wordu zapnou nástroje pro vývojáře, takže File/Options, Customize Ribbon a zaškrtneme Developer.
  • Na této záložce máme možnost zapnout zobrazení Design Mode.
  • Do dokumentu na jednotlivá místa vložíme Plain Text Content Control.
  • Stiskneme Properties a v dialogu zadáme název do pole Title a Tag, ten později využijeme v našem kódu.
  • Výsledný dokument uložíme buď jako standardní docx dokument nebo přímo jako šablonu dotx.

Nyní již můžeme vytvořit základní kód pro generování našeho dokumentu. Bude to metoda Generate, která na vstupu dostane pole bajtů šablony dokumentu (načtené ze souboru, z resource nebo databáze), výstupem bude pole bajtů výsledného dokumentu.

public byte[] Generate(byte[] sablona)
{
    byte[] output;
    using (var stream = new MemoryStream(sablona.Length))
    {
        stream.Write(sablona, 0, sablona.Length);
        stream.Position = 0;

        using (var wordDocument = WordprocessingDocument.Open(stream, true))
        {
            wordDocument.ChangeDocumentType(DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
            var mainPart = wordDocument.MainDocumentPart;

            //Prepare list of document root parts
            var rootParts = new List<OpenXmlPartRootElement>();
            foreach (var headerPart in mainPart.HeaderParts)
            {
                rootParts.Add(headerPart.Header);
            }
            rootParts.Add(mainPart.Document);
            foreach (var footerPart in mainPart.FooterParts)
            {
                rootParts.Add(footerPart.Footer);
            }

            foreach (var rootPart in rootParts)
            {
                //Find all SdtElement with alias using linq query
                foreach (var element in rootPart.Descendants<SdtElement>()
                                            .Where(s => s.SdtProperties.GetFirstChild<SdtAlias>() != null).ToList())
                {
                    string name = element.SdtProperties.GetFirstChild<SdtAlias>().Val.Value;

                    //Insert text and remove ContentControl (keep formating and other elements in ContentControl)
                    ReplaceTextContentControl(element, GetDocumentData(name));
                }
            }
        }

        output = stream.ToArray();
    }

    return output;
}

Po načtení šablony a vytvoření dokumentu se nejprve sestaví list s kořenovými částmi dokumentu, jsou to hlavičky, hlavní část dokumentu a zápatí dokumentu (typ OpenXmlPartRootElement). Pomoci LINQ dotazu se pak pro každou kořenovou část dokumentu hledají všechny objekty SdtElement, které obsahují element SdtAlias – to jsou naše připravené Content Controls v šabloně.

XML reprezentace Content controlu v dokumentu vypadá např. takto:

<w:sdt>
  <w:sdtPr>
    <w:alias w:val="Firma"/>
    <w:tag w:val="Firma"/>
    <w:id w:val="-1564864779"/>
    <w:placeholder>
      <w:docPart w:val="BDBEA95FA2A24406B69F1F5D6AC16989"/>
    </w:placeholder>
    <w:text/>
  </w:sdtPr>
  <w:sdtContent>
    <w:r>
      <w:rPr>
        <w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/>
        <w:b/>
        <w:szCs w:val="22"/>
      </w:rPr>
      <w:t>Firma</w:t>
    </w:r>
  </w:sdtContent>
</w:sdt>

Pokud jsme při vytváření Controlu zadali pole Title, je jeho hodnota uložená v elementu w:alias, kterému odpovídá objekt SdtAlias (v elementu SdtProperties), podobně hodnota pole Tag je uložena v elementu w:tag (objekt Tag). My v našem kódu jako jméno controlu použijeme hodnotu Title z objektu SdtAlias.

Podle jména controlu voláním GetDocumentData načteme požadovaný text, který budeme vkládat do dokumentu. Ten je pak předán do pomocné metody ReplaceTextContentControl, která má za úkol content control nahradit textem, ale přitom zachovat ostatní elementy, které mohou obsahovat formátování odstavce, textu apod. Implementace metody vypadá takto:

private static OpenXmlElement ReplaceTextContentControl(OpenXmlElement element, string value, bool removeIfEmpty = false)
{
    OpenXmlCompositeElement content = null;
    if (element is SdtBlock)
    {
        content = element.GetFirstChild<SdtContentBlock>();
    }
    if (element is SdtRun)
    {
        content = element.GetFirstChild<SdtContentRun>();
    }

    var refElement = element;
    if (content != null)
    {
        ReplaceTextInContent(content, value, removeIfEmpty);

        //Copy content
        foreach (var item in content.ChildElements)
        {
            var newElement = (OpenXmlElement)item.Clone();
            refElement = refElement.InsertAfterSelf(newElement);
        }

        if (refElement == element)
        {
            refElement = element.PreviousSibling();
        }
        element.Remove();
    }

    return refElement;
}

private static void ReplaceTextInContent(OpenXmlCompositeElement content, string value, bool removeIfEmpty)
{
    if (removeIfEmpty && string.IsNullOrEmpty(value))
    {
        content.RemoveAllChildren();
        return;
    }

    if (value == null)
    {
        value = "";
    }

    bool first = true;
    foreach (var paragraph in content.Descendants())
    {
        if (first)
        {
            //Keep only first Paragraph
            bool firstRun = true;
            foreach (var run in content.Descendants())
            {
                //Keep only first Run
                if (!firstRun)
                {
                    run.Remove();
                }
                firstRun = false;
            }

            //Create text paragraphs
            OpenXmlElement paragraphRefElement = paragraph;
            value = value.Replace("\r\n", "\r").Replace("\n", "\r").Replace("\r", "\r\n");
            foreach (string line in value.Split(new string[] { "\r\n" }, StringSplitOptions.None))
            {
                var newParagraph = (OpenXmlElement)paragraph.Clone();
                paragraphRefElement = paragraphRefElement.InsertAfterSelf(newParagraph);

                var run = newParagraph.Descendants().FirstOrDefault();
                //Replace text in run
                SetTextToRun(run, line);
            }
        }
        first = false;
        paragraph.Remove();
    }

    if (first)
    {
        foreach (var run in content.Descendants())
        {
            //Keep only first Run
            if (first)
            {
                SetTextToRun(run, value);
            }
            else
            {
                run.Remove();
            }
            first = false;
        }
    }
}

private static void SetTextToRun(Run run, string value)
{
    var text = run.Descendants().FirstOrDefault();
    if (text == null)
    {
        return;
    }

    OpenXmlElement refElement = text;
    foreach (string tabs in value.Split(new string[] { "\t" }, StringSplitOptions.None))
    {
        if (text != null)
        {
            text.Text = tabs;
            text = null;
        }
        else
        {
            refElement = refElement.InsertAfterSelf(new Run(new TabChar(), new Text() { Text = tabs }));
        }
    }
}

V metodě se zpracovávají dva typy content controlu - elementy SdtContentBlock nebo SdtContentRun. Z nich se načte obsah, ve kterém je nejprve zaměněn text a poté je celý zkopírován do dokumentu místo elementu content controlu, který je nakonec odstraněn.

Základní použití Open XML SDK máme za sebou. V souvislosti s generováním dokumentu ze šablony bych ještě rád uvedl druhý velice častý praktický případ, a to, že ve vytvořené šabloně budeme mí připravenou jednu stránku dokumentu, ale kódem budeme generovat stránek více (například každou stránku pro jeden záznam vstupních dat).

K tomu použijeme třídu z volné nadstavby knihovny DocumentFormat.OpenXml.dll, která se jmenuje PowerTools for Open XML(aktuálně ve verzi 2.2). Jedná se o sadu PowerShell příkazů (cmdlets), které pro daný úkol volají připravenou C# třídu. Tyto třídy se ale dají bez problému využít i bez PowerShellu přímo voláním jejich public interface. Postup je ten, že buď si po stažení zdrojových souborů celou knihovnu zkompilujeme, nebo použijeme jen ty třídy, které se nám pro daný problém hodí.

My pro generování vícestránkového dokumentu použijeme třídu DocumentBuilder. Pokud nechceme použít celou knihovnu, tak si do projektu z PowerTool přidáme následující soubory: DocumentBuilder.cs, PtOpenXmlDocument.cs, PtOpenXmlUtil.cs a PtUtil.cs. Kód pro generování dokumentu s použitím třídy DocumentBuilder je následující:

public byte[] Generate(byte[] sablona, int pageCount)
{
    var sources = new List<OpenXmlPowerTools.Source>();

    for (int page = 0; page < pageCount; page++)
    {
        using (var stream = new MemoryStream(sablona.Length))
        {
            stream.Write(sablona, 0, sablona.Length);
            stream.Position = 0;

            using (var wordDocument = WordprocessingDocument.Open(stream, true))
            {
                wordDocument.ChangeDocumentType(DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
                var mainPart = wordDocument.MainDocumentPart;

                //Prepare list of document root parts
                var rootParts = new List<OpenXmlPartRootElement>();
                foreach (var headerPart in mainPart.HeaderParts)
                {
                    rootParts.Add(headerPart.Header);
                }
                rootParts.Add(mainPart.Document);
                foreach (var footerPart in mainPart.FooterParts)
                {
                    rootParts.Add(footerPart.Footer);
                }

                foreach (var rootPart in rootParts)
                {
                    //Find all SdtElement with alias using linq query
                    foreach (var element in rootPart.Descendants<SdtElement>()
                                                .Where(s => s.SdtProperties.GetFirstChild<SdtAlias>() != null).ToList())
                    {
                        string name = element.SdtProperties.GetFirstChild<SdtAlias>().Val.Value;

                        //Insert text and remove ContentControl (keep formating and other elements in ContentControl)
                        ReplaceTextContentControl(element, GetDocumentData(name, page));
                    }
                }

                if (page < pageCount - 1)
                {
                    //Add Page break
                    mainPart.Document.Body.AppendChild(new Paragraph(new Run(new Break() { Type = BreakValues.Page })));
                }

                mainPart.Document.Save();
            }

            //Add document as part to source list
            sources.Add(new OpenXmlPowerTools.Source(new WmlDocument(new OpenXmlPowerToolsDocument(stream.ToArray())), false));
        }
    }

    return DocumentBuilder.BuildDocument(sources).DocumentByteArray;
}

Tato metoda Generate provádí postupně generování jednotlivých stránek výsledného dokumentu. Jednotlivé stránky se generují pomoci šablony naprosto shodně jako v prvním příkladu, ale místo vrácení, se dokument obsahující stránku vloží do objektu OpenXmlPowerTools.Source. Na konci metody je ze seznamu všech těchto částí sestaven výsledný dokument pomoci volání DocumentBuilder.BuildDocument.

 

hodnocení článku

1 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback