03. November 2014 · von Steffen Nörtershäuser

Erklärt: Dokumenten Generierung mit dem Open XML SDK

Bei einem aktuellen Projekt im SharePoint Bereich war die Generierung von Dokumenten eine der vielen Anforderungen. Genauer gesagt war es hier nötig, Dokumente mit Platzhaltern zu versehen und diese anschließend aus dem Code heraus mit Werten zu befüllen.

In diesem Artikel möchte ich eine mögliche Lösung für diese Anforderung vorstellen.

OpenXML SDK
Seit Office 2007 hat Microsoft begonnen, das Office Open XML Format als Dateiformat für Dokumente zu nutzen. Dieses Dateiformat beinhaltet mehrere XML-Dateien, welche als ZIP-Archiv gespeichert wird. Eine entpackte „.docx“ sieht beispielsweise wie folgt aus:

docx_content

Für die Arbeit mit OpenXML Dateien hat Microsoft das OpenXML SDK veröffentlicht. Der Vorteil des OpenXML SDK ist, dass kein Microsoft Office auf dem Server installiert sein muss. Im Gegensatz zu älteren Vorgehensweisen, welche auf COM-Objekte von Word setzten, ist man nun frei von Software Anforderungen.

Heruntergeladen werden kann das OpenXML SDK unter folgendem Link: OpenXML SDK.

Im Rahmen dieses Artikels werde ich mich auf die Manipulation von Word Dokumenten konzentrieren – auch, wenn man mithilfe des OpenXML SDK weitere Office Dokumente wie Excel oder Powerpoint bearbeiten kann.

Erzeugen eines Templates für die Verwendung in C#
Bevor ich jedoch darauf eingehe, wie man in C# ein Template bearbeiten kann, will ich vorstellen, wie die Platzhalter für diesen Beitrag in einem Word-Dokument erzeugt werden. Dafür öffnet man in Word ein Dokument, welches mit Platzhaltern versehen werden soll. Anschließend muss man die Eigenschaften des Dokumentes bearbeiten. Hier wählt man unter Properties -> Advanced Properties aus:

docx_properties

Anschließend kann man unter dem Reiter Custom eine neue Eigenschaften einfügen:

docx_create_property

Unter Value wird der eigentliche Wert eingetragen, welcher später im Dokument an der Stelle des Platzhalters angezeigt wird. Der Name ist ein interner Bezeichner.
Nachdem nun diese Eigenschaft angelegt wurde, kann sie als Quick Part in das Word-Dokument eingefügt werden. Hierfür wählt man unter Insert -> Quick Part und anschließend Fields aus.
Im nächsten Fenster muss nun Document Information -> Doc Property ausgewählt werden. Hier befinden sich nun die neu angelegten Eigenschaften, welche per OK in das Dokument eingefügt werden können.

docx_insert_field

 

Das Dokument kann nun ganz normal als Datei gespeichert werden und dient somit als Template.

Manipulieren des Templates mit C# und dem OpenXML SDK
Nun können diese eingefügten Platzhalter per C# Code ausgelesen und manipuliert werden. Beginnen möchte ich mit einem Codebeispiel, welches ich anschließend erklären werde:

    
    // Template Dateiname aus Parameter, Datenbank oder ähnliches auslesen. In diesem Artikel nicht vorgestellt.
    // Die Template Datei muss ebenfalls eine ".docx" Datei sein.
    string templateFile = GetTemplateFilename();

    // Daten intialisieren
    byte[] byteArray = File.ReadAllBytes(templateFile);
    MemoryStream docStream = new MemoryStream();
    docStream.Write(byteArray, 0, (int)byteArray.Length);

    // OpenXML intialisieren
    using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(docStream, true))
    {
        // Felder abfragen
        List fields = ExtractFields(wordDoc);

        // Felder mit Werten ersetzen
        foreach (PlaceholderField curField in fields)
        {
            // ResolvePlaceholder kann über verschiedenste Arten den konkreten Wert zu einem Platzhalter abfragen. 
            // Auf die Implementierung wird in diesem Artikel nicht eingegangen und muss auf die eigenen Anforderungen angepasst werden.
            string newValue = ResolvePlaceholder(curField.Key); 
            curField.InsertPlaceholderValue(newValue);
        }

        // Platzhalter Elemente löschen
        foreach (OpenXmlElement curElem in curField.Elements)
        {
            curElem.RemoveAllChildren();
            if (curElem.Parent != null)
            {
                curElem.Remove();
            }
        }
    }

