Reemplazar el texto de marcador en el archivo de Word utilizando Open XML SDK

Supongo que v2.0 es mejor … tienen algunos buenos ejemplos de “cómo …: pero los marcadores no parecen actuar tan obviamente como una tabla … un marcador está definido por dos elementos XML BookmarkStart & BookmarkEnd . Tenemos algunas plantillas con texto como marcadores y simplemente queremos reemplazar los marcadores con algún otro texto … no está ocurriendo ningún formato extraño, pero ¿cómo selecciono / sustituyo el texto del marcador?

Aquí está mi enfoque después de usarlos como inspiración:

IDictionary bookmarkMap = new Dictionary(); foreach (BookmarkStart bookmarkStart in file.MainDocumentPart.RootElement.Descendants()) { bookmarkMap[bookmarkStart.Name] = bookmarkStart; } foreach (BookmarkStart bookmarkStart in bookmarkMap.Values) { Run bookmarkText = bookmarkStart.NextSibling(); if (bookmarkText != null) { bookmarkText.GetFirstChild().Text = "blah"; } } 

Reemplazar marcadores con un solo contenido (posiblemente múltiples bloques de texto).

 public static void InsertIntoBookmark(BookmarkStart bookmarkStart, string text) { OpenXmlElement elem = bookmarkStart.NextSibling(); while (elem != null && !(elem is BookmarkEnd)) { OpenXmlElement nextElem = elem.NextSibling(); elem.Remove(); elem = nextElem; } bookmarkStart.Parent.InsertAfter(new Run(new Text(text)), bookmarkStart); } 

En primer lugar, se elimina el contenido existente entre el inicio y el final. Luego se agrega una nueva ejecución directamente detrás del inicio (antes del final).

Sin embargo, no estoy seguro de si el marcador está cerrado en otra sección cuando se abrió o en diferentes celdas de la tabla, etc.

Para mí es suficiente por ahora.

Acabo de descifrar esto hace 10 minutos, así que perdona la naturaleza hacker del código.

Primero escribí una función de ayudante recursiva auxiliar para encontrar todos los marcadores:

 private static Dictionary FindBookmarks(OpenXmlElement documentPart, Dictionary results = null, Dictionary unmatched = null ) { results = results ?? new Dictionary(); unmatched = unmatched ?? new Dictionary(); 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; } 

Eso me devuelve un diccionario que puedo usar para separar mi lista de reemplazo y agregar el texto después del marcador:

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

Por lo que puedo decir, parece más difícil insertar y reemplazar los marcadores. Cuando utilicé InsertAt en lugar de InsertIntoSelf, obtuve: “Los elementos no compuestos no tienen elementos secundarios”. YMMV

Después de muchas horas, escribí este método:

  Public static void ReplaceBookmarkParagraphs(WordprocessingDocument doc, string bookmark, string text) { //Find all Paragraph with 'BookmarkStart' var t = (from el in doc.MainDocumentPart.RootElement.Descendants() where (el.Name == bookmark) && (el.NextSibling() != null) select el).First(); //Take ID value var val = t.Id.Value; //Find the next sibling 'text' OpenXmlElement next = t.NextSibling(); //Set text value next.GetFirstChild().Text = text; //Delete all bookmarkEnd node, until the same ID deleteElement(next.GetFirstChild().Parent, next.GetFirstChild().NextSibling(), val, true); } 

Después de eso, llamo:

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

Este código funciona bien si no tienes Marcadores vacíos. Espero que pueda ayudar a alguien.

La mayoría de las soluciones aquí asumen un patrón de marcado habitual de comenzar antes y después de ejecutarse, lo que no siempre es cierto, por ejemplo, si el marcador comienza en un para o tabla y termina en algún otro párrafo (como otros han notado). ¿Qué hay de usar el orden de los documentos para lidiar con el caso en el que los marcadores no se colocan en una estructura regular? El orden del documento aún encontrará todos los nodos de texto relevantes entre los cuales se puede reemplazar. Solo haga root.DescendantNodes (). Where (xtext o bookmarkstart o bookmark end) que recorrerá en orden de documento, entonces uno puede reemplazar los nodos de texto que aparecen después de ver un nodo de inicio de marcador pero antes de ver un nodo final.

Aquí es cómo lo hago y VB para agregar / reemplazar texto entre bookmarkStart y BookmarkEnd.

  -  forbund_kort   Imports DocumentFormat.OpenXml.Packaging Imports DocumentFormat.OpenXml.Wordprocessing Public Class PPWordDocx Public Sub ChangeBookmarks(ByVal path As String) Try Dim doc As WordprocessingDocument = WordprocessingDocument.Open(path, True) 'Read the entire document contents using the GetStream method: Dim bookmarkMap As IDictionary(Of String, BookmarkStart) = New Dictionary(Of String, BookmarkStart)() Dim bs As BookmarkStart For Each bs In doc.MainDocumentPart.RootElement.Descendants(Of BookmarkStart)() bookmarkMap(bs.Name) = bs Next For Each bs In bookmarkMap.Values Dim bsText As DocumentFormat.OpenXml.OpenXmlElement = bs.NextSibling If Not bsText Is Nothing Then If TypeOf bsText Is BookmarkEnd Then 'Add Text element after start bookmark bs.Parent.InsertAfter(New Run(New Text(bs.Name)), bs) Else 'Change Bookmark Text If TypeOf bsText Is Run Then If bsText.GetFirstChild(Of Text)() Is Nothing Then bsText.InsertAt(New Text(bs.Name), 0) End If bsText.GetFirstChild(Of Text)().Text = bs.Name End If End If End If Next doc.MainDocumentPart.RootElement.Save() doc.Close() Catch ex As Exception Throw ex End Try End Sub End Class 

Tomé el código de la respuesta y tuve varios problemas para casos excepcionales:

  1. Es posible que desee ignorar los marcadores ocultos. Los marcadores están ocultos si el nombre comienza con un _ (guión bajo)
  2. Si el marcador es para uno más de TableCell, lo encontrará en el BookmarkStart en la primera celda de la fila con la propiedad ColumnFirst haciendo referencia al índice de columna basado en 0 de la celda donde se inicia el marcador. ColumnLast hace referencia a la celda donde termina el marcador, para mi caso especial siempre fue ColumnFirst == ColumnLast (los marcadores marcaron solo una columna). En este caso, tampoco encontrará un BookmarkEnd.
  3. Los marcadores pueden estar vacíos, por lo que un BookmarkStart sigue directamente a BookmarkEnd, en este caso puede llamar a bookmarkStart.Parent.InsertAfter(new Run(new Text("Hello World")), bookmarkStart)
  4. Además, un marcador puede contener muchos elementos de texto, por lo que es posible que desee eliminar todos los demás elementos; de lo contrario, se podrían reemplazar partes del marcador, mientras que otras partes siguientes permanecerán.
  5. Y no estoy seguro si mi último truco es necesario, ya que no conozco todas las limitaciones de OpenXML, pero después de descubrir las 4 anteriores, tampoco confiaba más en que haya un hermano de Run, con un hijo de Texto. Así que, en cambio, simplemente miro a todos mis hermanos (hasta BookmarEnd, que tiene la misma identificación que BookmarkStart) y reviso a todos los niños hasta que encuentre cualquier texto. – ¿Tal vez alguien con más experiencia con OpenXML puede responder si es necesario?

Puede ver mi implementación específica aquí )

