docx unbreakable words - openxml

I'm trying to replace words in a docx file like described here:
public static void SearchAndReplace(string document)
{
using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(document, true))
{
string docText = null;
using (StreamReader sr = new StreamReader(wordDoc.MainDocumentPart.GetStream()))
{
docText = sr.ReadToEnd();
}
Regex regexText = new Regex("Hello world!");
docText = regexText.Replace(docText, "Hi Everyone!");
using (StreamWriter sw = new StreamWriter(wordDoc.MainDocumentPart.GetStream(FileMode.Create)))
{
sw.Write(docText);
}
}
}
That's working fine except that sometimes for SomeTest in a document you would get something like:
<w:t>
Some
</w:t>
</w:r>
<w:r w:rsidR="009E5AFA">
<w:rPr>
<w:b/>
<w:color w:val="365F91"/>
<w:sz w:val="22"/>
</w:rPr>
<w:t>
Test
</w:t>
</w:r>
And of course replacement fails. Perhaps there is a workaround to make some words unbreakable in docx? Or perhaps I'm doing replace wrong?

One way to solve this is normalizing the xml of your document before doing transformtions. You can make use of OpenXml Powertools to do this.
Sample code to normalize xml
using (WordprocessingDocument doc =
WordprocessingDocument.Open("Test.docx", true))
{
SimplifyMarkupSettings settings = new SimplifyMarkupSettings
{
NormalizeXml = true, // Merges Run's in a paragraph with similar formatting
// Additional settings if required
AcceptRevisions = true,
RemoveBookmarks = true,
RemoveComments = true,
RemoveGoBackBookmark = true,
RemoveWebHidden = true,
RemoveContentControls = true,
RemoveEndAndFootNotes = true,
RemoveFieldCodes = true,
RemoveLastRenderedPageBreak = true,
RemovePermissions = true,
RemoveProof = true,
RemoveRsidInfo = true,
RemoveSmartTags = true,
RemoveSoftHyphens = true,
ReplaceTabsWithSpaces = true
};
MarkupSimplifier.SimplifyMarkup(doc, settings);
}
This will simplify the markup of Open Xml document to make further transformations easier to work with the document programatically. I always use it before working with a open xml document programatically.
More Info about using these tools can be found here and a good blog article here.

Related

Word found unreadable content in xxx.docx after split a docx using openxml