In diesem Codebeispiel wird zu Beginn eine „.docx“ Datei eingelesen und in einen MemoryStream geschrieben. Ich habe dieses Vorgehen an dieser Stelle gewählt, um später leichter mit der manipulierten Word-Datei arbeiten zu können. Wie genau eine solche Weiterverarbeitung aussehen kann, werde ich jedoch noch später im Artikel erklären.

Nachdem die Template Word-Datei eingelesen wurde, wird ein WordprocessingDocument erzeugt. Hierbei handelt es sich um die zentrale Klasse aus dem OpenXML SDK, um mit einer Word-Datei zu arbeiten.

Nun müssen die Platzhalter aus diesem Word-Dokument extrahiert werden und anschließend durch die konkreten Werte ersetzt werden. Dieses erfolgt in der Funktion ExtractFields, welche wie folgt aussieht:

    
/// <summary>
/// Extrahiert Platzhalter Felder aus einem Dokument
/// </summary>
/// <param name="document">Dokument mit Platzhaltern</param>
/// <returns>Platzhalter des Dokuments</returns>
private List<PlaceholderField> ExtractFields(WordprocessingDocument document)
{
    List<PlaceholderField> fields = new List<PlaceholderField>();
                
    // Alle Elemente iterieren um auch FieldChars extrahieren zu können
    List<OpenXmlElement> fieldCharElements = null;
    OpenXmlElement[] descendants = document.MainDocumentPart.Document.Descendants().ToArray();
    foreach (var item in descendants)
    {
        // SimpleField prüfen
        if(item is SimpleField)
        {
            fields.Add(ExtractSimpleField((SimpleField)item));
            continue;
        }

        // FieldChar prüfen
        if (item is FieldChar)
        {
            FieldChar fieldChar = (FieldChar)item;

            // Prüfen ob neues FieldChar Feld beginnt
            if (fieldChar.FieldCharType == "begin")
            {
                // Wenn bereits ein FieldChar Feld aktiv ist Fehler werfen
                if (fieldCharElements != null)
                {
                    throw new Exception(Constants.ErrorDocumentNestedFieldChars);
                }

                fieldCharElements = new List<OpenXmlElement>();
            }
            else if (fieldChar.FieldCharType == "end")
            {
                // Endes des FieldChars => Daten in PlaceHolder Field umwandeln
                fields.Add(ExtractFieldChar(fieldCharElements));
                fieldCharElements = null;
                continue;
            }
        }

        if (fieldCharElements != null)
        {
            fieldCharElements.Add(item);
        }
    }

    return fields;
}


/// <summary>
/// Generiert ein PlaceholderField aus einem OpenXML SimpleField
/// </summary>
/// <param name="field">OpenXML SimpleField</param>
/// <returns>PlaceholderField</returns>
private PlaceholderField ExtractSimpleField(SimpleField field)
{
    PlaceholderField placeholderField = new PlaceholderField(PlaceholderField.FieldType.SimpleField);
    placeholderField.Key = CleanKey(field.InnerText);
    placeholderField.Elements.Add(field);

    return placeholderField;
}

/// <summary>
/// Generiert ein PlaceholderField aus einem OpenXML FieldChar
/// </summary>
/// <param name="fieldElements">OpenXML FieldChar Elemente</param>
/// <returns>PlaceholderField</returns>
private PlaceholderField ExtractFieldChar(IEnumerable<OpenXmlElement> fieldElements)
{
    PlaceholderField placeholderField = new PlaceholderField(PlaceholderField.FieldType.FieldChar);

    // Schlüssel extrahieren
    string key = string.Empty;
    foreach (OpenXmlElement curElem in fieldElements)
    {
        if (curElem.Descendants<FieldCode>().FirstOrDefault() != null)
        {
            continue;
        }

        if (curElem is Run)
        {
            key += ((Run)curElem).InnerText;
        }
    }

    placeholderField.Key = CleanKey(key);
    placeholderField.Elements.AddRange(fieldElements);

    return placeholderField;
}

/// <summary>
/// Entfernt unnötige und unerwünsche Zeichen im Schlüssel eines Feldes
/// </summary>
/// <param name="key">Schlüssel des Feldes</param>
/// <returns>Bereinigter Schlüssel</returns>
private string CleanKey(string key)
{
    key = key.Trim().ToLower();
    key = key.Replace("<<", "");
    key = key.Replace(">>", "");

    return key;
}

