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:
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?
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:
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;
}