Insert child element in the right order when working with OpenXML

3.9k Views Asked by At

I'm modifying .docx documents with DocumentFormat.OpenXml library. I know element ordering is important, otherwise the document will not pass schema validation and might result a document that can't be opened in Word.

Now I need to add a DocumentProtection element to DocumentSettingsPart. And I need to insert this child element in the right place inside of a parent.

The schema looks like this:

OpenXML Schema

There are quite a lot of possible ordering of child elements. At the moment I'm adding this element like this:

var documentProtection = new DocumentProtection()
{
    // do the configuration
};

DocumentSettingsPart settings = doc.MainDocumentPart.DocumentSettingsPart;
var rootElement = settings.RootElement;
var prevElement = 
                rootElement.GetFirstChild<DoNotTrackFormatting>() ??
                rootElement.GetFirstChild<DoNotTrackMoves>() ??
                rootElement.GetFirstChild<TrackRevisions>() ??
                rootElement.GetFirstChild<RevisionView>() ??
                rootElement.GetFirstChild<DocumentType>() ??
                rootElement.GetFirstChild<StylePaneSortMethods>() ??
                // SNIP
                rootElement.GetFirstChild<Zoom>() ??
                rootElement.GetFirstChild<View>() ??
                (OpenXmlLeafElement)rootElement.GetFirstChild<WriteProtection>();
rootElement.InsertAfter(documentProtection, prevElement);

I.e. I'm trying to find if any possible element that should go before mine already exists in the document. And then insert DocumentProtection after that element. And given amount of elements this list gets pretty boring.

Is there a better way to add DocumentProtection so it is schema compliant and does not involve enumeration of all possible elements?

2

There are 2 best solutions below

2
On BEST ANSWER

There isn't a nice way to achieve what you want. You'll have to tinker with the collection and you're responsible for keeping the order correct.

Using ILSpy on the Settings class you'll find that the implementors used a helper method SetElement<T> on the base class that takes a position and an instance to insert.

Unfortunately that helper method is marked internal so we can't leverage it if you try to subclass Settings. Instead I re-implemented the needed functionality so you'll have a subclass of Settings that does offer a property for DocumentProtection but uses the re-implemented solution to find the correct location to insert the node:

SettingsExt

public class SettingsExt: Settings
{
    // contruct based on XML
    public SettingsExt(string outerXml)
        :base(outerXml)
    {
        // empty
    }

    public DocumentProtection DocumentProtection
    {
        // get is easy
        get
        {
            return this.GetFirstChild<DocumentProtection>();
        }
        // reimplemented SetElement based on 
        // reversed engineered Settings class
        set
        {

            // eleTagNames is a static string[] declared later
            // it holds all the names of the elements in the right order
            int sequenceNumber = eleTagNames
                .Select((s, i) => new { s= s, idx = i })
                    .Where(s => s.s == "documentProtection")
                    .Select((s) => s.idx)
                    .First(); 
            OpenXmlElement openXmlElement = this.FirstChild;

            OpenXmlElement refChild = null;
            while (openXmlElement != null)
            {
                // a bit naive
                int currentSequence = eleTagNames
                    .Select((s, i) => new { s = s, idx = i })
                    .Where(s => s.s == openXmlElement.LocalName)
                    .Select((s) => s.idx)
                    .First(); ; 
                if (currentSequence == sequenceNumber)
                {
                    if (openXmlElement is DocumentProtection)
                    {
                        refChild = openXmlElement.PreviousSibling();
                        this.RemoveChild<OpenXmlElement>(openXmlElement);
                        break;
                    }
                    refChild = openXmlElement;
                }
                else
                {
                    if (currentSequence > sequenceNumber)
                    {
                        break;
                    }
                    refChild = openXmlElement;
                }
                openXmlElement = openXmlElement.NextSibling();
            }
            if (value != null)
            {
                this.InsertAfter(value, refChild);
            }
        }
    }
    