Interessant sind an dieser Stelle die verschiedenen Arten, wie Word Platzhalter speichert. Die einfachste Art, wie Word Felder speichert, ist das „SimpleField“. Das SimpleField entspricht einem einzelnen XML-Knoten im unterliegenden Word-Dokument. Im Gegensatz hierzu besteht ein FieldChar aus mehreren XML-Knoten. Ein FieldChar beginnt mit einem FieldChar Knoten, welcher als Attribut „FieldCharType“ den Wert „Begin“ hat. Anschließend zählen alle nachfolgenden XML-Knoten zu diesem Feld, bis ein FieldChar Knoten mit dem Attribut „FieldCharType“ und dem Wert „End“ angetroffen wird.

Aus diesem Grund ist eine Fallunterscheidung in der Funktion ExtractFields vorzufinden, welche, abhängig von den Feldtypen, die korrekte Vorgehensweise zum Extrahieren auswählt. In diesen einzelnen Funktionen wird der Text des Platzhalters als Schlüssel gespeichert, um später den konkreten Wert für diesen Platzhalter ermitteln und ersetzen zu können. Die CleanKey Funktion entfernt an dieser Stelle lediglich „<<„, „>>“ und wandelt die Schlüssel in Kleinbuchstaben um.

Nachdem alle Felder extrahiert wurden, können diese mit der nachstehenden Memberfunktion der PlaceholderField Klasse durch einen passenden Wert ersetzt werden:

/// <summary>
/// Fügt einen konkreten Wert für den Platzhalter ein. Löscht die OpenXML Elemente nicht.
/// </summary>
/// <param name="value">Wert der zu dem Platzhalter eingefügt wird</param>
public void InsertPlaceholder(string value)
{
    // SimpleField ersetzen
    if (ExtractionType == FieldType.SimpleField)
    {
        if(Elements.Count == 0)
        {
            return;
        }

        Run fieldRun = Elements[0].Elements<Run>().FirstOrDefault();
        if (fieldRun == null)
        {
            return;
        }

        Run clonedRun = (Run)fieldRun.CloneNode(true);
        Text updateText = clonedRun.Descendants<Text>().FirstOrDefault();
        if (updateText == null)
        {
            return;
        }

        updateText.Text = value;
        Elements[0].PreviousSibling().Append(clonedRun);
    }
    else if(ExtractionType == FieldType.FieldChar)
    {
        //Field Char ersetzen
        Run insertRun = new Run();
        insertRun.Append(new Text(value));

        foreach (OpenXmlElement curElem in Elements)
        {
            if (curElem.PreviousSibling() != null)
            {
                curElem.PreviousSibling().Append(insertRun);
                return;
            }
        }
    }
}

An dieser Stelle muss erneut unterschieden werden, um welche Art von Platzhalter es sich handelt: ein SimpleField oder FieldChar. Bei einem SimpleField ist es relativ einfach, den Textknoten zu klonen und durch den eigentlichen Wert zu überschreiben. Durch dieses Vorgehen werden Formatierungen korrekt übernommen.

Da ein FieldChar aus vielen einzelnen XML-Knoten besteht, ist das Vorgehen hier nicht möglich. Aus diesem Grund erzeuge ich an dieser Stelle einen neuen Run mit einem Text Inhalt. Nach meinen Erfahrungen werden hier die meisten Formatierungen korrekt angezeigt, da diese häufig durch überliegende XML-Elemente gesteuert werden. Dieses neu erzeugte Elemente muss nun an das Element, welches einen passenden Vorgänger hat, angehängt werden.

Anschließend beinhaltet der MemoryStream „docStream“ die geänderten Daten und ist bereit für die Weiterverarbeitung.

Fazit
Das OpenXML SDK bietet eine mächtige und einfache Art, Office Dokumente zu bearbeiten, ohne, dass es nötig ist, Microsoft Office auf dem Server, welcher diese Arbeiten durchführt, installieren zu müssen.

Die hier vorgestellte Methode, Dokumente zu bearbeiten, ermöglicht es, die Daten auf verschiedenste Arten weiter zu verarbeiten. So ist es denkbar, dass erzeugte Dokument in einer SharePoint Dokumentenbibliothek zu speichern oder dem Nutzer auf einer ASPX-Seite zum Download anzubieten. Auch ist es möglich, mithilfe der Word Automation Services innerhalb von SharePoint 2013 das Dokument in ein PDF-Dokument umzuwandeln.

Auf diese Möglichkeiten, mit dem generierten Dokument weiter zu arbeiten, werde ich in einem weiteren Blog Artikel eingehen.



Diesen Blogeintrag bewerten:

13 Stimmen mit durchschnittlich 3/5 Punkten

Haben Sie Fragen zu diesem Artikel oder brauchen Sie Unterstützung?