I have a full.docx which includes two math questions, the docx embeds some pictures and MathType equation (oleobject), I split the doc according to this, get two files (first.docx, second.docx) , first.docx works fine, the second.docx, however, pops up a warning dialog when I try to open it:
"Word found unreadable content in second.docx. Do you want to recover the contents of this document? If you trust the source of this document, click Yes."
After click "Yes", the doc can be opened, the content is also correct, I want to know what is wrong with the second.docx? I have checked it with "Open xml sdk 2.5 productivity tool", but found no reason. Very appreciated for any help. Thanks.
The three files have been uploaded to here.
Show some code:
byte[] templateBytes = System.IO.File.ReadAllBytes(TEMPLATE_YANG_FILE);
using (MemoryStream templateStream = new MemoryStream())
{
templateStream.Write(templateBytes, 0, (int)templateBytes.Length);
string guidStr = Guid.NewGuid().ToString();
using (WordprocessingDocument document = WordprocessingDocument.Open(templateStream, true))
{
document.ChangeDocumentType(DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
MainDocumentPart mainPart = document.MainDocumentPart;
mainPart.Document = new Document();
Body bd = new Body();
foreach (DocumentFormat.OpenXml.Wordprocessing.Paragraph clonedParagrph in lst)
{
bd.AppendChild<DocumentFormat.OpenXml.Wordprocessing.Paragraph>(clonedParagrph);
clonedParagrph.Descendants<Blip>().ToList().ForEach(blip =>
{
var newRelation = document.CopyImage(blip.Embed, this.wordDocument);
blip.Embed = newRelation;
});
clonedParagrph.Descendants<DocumentFormat.OpenXml.Vml.ImageData>().ToList().ForEach(imageData =>
{
var newRelation = document.CopyImage(imageData.RelationshipId, this.wordDocument);
imageData.RelationshipId = newRelation;
});
}
mainPart.Document.Body = bd;
mainPart.Document.Save();
}
string subDocFile = System.IO.Path.Combine(this.outDir, guidStr + ".docx");
this.subWordFileLst.Add(subDocFile);
File.WriteAllBytes(subDocFile, templateStream.ToArray());
}
the lst contains Paragraph cloned from original docx using:
(DocumentFormat.OpenXml.Wordprocessing.Paragraph)p.Clone();
Using productivity tool, found oleobjectx.bin not copied, so I add below code after copy Blip and ImageData:
clonedParagrph.Descendants<OleObject>().ToList().ForEach(ole =>
{
var newRelation = document.CopyOleObject(ole.Id, this.wordDocument);
ole.Id = newRelation;
});
Solved the issue.

Adding Runs to Paragraphs

I'm trying to convert xml formatted with tags to a DOCX file. I'm not generating a new document, but inserting text in a template document.
<p id="_fab91699-6d85-4ce5-b0b5-a17197520a7f">This document is amongst a series of International Standards dealing with the conversion of systems of writing produced by Technical Committee ISO/TC 46, <em>Information and documentation</em>, WG 3 <em>Conversion of written languages</em>.</p>
I collected the text fragments in an array, then tried to process them with code like this:
foreach (var bkmkStart in wordDoc.MainDocumentPart.RootElement.Descendants<BookmarkStart>())
{
if (bkmkStart.Name == "ForewordText")
{
forewordbkmkParent = bkmkStart.Parent;
for (var y = 0; y <= ForewordArray.Length / (double)2 - 1; y++)
{
if (ForewordArray[0, y] == "Normal")
{
if (y < ForewordArray.Length / (double)2 - 1)
{
if (ForewordArray[0, y + 1] == "Normal")
{
forewordbkmkParent.InsertBeforeSelf(new Paragraph(new Run(new Text(ForewordArray[1, y]))));
}
else
{
fPara = forewordbkmkParent.InsertBeforeSelf(new Paragraph(new Run(new Text(ForewordArray[1, y]))));
}
}
else
{
fPara.InsertAfter(new Run(new Text(ForewordArray[1, y])), fPara.GetFirstChild<Run>());
}
}
else
{
NewRun = forewordbkmkParent.InsertBeforeSelf(new Run());
NewRunProps = new RunProperties();
NewRunProps.AppendChild<Italic>(new Italic());
NewRun.AppendChild<RunProperties>(NewRunProps);
NewRun.AppendChild(new Text(ForewordArray[1, y]));
}
}
}
}
but I end up with malformed XML because the runs are inserted after the paragraphs instead of inside them:
<w:p>
<w:r>
<w:t>This document is amongst a series of International Standards dealing with the conversion of systems of writing produced by Technical Committee ISO/TC 46, </w:t>
</w:r>
</w:p>
<w:r>
<w:rPr>
<w:i />
</w:rPr>
<w:t>Information and documentation</w:t>
</w:r>
<w:p>
<w:r>
<w:t>, WG 3 </w:t>
</w:r>
<w:r>
<w:t>.</w:t>
</w:r>
</w:p>
<w:r>
<w:rPr>
<w:i />
</w:rPr>
<w:t>Conversion of written languages</w:t>
</w:r>
Doing this the right way, using the SDK, would be best. As an alternative, I was able to create a string with all the correct XML and text using regexes, but I can't find a WordprocessingDocument method to turn that into an XML fragment that I can insert.
The solution for this kind of problem is to perform a pure functional transformation, as shown in the following code example.
The code example uses the sample XML element <p> given in the question (see Xml constant below). It transforms it into a corresponding Open XML w:p element, i.e., a Paragraph instance in terms of the strongly-typed classes provided by the Open XML SDK. The expected outer XML of that w:p or Paragraph is defined by the OuterXml constant.
using System;
using System.Linq;
using System.Xml.Linq;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
using Xunit;
namespace CodeSnippets.Tests.OpenXml.Wordprocessing
{
public class XmlTransformationTests
{
private const string Xml =
#"<p id=""_fab91699-6d85-4ce5-b0b5-a17197520a7f"">" +
#"This document is amongst a series of International Standards dealing with the conversion of systems of writing produced by Technical Committee ISO/TC 46, " +
#"<em>Information and documentation</em>" +
#", WG 3 " +
#"<em>Conversion of written languages</em>" +
#"." +
#"</p>";
private const string OuterXml =
#"<w:p xmlns:w=""http://schemas.openxmlformats.org/wordprocessingml/2006/main"">" +
#"<w:r><w:t xml:space=""preserve"">This document is amongst a series of International Standards dealing with the conversion of systems of writing produced by Technical Committee ISO/TC 46, </w:t></w:r>" +
#"<w:r><w:rPr><w:i /></w:rPr><w:t>Information and documentation</w:t></w:r>" +
#"<w:r><w:t xml:space=""preserve"">, WG 3 </w:t></w:r>" +
#"<w:r><w:rPr><w:i /></w:rPr><w:t>Conversion of written languages</w:t></w:r>" +
#"<w:r><w:t>.</w:t></w:r>" +
#"</w:p>";
[Fact]
public void CanTransformXmlToOpenXml()
{
// Arrange, creating an XElement based on the given XML.
var xmlParagraph = XElement.Parse(Xml);
// Act, transforming the XML into Open XML.
var paragraph = (Paragraph) TransformElementToOpenXml(xmlParagraph);
// Assert, demonstrating that we have indeed created an Open XML Paragraph instance.
Assert.Equal(OuterXml, paragraph.OuterXml);
}
private static OpenXmlElement TransformElementToOpenXml(XElement element)
{
return element.Name.LocalName switch
{
"p" => new Paragraph(element.Nodes().Select(TransformNodeToOpenXml)),
"em" => new Run(new RunProperties(new Italic()), CreateText(element.Value)),
"b" => new Run(new RunProperties(new Bold()), CreateText(element.Value)),
_ => throw new ArgumentOutOfRangeException()
};
}
private static OpenXmlElement TransformNodeToOpenXml(XNode node)
{
return node switch
{
XElement element => TransformElementToOpenXml(element),
XText text => new Run(CreateText(text.Value)),
_ => throw new ArgumentOutOfRangeException()
};
}
private static Text CreateText(string text)
{
return new Text(text)
{
Space = text.Length > 0 && (char.IsWhiteSpace(text[0]) || char.IsWhiteSpace(text[^1]))
? new EnumValue<SpaceProcessingModeValues>(SpaceProcessingModeValues.Preserve)
: null
};
}
}
}
The above sample deals with <p> (paragraph), <em> (emphasis / italic), and <b> (bold) elements. Adding further formatting elements (e.g., underlining) is easy.
Note that the sample code makes the simplifying assumption that <em>, <b>, and potentially further formatting elements are not nested. Adding the capability to nest those elements would make the sample code a little more complicated (but it's obviously possible).

How to merge word documents with different headers using openxml?

I am trying to merge multiple documents into a single one by following examples as posted in this other post.
I am using AltChunk altChunk = new AltChunk(). When documents are merged, it does not seem to retain seperate hearders of each document. The merged document will contain the headers of the first document during the merging. If the first document being merged contains no hearders, then all the rest of the newly merged document will contain no headers, and vise versa.
My question is, how can I preserve different headers of the documents being merged?
Merge multiple word documents into one Open Xml
using System;
using System.IO;
using System.Linq;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
namespace WordMergeProject
{
public class Program
{
private static void Main(string[] args)
{
byte[] word1 = File.ReadAllBytes(#"..\..\word1.docx");
byte[] word2 = File.ReadAllBytes(#"..\..\word2.docx");
byte[] result = Merge(word1, word2);
File.WriteAllBytes(#"..\..\word3.docx", result);
}
private static byte[] Merge(byte[] dest, byte[] src)
{
string altChunkId = "AltChunkId" + DateTime.Now.Ticks.ToString();
var memoryStreamDest = new MemoryStream();
memoryStreamDest.Write(dest, 0, dest.Length);
memoryStreamDest.Seek(0, SeekOrigin.Begin);
var memoryStreamSrc = new MemoryStream(src);
using (WordprocessingDocument doc = WordprocessingDocument.Open(memoryStreamDest, true))
{
MainDocumentPart mainPart = doc.MainDocumentPart;
AlternativeFormatImportPart altPart =
mainPart.AddAlternativeFormatImportPart(AlternativeFormatImportPartType.WordprocessingML, altChunkId);
altPart.FeedData(memoryStreamSrc);
var altChunk = new AltChunk();
altChunk.Id = altChunkId;
OpenXmlElement lastElem = mainPart.Document.Body.Elements<AltChunk>().LastOrDefault();
if(lastElem == null)
{
lastElem = mainPart.Document.Body.Elements<Paragraph>().Last();
}
//Page Brake einfügen
Paragraph pageBreakP = new Paragraph();
Run pageBreakR = new Run();
Break pageBreakBr = new Break() { Type = BreakValues.Page };
pageBreakP.Append(pageBreakR);
pageBreakR.Append(pageBreakBr);
return memoryStreamDest.ToArray();
}
}
}
I encountered this question a few years ago and spent quite some time on it; I eventually wrote a blog article that links to a sample file. Achieving integrating files with headers and footers using Alt-Chunk is not straight-forward. I'll try to cover the essentials, here. Depending on what kinds of content the headers and footers contain (and assuming Microsoft has not addressed any of the problems I originally ran into) it may not be possible to rely soley on AltChunk.
(Note also that there may be Tools/APIs that can handle this - I don't know and asking that on this site would be off-topic.)
Background
Before attacking the problem, it helps to understand how Word handles different headers and footers. To get a feel for it, start Word...
Section Breaks / Unlinking headers/footers
Type some text on the page and insert a header
Move the focus to the end of the page and go to the Page Layout tab in the Ribbon
Page Setup/Breaks/Next Page section break
Go into the Header area for this page and note the information in the blue "tags": you'll see a section identifier on the left and "Same as previous" on the right. "Same as Previous" is the default, to create a different Header click the "Link to Previous" button in the Header
So, the rule is:
a section break is required, with unlinked headers (and/or footers),
in order to have different header/footer content within a document.
Master/Sub-documents
Word has an (in)famous functionality called "Master Document" that enables linking outside ("sub") documents into a "master" document. Doing so automatically adds the necessary section breaks and unlinks the headers/footers so that the originals are retained.
Go to Word's Outline view
Click "Show Document"
Use "Insert" to insert other files
Notice that two section breaks are inserted, one of the type "Next page" and the other "Continuous". The first is inserted in the file coming in; the second in the "master" file.
Two section breaks are necessary when inserting a file because the last paragraph mark (which contains the section break for the end of the document) is not carried over to the target document. The section break in the target document carries the information to unlink the in-coming header from those already in the target document.
When the master is saved, closed and re-opened the sub documents are in a "collapsed" state (file names as hyperlinks instead of the content). They can be expanded by going back to the Outline view and clicking the "Expand" button. To fully incorporate a sub-document into the document click on the icon at the top left next to a sub-document then clicking "Unlink".
Merging Word Open XML files
This, then, is the type of environment the Open XML SDK needs to create when merging files whose headers and footers need to be retained. Theoretically, either approach should work. Practically, I had problems with using only section breaks; I've never tested using the Master Document feature in Word Open XML.
Inserting section breaks
Here's the basic code for inserting a section break and unlinking headers before bringing in a file using AltChunk. Looking at my old posts and articles, as long as there's no complex page numbering involved, it works:
private void btnMergeWordDocs_Click(object sender, EventArgs e)
{
string sourceFolder = #"C:\Test\MergeDocs\";
string targetFolder = #"C:\Test\";
string altChunkIdBase = "acID";
int altChunkCounter = 1;
string altChunkId = altChunkIdBase + altChunkCounter.ToString();
MainDocumentPart wdDocTargetMainPart = null;
Document docTarget = null;
AlternativeFormatImportPartType afType;
AlternativeFormatImportPart chunk = null;
AltChunk ac = null;
using (WordprocessingDocument wdPkgTarget = WordprocessingDocument.Create(targetFolder + "mergedDoc.docx", DocumentFormat.OpenXml.WordprocessingDocumentType.Document, true))
{
//Will create document in 2007 Compatibility Mode.
//In order to make it 2010 a Settings part must be created and a CompatMode element for the Office version set.
wdDocTargetMainPart = wdPkgTarget.MainDocumentPart;
if (wdDocTargetMainPart == null)
{
wdDocTargetMainPart = wdPkgTarget.AddMainDocumentPart();
Document wdDoc = new Document(
new Body(
new Paragraph(
new Run(new Text() { Text = "First Para" })),
new Paragraph(new Run(new Text() { Text = "Second para" })),
new SectionProperties(
new SectionType() { Val = SectionMarkValues.NextPage },
new PageSize() { Code = 9 },
new PageMargin() { Gutter = 0, Bottom = 1134, Top = 1134, Left = 1318, Right = 1318, Footer = 709, Header = 709 },
new Columns() { Space = "708" },
new TitlePage())));
wdDocTargetMainPart.Document = wdDoc;
}
docTarget = wdDocTargetMainPart.Document;
SectionProperties secPropLast = docTarget.Body.Descendants<SectionProperties>().Last();
SectionProperties secPropNew = (SectionProperties)secPropLast.CloneNode(true);
//A section break must be in a ParagraphProperty
Paragraph lastParaTarget = (Paragraph)docTarget.Body.Descendants<Paragraph>().Last();
ParagraphProperties paraPropTarget = lastParaTarget.ParagraphProperties;
if (paraPropTarget == null)
{
paraPropTarget = new ParagraphProperties();
}
paraPropTarget.Append(secPropNew);
Run paraRun = lastParaTarget.Descendants<Run>().FirstOrDefault();
//lastParaTarget.InsertBefore(paraPropTarget, paraRun);
lastParaTarget.InsertAt(paraPropTarget, 0);
//Process the individual files in the source folder.
//Note that this process will permanently change the files by adding a section break.
System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(sourceFolder);
IEnumerable<System.IO.FileInfo> docFiles = di.EnumerateFiles();
foreach (System.IO.FileInfo fi in docFiles)
{
using (WordprocessingDocument pkgSourceDoc = WordprocessingDocument.Open(fi.FullName, true))
{
IEnumerable<HeaderPart> partsHeader = pkgSourceDoc.MainDocumentPart.GetPartsOfType<HeaderPart>();
IEnumerable<FooterPart> partsFooter = pkgSourceDoc.MainDocumentPart.GetPartsOfType<FooterPart>();
//If the source document has headers or footers we want to retain them.
//This requires inserting a section break at the end of the document.
if (partsHeader.Count() > 0 || partsFooter.Count() > 0)
{
Body sourceBody = pkgSourceDoc.MainDocumentPart.Document.Body;
SectionProperties docSectionBreak = sourceBody.Descendants<SectionProperties>().Last();
//Make a copy of the document section break as this won't be imported into the target document.
//It needs to be appended to the last paragraph of the document
SectionProperties copySectionBreak = (SectionProperties)docSectionBreak.CloneNode(true);
Paragraph lastpara = sourceBody.Descendants<Paragraph>().Last();
ParagraphProperties paraProps = lastpara.ParagraphProperties;
if (paraProps == null)
{
paraProps = new ParagraphProperties();
lastpara.Append(paraProps);
}
paraProps.Append(copySectionBreak);
}
pkgSourceDoc.MainDocumentPart.Document.Save();
}
//Insert the source file into the target file using AltChunk
afType = AlternativeFormatImportPartType.WordprocessingML;
chunk = wdDocTargetMainPart.AddAlternativeFormatImportPart(afType, altChunkId);
System.IO.FileStream fsSourceDocument = new System.IO.FileStream(fi.FullName, System.IO.FileMode.Open);
chunk.FeedData(fsSourceDocument);
//Create the chunk
ac = new AltChunk();
//Link it to the part
ac.Id = altChunkId;
docTarget.Body.InsertAfter(ac, docTarget.Body.Descendants<Paragraph>().Last());
docTarget.Save();
altChunkCounter += 1;
altChunkId = altChunkIdBase + altChunkCounter.ToString();
chunk = null;
ac = null;
}
}
}
If there's complex page numbering (quoted from my blog article):
Unfortunately, there’s a bug in the Word application when integrating
Word document “chunks” into the main document. The process has the
nasty habit of not retaining a number of SectionProperties, among them
the one that sets whether a section has a Different First Page
() and the one to restart Page Numbering () in a section. As long as your documents don’t need to
manage these kinds of headers and footers you can probably use the
“altChunk” approach.
But if you do need to handle complex headers and footers the only
method currently available to you is to copy in the each document in
its entirety, part-by-part. This is a non-trivial undertaking, as
there are numerous possible types of Parts that can be associated not
only with the main document body, but also with each header and footer
part.
...or try the Master/Sub Document approach.
Master/Sub Document
This approach will certainly maintain all information, it will open as a Master document, however, and the Word API (either the user or automation code) is required to "unlink" the sub-documents to turn it into a single, integrated document.
Opening a Master Document file in the Open XML SDK Productivity Tool shows that inserting sub documents into the master document is a fairly straight-forward procedure:
The underlying Word Open XML for the document with one sub-document:
<w:body xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p>
<w:pPr>
<w:pStyle w:val="Heading1" />
</w:pPr>
<w:subDoc r:id="rId6" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" />
</w:p>
<w:sectPr>
<w:headerReference w:type="default" r:id="rId7" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" />
<w:type w:val="continuous" />
<w:pgSz w:w="11906" w:h="16838" />
<w:pgMar w:top="1417" w:right="1417" w:bottom="1134" w:left="1417" w:header="708" w:footer="708" w:gutter="0" />
<w:cols w:space="708" />
<w:docGrid w:linePitch="360" />
</w:sectPr>
</w:body>
and the code:
public class GeneratedClass
{
// Creates an Body instance and adds its children.
public Body GenerateBody()
{
Body body1 = new Body();
Paragraph paragraph1 = new Paragraph();
ParagraphProperties paragraphProperties1 = new ParagraphProperties();
ParagraphStyleId paragraphStyleId1 = new ParagraphStyleId(){ Val = "Heading1" };
paragraphProperties1.Append(paragraphStyleId1);
SubDocumentReference subDocumentReference1 = new SubDocumentReference(){ Id = "rId6" };
paragraph1.Append(paragraphProperties1);
paragraph1.Append(subDocumentReference1);
SectionProperties sectionProperties1 = new SectionProperties();
HeaderReference headerReference1 = new HeaderReference(){ Type = HeaderFooterValues.Default, Id = "rId7" };
SectionType sectionType1 = new SectionType(){ Val = SectionMarkValues.Continuous };
PageSize pageSize1 = new PageSize(){ Width = (UInt32Value)11906U, Height = (UInt32Value)16838U };
PageMargin pageMargin1 = new PageMargin(){ Top = 1417, Right = (UInt32Value)1417U, Bottom = 1134, Left = (UInt32Value)1417U, Header = (UInt32Value)708U, Footer = (UInt32Value)708U, Gutter = (UInt32Value)0U };
Columns columns1 = new Columns(){ Space = "708" };
DocGrid docGrid1 = new DocGrid(){ LinePitch = 360 };
sectionProperties1.Append(headerReference1);
sectionProperties1.Append(sectionType1);
sectionProperties1.Append(pageSize1);
sectionProperties1.Append(pageMargin1);
sectionProperties1.Append(columns1);
sectionProperties1.Append(docGrid1);
body1.Append(paragraph1);
body1.Append(sectionProperties1);
return body1;
}
}

update word Customxml part using of OpenXml API, but can't update "document.xml" in Main Document

we update custom xml part using C# code. we are successfully update document in widows. but can we open this document in Linux environment, it could not be changed value.
how can we achieve change of custom xml part in windows as well as Document.xml in word Folder?
Using a slightly enhanced example from another question, let's assume you have a MainDocumentPart (/word/document.xml) with a data-bound w:sdt (noting that this is a block-level structured document tag (SDT) containing a w:p in this example; it could also be an inline-level SDT contained in a w:p).
<?xml version="1.0" encoding="utf-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:sdt>
<w:sdtPr>
<w:tag w:val="Node" />
<w:dataBinding w:prefixMappings="xmlns:ex='http://example.com'"
w:xpath="ex:Root[1]/ex:Node[1]"
w:storeItemID="{C152D0E4-7C03-4CFA-97E6-721B2DCB5C7B}" />
</w:sdtPr>
<w:sdtContent>
<w:p>
<w:r>
<w:t>VALUE1</w:t>
</w:r>
</w:p>
</w:sdtContent>
</w:sdt>
</w:body>
</w:document>
Our MainDocumentPart references the following CustomXmlPart (/customXML/item.xml):
<?xml version="1.0" encoding="utf-8"?>
<ex:Root xmlns:ex="http://example.com">
<ex:Node>VALUE1</ex:Node>
</ex:Root>
The above CustomXmlPart then references the following CustomXmlPropertiesPart (/customXML/itemProps1.xml):
<?xml version="1.0" encoding="utf-8"?>
<ds:datastoreItem xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/customXml"
ds:itemID="{C152D0E4-7C03-4CFA-97E6-721B2DCB5C7B}">
<ds:schemaRefs>
<ds:schemaRef ds:uri="http://example.com" />
</ds:schemaRefs>
</ds:datastoreItem>
The "enhancement" in this case is the additional w:tag element in the MainDocumentPart at the top. This w:tag element is one way to create an easy-to-use link between a w:sdt element and the custom XML element to which it is bound (e.g., ex:Node). In this example, the value Node happens to be the local name of the ex:Node element.
Finally, here is a working code example that shows how you can update both the CustomXmlPart and the MainDocumentPart. This uses the Open-XML-PowerTools.
[Fact]
public void CanUpdateCustomXmlAndMainDocumentPart()
{
// Define the initial and updated values of our custom XML element and
// the data-bound w:sdt element.
const string initialValue = "VALUE1";
const string updatedValue = "value2";
// Create the root element of the custom XML part with the initial value.
var customXmlRoot =
new XElement(Ns + "Root",
new XAttribute(XNamespace.Xmlns + NsPrefix, NsName),
new XElement(Ns + "Node", initialValue));
// Create the w:sdtContent child element of our w:sdt with the initial value.
var sdtContent =
new XElement(W.sdtContent,
new XElement(W.p,
new XElement(W.r,
new XElement(W.t, initialValue))));
// Create a WordprocessingDocument with the initial values.
using var stream = new MemoryStream();
using (WordprocessingDocument wordDocument = WordprocessingDocument.Create(stream, Type))
{
InitializeWordprocessingDocument(wordDocument, customXmlRoot, sdtContent);
}
// Assert the WordprocessingDocument has the expected, initial values.
using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(stream, true))
{
AssertValuesAreAsExpected(wordDocument, initialValue);
}
// Update the WordprocessingDocument, using the updated value.
using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(stream, true))
{
MainDocumentPart mainDocumentPart = wordDocument.MainDocumentPart;
// Change custom XML element, again using the simplifying assumption
// that we only have a single custom XML part and a single ex:Node
// element.
CustomXmlPart customXmlPart = mainDocumentPart.CustomXmlParts.Single();
XElement root = customXmlPart.GetXElement();
XElement node = root.Elements(Ns + "Node").Single();
node.Value = updatedValue;
customXmlPart.PutXDocument();
// Change the w:sdt contained in the MainDocumentPart.
XElement document = mainDocumentPart.GetXElement();
XElement sdt = FindSdtWithTag("Node", document);
sdtContent = sdt.Elements(W.sdtContent).Single();
sdtContent.ReplaceAll(
new XElement(W.p,
new XElement(W.r,
new XElement(W.t, updatedValue))));
mainDocumentPart.PutXDocument();
}
// Assert the WordprocessingDocument has the expected, updated values.
using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(stream, true))
{
AssertValuesAreAsExpected(wordDocument, updatedValue);
}
}
private static void InitializeWordprocessingDocument(
WordprocessingDocument wordDocument,
XElement customXmlRoot,
XElement sdtContent)
{
MainDocumentPart mainDocumentPart = wordDocument.AddMainDocumentPart();
string storeItemId = CreateCustomXmlPart(mainDocumentPart, customXmlRoot);
mainDocumentPart.PutXDocument(new XDocument(
new XElement(W.document,
new XAttribute(XNamespace.Xmlns + "w", W.w.NamespaceName),
new XElement(W.body,
new XElement(W.sdt,
new XElement(W.sdtPr,
new XElement(W.tag, new XAttribute(W.val, "Node")),
new XElement(W.dataBinding,
new XAttribute(W.prefixMappings, $"xmlns:{NsPrefix}='{NsName}'"),
new XAttribute(W.xpath, $"{NsPrefix}:Root[1]/{NsPrefix}:Node[1]"),
new XAttribute(W.storeItemID, storeItemId))),
sdtContent)))));
}
private static void AssertValuesAreAsExpected(
WordprocessingDocument wordDocument,
string expectedValue)
{
// Retrieve inner text of w:sdt element.
MainDocumentPart mainDocumentPart = wordDocument.MainDocumentPart;
XElement sdt = FindSdtWithTag("Node", mainDocumentPart.GetXElement());
string sdtInnerText = GetInnerText(sdt);
// Retrieve inner text of custom XML element, making the simplifying
// assumption that we only have a single custom XML part. In reality,
// we would have to find the custom XML part to which our w:sdt elements
// are bound among any number of custom XML parts. Further, in our
// simplified example, we also assume there is a single ex:Node element.
CustomXmlPart customXmlPart = mainDocumentPart.CustomXmlParts.Single();
XElement root = customXmlPart.GetXElement();
XElement node = root.Elements(Ns + "Node").Single();
string nodeInnerText = node.Value;
// Assert those inner text are indeed equal.
Assert.Equal(expectedValue, sdtInnerText);
Assert.Equal(expectedValue, nodeInnerText);
}
private static XElement FindSdtWithTag(string tagValue, XElement openXmlCompositeElement)
{
return openXmlCompositeElement
.Descendants(W.sdt)
.FirstOrDefault(e => e
.Elements(W.sdtPr)
.Elements(W.tag)
.Any(tag => (string) tag.Attribute(W.val) == tagValue));
}
private static string GetInnerText(XElement openXmlElement)
{
return openXmlElement
.DescendantsAndSelf(W.r)
.Select(UnicodeMapper.RunToString)
.StringConcatenate();
}
The complete source code is contained in my CodeSnippets GitHub repository. Look for the DataBoundContentControlTests class.

Create OpenXML Table With C#

I'm having issues making an actual table in my excel file. The file is created and all the data is there, but when I open the file the table definition doesn't exist. I'm using this method to try and turn the data range into a table.
private static Table CreateTable(string tableRange, IEnumerable<string> headers)
{
var table = new Table {
Id = 1,
Name = "Table 1",
DisplayName = "Table 1",
Reference = tableRange,
TotalsRowShown = false,
HeaderRowFormatId = 0
};
table.AddNamespaceDeclaration("x", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
UInt32Value i = 1;
var columns = new TableColumns();
foreach (var header in headers) {
columns.Append(new TableColumn {
Id = i++,
Name = header
});
}
var style = new TableStyleInfo {
Name = "TableStyleMedium2",
ShowFirstColumn = false,
ShowLastColumn = false,
ShowRowStripes = true,
ShowColumnStripes = false
};
table.Append(new AutoFilter { Reference = tableRange });
table.Append(columns);
table.Append(style);
return table;
}
The tableRange variable is a simple excel range string, like "A1:D27". I'm trying to add it to the worksheet like so:
var tableDefinitionPart = worksheetPart.AddNewPart<TableDefinitionPart>();
tableDefinitionPart.Table = CreateTable(tableRange, headers);
I'm not getting any errors when I open the file.
If I open the file with the XML Productivity tool and click on the "x:table (Table)" part within the worksheet I see this:
<x:table xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main" id="1" name="Table 1" displayName="Table 1" ref="A1:Z2" totalsRowShown="0" headerRowDxfId="0">
<x:autoFilter ref="A1:Z2" />
<x:tableColumns>
<x:tableColumn id="1" name="Lab Name" />
....
<x:tableColumn id="26" name="Description" />
</x:tableColumns>
<x:tableStyleInfo name="TableStyleMedium2" showFirstColumn="0" showLastColumn="0" showRowStripes="1" showColumnStripes="0" />
</x:table>
So it seems like I'm putting something properly into the file. It's just not an actual "table" as far as Excel is concerned.