    // order of elements in the sequence!
    static readonly string[] eleTagNames = new string[]
    {
        "writeProtection",
        "view",
        "zoom",
        "removePersonalInformation",
        "removeDateAndTime",
        "doNotDisplayPageBoundaries",
        "displayBackgroundShape",
        "printPostScriptOverText",
        "printFractionalCharacterWidth",
        "printFormsData",
        "embedTrueTypeFonts",
        "embedSystemFonts",
        "saveSubsetFonts",
        "saveFormsData",
        "mirrorMargins",
        "alignBordersAndEdges",
        "bordersDoNotSurroundHeader",
        "bordersDoNotSurroundFooter",
        "gutterAtTop",
        "hideSpellingErrors",
        "hideGrammaticalErrors",
        "activeWritingStyle",
        "proofState",
        "formsDesign",
        "attachedTemplate",
        "linkStyles",
        "stylePaneFormatFilter",
        "stylePaneSortMethod",
        "documentType",
        "mailMerge",
        "revisionView",
        "trackRevisions",
        "doNotTrackMoves",
        "doNotTrackFormatting",
        "documentProtection",
        "autoFormatOverride",
        "styleLockTheme",
        "styleLockQFSet",
        "defaultTabStop",
        "autoHyphenation",
        "consecutiveHyphenLimit",
        "hyphenationZone",
        "doNotHyphenateCaps",
        "showEnvelope",
        "summaryLength",
        "clickAndTypeStyle",
        "defaultTableStyle",
        "evenAndOddHeaders",
        "bookFoldRevPrinting",
        "bookFoldPrinting",
        "bookFoldPrintingSheets",
        "drawingGridHorizontalSpacing",
        "drawingGridVerticalSpacing",
        "displayHorizontalDrawingGridEvery",
        "displayVerticalDrawingGridEvery",
        "doNotUseMarginsForDrawingGridOrigin",
        "drawingGridHorizontalOrigin",
        "drawingGridVerticalOrigin",
        "doNotShadeFormData",
        "noPunctuationKerning",
        "characterSpacingControl",
        "printTwoOnOne",
        "strictFirstAndLastChars",
        "noLineBreaksAfter",
        "noLineBreaksBefore",
        "savePreviewPicture",
        "doNotValidateAgainstSchema",
        "saveInvalidXml",
        "ignoreMixedContent",
        "alwaysShowPlaceholderText",
        "doNotDemarcateInvalidXml",
        "saveXmlDataOnly",
        "useXSLTWhenSaving",
        "saveThroughXslt",
        "showXMLTags",
        "alwaysMergeEmptyNamespace",
        "updateFields",
        "hdrShapeDefaults",
        "footnotePr",
        "endnotePr",
        "compat",
        "docVars",
        "rsids",
        "mathPr",
        "uiCompat97To2003",
        "attachedSchema",
        "themeFontLang",
        "clrSchemeMapping",
        "doNotIncludeSubdocsInStats",
        "doNotAutoCompressPictures",
        "forceUpgrade",
        "captions",
        "readModeInkLockDown",
        "smartTagType",
        "schemaLibrary",
        "shapeDefaults",
        "doNotEmbedSmartTags",
        "decimalSymbol",
        "listSeparator",
        "docId",
        "discardImageEditingData",
        "defaultImageDpi",
        "conflictMode"};

}

A typical usage scenario with this class is as follows:

using (var doc = WordprocessingDocument.Open(@"c:\tmp\test.docx", true))
{

    var documentProtection = new DocumentProtection()
    {
        Formatting = DocumentFormat.OpenXml.OnOffValue.FromBoolean(true)
    };

    DocumentSettingsPart settings = doc.MainDocumentPart.DocumentSettingsPart;

    // instantiate our ExtendedSettings class based on the
    // original Settings
    var extset = new SettingsExt(settings.Settings.OuterXml);

    // new or existing?
    if (extset.DocumentProtection == null)
    {
        extset.DocumentProtection = documentProtection;
    }
    else
    {
        // replace existing values
    }

    // this is key to make sure our own DOMTree is saved!
    // don't forget this
    settings.Settings = extset;
}
            
0
On

There is an AddChild() method that adds the child in the correct order according to the schema.

It's possible that this method was added in the 3.0 version of the Office OpenXML SDK (Microsoft nuget package).

I can't find any information about this when Googleing, not even the learn.microsoft.com website has anything about it.

But here is an image of the IntelliSense definition. I have verified in my code that this works as advertised :)

enter image description here

A warning though. It appears if you call AddChild() multiple times with the same type of child, it will overwrite the existing child :/ (bug?) Workaround is to use this type of code logic:

var existingElements = wsp.Worksheet.Elements<ConditionalFormatting>();
var count = existingElements.Count();

ConditionalFormatting cf = new ConditionalFormatting();
...

if (count == 0)
{
    wsp.Worksheet.AddChild(cf);
}
else wsp.Worksheet.InsertAfter(cf, existingElements.Last());