c# - office - Reemplace el texto del marcador en un archivo de Word usando Open XML SDK
openxml c# (11)
Supongo que v2.0 es mejor ... tienen algunos buenos examples "cómo: ..." pero los marcadores no parecen actuar tan obviamente como dice 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 otro texto ... no se está realizando un formateo extraño, pero ¿cómo selecciono / reemplazo el texto del marcador?
Acabo de darme cuenta de esto hace 10 minutos, así que perdona la naturaleza pirata del código.
Primero escribí una función auxiliar recursiva auxiliar para encontrar todos los marcadores:
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;
}
Eso me devuelve un Diccionario que puedo usar para dividirme en 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, insertar y reemplazar los marcadores se ve más difícil. Cuando usé InsertAt en lugar de InsertIntoSelf obtuve: "Los elementos no compuestos no tienen elementos secundarios". YMMV
Aquí es cómo lo hago y VB para agregar / reemplazar texto entre bookmarkStart y BookmarkEnd.
<w:bookmarkStart w:name="forbund_kort" w:id="0" />
- <w:r>
<w:t>forbund_kort</w:t>
</w:r>
<w:bookmarkEnd w:id="0" />
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
Aquí está mi enfoque después de usarlos a ustedes como inspiración:
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";
}
}
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 <br> 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
Después de muchas horas, he escrito 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<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);
}
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.
Esto es lo que terminé con: no es 100% perfecto, pero funciona con marcadores simples y texto simple para insertar:
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));
}
}
La mayoría de las soluciones aquí asumen un patrón regular de marcadores de inicio y finalización después de las ejecuciones, lo que no siempre es cierto, por ejemplo, si el marcador comienza en un para o tabla y termina en algún lugar en otro para (como han señalado otros). ¿Qué le parece usar el orden de los documentos para hacer frente al caso en el que los marcadores no se colocan en una estructura regular? El orden de los documentos seguirá encontrando todos los nodos de texto relevantes que se pueden reemplazar. Simplemente haga root.DescendantNodes (). Donde (xtext o bookmarkstart o bookmark end) que se desplazará en el orden del documento, entonces se pueden reemplazar los nodos de texto que aparecen después de ver un nodo de inicio de marcador pero antes de ver un nodo final.
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 de C #, que puede ocuparse de reemplazar los marcadores que se extienden a lo largo de varios párrafos y reemplaza correctamente los marcadores que no comienzan ni terminan en los límites de los párrafos. Todavía no es perfecto, pero más cerca ... espero que sea útil. Edita si encuentras más formas de mejorarlo!
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);
}
}
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<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;
}
Espero que esto ayude
Reemplace los marcadores con un solo contenido (posiblemente varios 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<Run>(new Run(new Text(text)), bookmarkStart);
}
Primero, 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 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.
Tomé el código de la respuesta y tuve varios problemas con él en casos excepcionales:
- Es posible que desee ignorar los marcadores ocultos. Los marcadores están ocultos si el nombre comienza con un _ (guión bajo)
- Si el marcador es para una TableCell más, lo encontrará en BookmarkStart en la primera celda de la fila con la propiedad ColumnFirst que hace referencia al índice de columna basado en 0 de la celda donde comienza el marcador. ColumnLast se refiere a la celda donde finaliza el marcador, para mi caso especial siempre fue ColumnFirst == ColumnLast (los marcadores marcaron solo una columna). En este caso, tampoco encontrarás un BookmarkEnd.
- Los marcadores pueden estar vacíos, por lo que un BookmarkStart sigue directamente un BookmarkEnd, en este caso, simplemente puede llamar a
bookmarkStart.Parent.InsertAfter(new Run(new Text("Hello World")), bookmarkStart)
- 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 siguientes se mantendrán.
- Y no estoy seguro de si mi último truco es necesario, ya que no conozco todas las limitaciones de OpenXML, pero después de descubrir los 4 anteriores, tampoco confié más en que habrá un hermano de Run, con un hijo del texto. Así que en vez de eso, solo miro a todos mis hermanos (hasta BookmarEnd, que tiene la misma ID que BookmarkStart) y reviso a todos los niños hasta que encuentre algún texto. - ¿Tal vez alguien con más experiencia con OpenXML pueda responder si es necesario?
Puedes ver mi implementación específica here )
Espero que esto ayude a algunos de ustedes que experimentaron los mismos problemas.