3

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?

Community
  • 1
  • 1
trailmax
  • 34,305
  • 22
  • 140
  • 234
  • Interesting question... It might be a good idea to add appropriate XML tag(s) to this question as an optimal answer might already be a "known" in the XML world. The InnerXml property of rootElement should give you the XML as a string which can analyzed differently than OOXML object model. Using a "Find" of some kind, for example. But there might be a special trick for this the XML pros know? – Cindy Meister Mar 03 '16 at 16:21
  • @CindyMeister thanks for suggestion - added `xml` and `xsd` tags. I was really hoping not to go down to XML/XSD analysis if it can be helped. – trailmax Mar 03 '16 at 16:37
  • 1
    It's possible that System.XML folks might also know a general way for handling something like this in an "object model" approach. You could remove the openxml tag and add that one, as well.... – Cindy Meister Mar 03 '16 at 16:50

1 Answers1

4

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;
}
            
Community
  • 1
  • 1
rene
  • 41,474
  • 78
  • 114
  • 152