Espero que esto ayude a algunos de ustedes que experimentaron los mismos problemas.

Necesitaba reemplazar el texto de un marcador (el nombre de los marcadores es “Tabla”) con una tabla. Este es mi enfoque:

 public void ReplaceBookmark( DatasetToTable( ds ) ) { MainDocumentPart mainPart = myDoc.MainDocumentPart; Body body = mainPart.Document.GetFirstChild(); var bookmark = body.Descendants() .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; } 

Espero que esto ayude

Así es como lo hago en VB.NET:

 For Each curBookMark In contractBookMarkStarts ''# Get the "Run" immediately following the bookmark and then ''# get the Run's "Text" field runAfterBookmark = curBookMark.NextSibling(Of Wordprocessing.Run)() textInRun = runAfterBookmark.LastChild ''# Decode the bookmark to a contract attribute lines = DecodeContractDataToContractDocFields(curBookMark.Name, curContract).Split(vbCrLf) ''# If there are multiple lines returned then some work needs to be done to create ''# the necessary Run/Text fields to hold lines 2 thru n. If just one line then set the ''# Text field to the attribute from the contract For ptr = 0 To lines.Count - 1 line = lines(ptr) If ptr = 0 Then textInRun.Text = line.Trim() Else ''# Add a 
run/text component then add next line newRunForLf = New Run(runAfterBookmark.OuterXml) newRunForLf.LastChild.Remove() newBreak = New Break() newRunForLf.Append(newBreak) newRunForText = New Run(runAfterBookmark.OuterXml) DirectCast(newRunForText.LastChild, Text).Text = line.Trim curBookMark.Parent.Append(newRunForLf) curBookMark.Parent.Append(newRunForText) End If Next Next

La respuesta aceptada y algunas de las otras hacen suposiciones sobre dónde están los marcadores en la estructura del documento. Aquí está mi código C #, que puede ocuparse de reemplazar marcadores que se extienden a través de múltiples párrafos y reemplazar correctamente los marcadores que no comienzan y terminan en los límites de los párrafos. Todavía no es perfecto, pero está más cerca … espero que sea útil. ¡Edita si encuentras más formas de mejorarlo!

  private static void ReplaceBookmarkParagraphs(MainDocumentPart doc, string bookmark, IEnumerable paras) { var start = doc.Document.Descendants().Where(x => x.Name == bookmark).First(); var end = doc.Document.Descendants().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); } } 

Esto es lo que acabé: no es 100% perfecto, pero funciona con marcadores simples y texto simple para insertar:

 private void FillBookmarksUsingOpenXml(string sourceDoc, string destDoc, Dictionary 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 bookmarkDataVal in bookmarkData) { var bookmarks = from bm in part.Document.Body.Descendants() select bm; foreach (var bookmark in bookmarks) { if (bookmark.Name == bookmarkDataVal.Key) { Run bookmarkText = bookmark.NextSibling(); if (bookmarkText != null) // if the bookmark has text replace it { bookmarkText.GetFirstChild().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)); } }