I assume v2.0 is better... they have some nice \"how to:...\" examples but bookmarks don\'t seem to act as obviously as say a Table... a bookmark is defined by two
The accepted answer and some of the others make assumptions about where the bookmarks are in the document structure. Here's my C# code, which can deal with replacing bookmarks that stretch across multiple paragraphs and correctly replace bookmarks that do not start and end at paragraph boundaries. Still not perfect, but closer... hope it's useful. Edit if you find more ways to improve it!
private static void ReplaceBookmarkParagraphs(MainDocumentPart doc, string bookmark, IEnumerable<OpenXmlElement> paras) {
var start = doc.Document.Descendants<BookmarkStart>().Where(x => x.Name == bookmark).First();
var end = doc.Document.Descendants<BookmarkEnd>().Where(x => x.Id.Value == start.Id.Value).First();
OpenXmlElement current = start;
var done = false;
while ( !done && current != null ) {
OpenXmlElement next;
next = current.NextSibling();
if ( next == null ) {
var parentNext = current.Parent.NextSibling();
while ( !parentNext.HasChildren ) {
var toRemove = parentNext;
parentNext = parentNext.NextSibling();
toRemove.Remove();
}
next = current.Parent.NextSibling().FirstChild;
current.Parent.Remove();
}
if ( next is BookmarkEnd ) {
BookmarkEnd maybeEnd = (BookmarkEnd)next;
if ( maybeEnd.Id.Value == start.Id.Value ) {
done = true;
}
}
if ( current != start ) {
current.Remove();
}
current = next;
}
foreach ( var p in paras ) {
end.Parent.InsertBeforeSelf(p);
}
}
After a lot of hours, I have written this method:
Public static void ReplaceBookmarkParagraphs(WordprocessingDocument doc, string bookmark, string text)
{
//Find all Paragraph with 'BookmarkStart'
var t = (from el in doc.MainDocumentPart.RootElement.Descendants<BookmarkStart>()
where (el.Name == bookmark) &&
(el.NextSibling<Run>() != null)
select el).First();
//Take ID value
var val = t.Id.Value;
//Find the next sibling 'text'
OpenXmlElement next = t.NextSibling<Run>();
//Set text value
next.GetFirstChild<Text>().Text = text;
//Delete all bookmarkEnd node, until the same ID
deleteElement(next.GetFirstChild<Text>().Parent, next.GetFirstChild<Text>().NextSibling(), val, true);
}
After that, I call:
Public static bool deleteElement(OpenXmlElement parentElement, OpenXmlElement elem, string id, bool seekParent)
{
bool found = false;
//Loop until I find BookmarkEnd or null element
while (!found && elem != null && (!(elem is BookmarkEnd) || (((BookmarkEnd)elem).Id.Value != id)))
{
if (elem.ChildElements != null && elem.ChildElements.Count > 0)
{
found = deleteElement(elem, elem.FirstChild, id, false);
}
if (!found)
{
OpenXmlElement nextElem = elem.NextSibling();
elem.Remove();
elem = nextElem;
}
}
if (!found)
{
if (elem == null)
{
if (!(parentElement is Body) && seekParent)
{
//Try to find bookmarkEnd in Sibling nodes
found = deleteElement(parentElement.Parent, parentElement.NextSibling(), id, true);
}
}
else
{
if (elem is BookmarkEnd && ((BookmarkEnd)elem).Id.Value == id)
{
found = true;
}
}
}
return found;
}
This code is working good if u have no empty Bookmarks. I hope it can help someone.
Here's my approach after using you guys as inspiration:
IDictionary<String, BookmarkStart> bookmarkMap =
new Dictionary<String, BookmarkStart>();
foreach (BookmarkStart bookmarkStart in file.MainDocumentPart.RootElement.Descendants<BookmarkStart>())
{
bookmarkMap[bookmarkStart.Name] = bookmarkStart;
}
foreach (BookmarkStart bookmarkStart in bookmarkMap.Values)
{
Run bookmarkText = bookmarkStart.NextSibling<Run>();
if (bookmarkText != null)
{
bookmarkText.GetFirstChild<Text>().Text = "blah";
}
}
I just figured this out 10 minutes ago so forgive the hackish nature of the code.
First I wrote a helper recursive helper function to find all the bookmarks:
private static Dictionary<string, BookmarkEnd> FindBookmarks(OpenXmlElement documentPart, Dictionary<string, BookmarkEnd> results = null, Dictionary<string, string> unmatched = null )
{
results = results ?? new Dictionary<string, BookmarkEnd>();
unmatched = unmatched ?? new Dictionary<string,string>();
foreach (var child in documentPart.Elements())
{
if (child is BookmarkStart)
{
var bStart = child as BookmarkStart;
unmatched.Add(bStart.Id, bStart.Name);
}
if (child is BookmarkEnd)
{
var bEnd = child as BookmarkEnd;
foreach (var orphanName in unmatched)
{
if (bEnd.Id == orphanName.Key)
results.Add(orphanName.Value, bEnd);
}
}
FindBookmarks(child, results, unmatched);
}
return results;
}
That returns me a Dictionary that I can use to part through my replacement list and add the text after the bookmark:
var bookMarks = FindBookmarks(doc.MainDocumentPart.Document);
foreach( var end in bookMarks )
{
var textElement = new Text("asdfasdf");
var runElement = new Run(textElement);
end.Value.InsertAfterSelf(runElement);
}
From what I can tell inserting into and replacing the bookmarks looks harder. When I used InsertAt instead of InsertIntoSelf I got: "Non-composite elements do not have child elements." YMMV
Here's what I ended up with - not 100% perfect but works for simple bookmarks and simple text to insert:
private void FillBookmarksUsingOpenXml(string sourceDoc, string destDoc, Dictionary<string, string> bookmarkData)
{
string wordmlNamespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
// Make a copy of the template file.
File.Copy(sourceDoc, destDoc, true);
//Open the document as an Open XML package and extract the main document part.
using (WordprocessingDocument wordPackage = WordprocessingDocument.Open(destDoc, true))
{
MainDocumentPart part = wordPackage.MainDocumentPart;
//Setup the namespace manager so you can perform XPath queries
//to search for bookmarks in the part.
NameTable nt = new NameTable();
XmlNamespaceManager nsManager = new XmlNamespaceManager(nt);
nsManager.AddNamespace("w", wordmlNamespace);
//Load the part's XML into an XmlDocument instance.
XmlDocument xmlDoc = new XmlDocument(nt);
xmlDoc.Load(part.GetStream());
//Iterate through the bookmarks.
foreach (KeyValuePair<string, string> bookmarkDataVal in bookmarkData)
{
var bookmarks = from bm in part.Document.Body.Descendants<BookmarkStart>()
select bm;
foreach (var bookmark in bookmarks)
{
if (bookmark.Name == bookmarkDataVal.Key)
{
Run bookmarkText = bookmark.NextSibling<Run>();
if (bookmarkText != null) // if the bookmark has text replace it
{
bookmarkText.GetFirstChild<Text>().Text = bookmarkDataVal.Value;
}
else // otherwise append new text immediately after it
{
var parent = bookmark.Parent; // bookmark's parent element
Text text = new Text(bookmarkDataVal.Value);
Run run = new Run(new RunProperties());
run.Append(text);
// insert after bookmark parent
parent.Append(run);
}
//bk.Remove(); // we don't want the bookmark anymore
}
}
}
//Write the changes back to the document part.
xmlDoc.Save(wordPackage.MainDocumentPart.GetStream(FileMode.Create));
}
}
I needed to replace the text of a bookmark (bookmarks name is "Table") with a table. This is my approach:
public void ReplaceBookmark( DatasetToTable( ds ) )
{
MainDocumentPart mainPart = myDoc.MainDocumentPart;
Body body = mainPart.Document.GetFirstChild<Body>();
var bookmark = body.Descendants<BookmarkStart>()
.Where( o => o.Name == "Table" )
.FirstOrDefault();
var parent = bookmark.Parent; //bookmark's parent element
if (ds!=null)
{
parent.InsertAfterSelf( DatasetToTable( ds ) );
parent.Remove();
}
mainPart.Document.Save();
}
public Table DatasetToTable( DataSet ds )
{
Table table = new Table();
//creating table;
return table;
}
Hope this helps