Nehmen Sie mit uns Kontakt auf!

Wir unterstützen Sie gerne bei Ihren SharePoint-Vorhaben!


6 Kommentare zu “Erklärt: Dokumenten Generierung mit dem Open XML SDK”

Hallo,
ich habe mir dieses Thema einmal genauer angesehen und versucht die Code-Snippets zu übernehmen, da ich genau sowas gerade benötige. Jedoch gibt es einige Fehler, die mir die IDE angibt, wo ich auch selbst sagen muss, die IDE hat recht. Gibt es diesen Snippet als funktionierende Klasse als download, was auch schonmal durch einen Compiler durchgegangen ist (ohne diese Fehler, weil was fehlt (also Objekt/Klasse)?

Hallo Mandy,

ich habe unter folgendem Link ein vollständiges Beispiel hochgeladen:
Download Word Sample

Du musst hier innerhalb der DocumentServiceManager.cs Datei noch die Funktionen RemoveEmptyParagraph und GetPlaceholderValue mit Inhalt füllen.

RemoveEmptyParagraph:
Diese Funktion gibt für den Schlüssel eines Platzhalters zurück ob ein leerer Paragraph entfernt werden muss oder nicht. Dies ist beispielsweise nützlich wenn innerhalb eines Adressblocks eine leere Zeile entfernt werden soll.

GetPlaceholderValue
Diese Funktion gibt den Wert zurück der für einen Platzhalter eingefügt wird. Die GetPlaceholderValue Funktion erhält ebenfalls den Schlüssel des Platzhalters als Parameter.

Zusätzlich muss innerhalb des Projekts eine Referenz auf DocumentFormat.OpenXml sowie auf WindowsBase gesetzt werden.

Grüße
Steffen

Hallo Herr Nörtershäuser,

ich habe Ihren Code für ein privates Programm genutzt. Es ist eine kleine WindowsForms-Anwendung die verschiedene Textelemente vom Benutzer sammelt und diese in ein DIN5008-konformes Worddokument einfügt.
Unter anderem benutze ich für den Haupt-Textkörper eine „richtTextBox“. Leider werden darin gemachte Zeilenumbrüche nicht in das Dokument übernommen. Ich kann leider nicht nachvollziehen, an welcher Stelle die Zeilenumbrüche verloren gehen. Die Funktion „GetPlaceholderValue“ gibt den String jedenfalls noch mit den Zeilenumbrüchen zurück. Wie könnte man das lösen?

Ich würde mich freuen, wenn Sie die Zeit fänden, mir zu antworten.

Mit freundlichen Grüßen
Sebastian Heitz

Hallo Sebastian,

eine mögliche Lösung hierfür wäre es deinen Text anhand der Zeilenumbrüche zu trennen.
Anschließend kannst du für jeden Absatz einen neuen Paragraphen mit dem Inhalt einfügen.
Wie du einen Paragraphen einfügen kannst findest du hier: https://msdn.microsoft.com/de-de/library/office/gg278323.aspx#Anchor_2

Natürlich musst du hier txt mit dem Text des Absatz ersetzen und den Paragraphen nicht an den Body anfügen sondern an das Element an das auch der „clonedRun“ im Beispielcode angefügt wurde.

Viele Grüße,
Steffen

Hallo Steffen,

vielen Dank für deine schnelle Antwort!
Ich habe es mit dem Einfügen eines bzw. mehrerer Paragraphen versucht, aber das führte irgendwie zum selben Ergebnis. Gelöst habe ich es jetzt, indem ich nach jedem „Teilstring“ meiner richTextBox einen „Break“ (https://msdn.microsoft.com/de-de/library/office/documentformat.openxml.wordprocessing.break.aspx) an das Element anfüge.
Ich bin ganz ehrlich, so richtig nachvollziehen konnte ich das nicht, aber es funktioniert nun genau so wie ich es mir vorgestellt habe.

Kannst du neben der Dokumentation weitere Lektüre empfehlen, mit der man sich besser in das OpenXML-Thema einlesen kann? Jetzt habe ich etwas Blut geleckt! 😀
Mal gucken was man noch so im Bereich Excel zaubern kann…

Gruß Sebastian

Schreibe einen Kommentar zu Steffen Nörtershäuser

Kontakt.
Lassen Sie sich von uns beraten
Wir freuen uns über Ihr Interesse an unseren Leistungen. Hinterlassen Sie
uns Ihren Namen, Ihre Telefonnummer und E-Mail Adresse – wir melden
uns schnellstmöglich bei Ihnen.
Kontakt aufnehmen