Search code examples
c#xmlxsdopenxml-sdksystem.xml

Insert child element in the right order when working with OpenXML


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?


Solution

  • 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;
    }