C# OpenXML 段落移除

15

我正在尝试使用OpenXML从.docx文件中删除段落(我正在使用一些占位文本从docx模板文件进行生成),但是每当我删除段落时,它就会破坏我正在使用的foreach循环,这个循环用来遍历。

MainDocumentPart mainpart = doc.MainDocumentPart;
IEnumerable<OpenXmlElement> elems = mainPart.Document.Body.Descendants();

foreach(OpenXmlElement elem in elems){
    if(elem is Text && elem.InnerText == "##MY_PLACE_HOLDER##")
    {
        Run run = (Run)elem.Parent;
        Paragraph p = (Paragraph)run.Parent;
        p.RemoveAllChildren();
        p.Remove();
    }
}

这行代码可以移除我的占位符和它所在的段落,但是foreach循环停止迭代了。我需要在我的foreach循环中做更多的事情。

这种使用OpenXML在C#中移除段落的方法是可行的吗?为什么我的foreach循环会停止迭代或者如何使它不停止?谢谢。

3个回答

19
这是所谓的“万圣节问题”,因为一些开发人员在万圣节时注意到了它,并且觉得很可怕。这个问题涉及同时使用声明性代码(查询)和命令式代码(删除节点)。如果你仔细想一下,你会发现当你遍历一个链表并开始删除其中的节点时,你会彻底搞乱迭代器。避免这个问题的更简单的方法是将查询结果“实体化”为列表,然后你可以随意遍历列表并删除节点。以下代码唯一的区别是在调用Descendants轴之后调用了ToList函数。
MainDocumentPart mainpart = doc.MainDocumentPart; 
IEnumerable<OpenXmlElement> elems = mainPart.Document.Body.Descendants().ToList(); 

foreach(OpenXmlElement elem in elems){ 
    if(elem is Text && elem.InnerText == "##MY_PLACE_HOLDER##") 
    { 
        Run run = (Run)elem.Parent; 
        Paragraph p = (Paragraph)run.Parent; 
        p.RemoveAllChildren(); 
        p.Remove(); 
    } 
} 

然而,我必须指出你的代码中有另一个 bug。没有什么可以阻止 Word 将该文本节点分成多个来自多个运行的文本元素。虽然在大多数情况下,你的代码将正常工作,但迟早你或用户会采取某些动作(比如选择一个字符,不小心点击了功能区上的加粗按钮),然后你的代码将不再起作用。
如果您真的想在文本级别上工作,那么您需要使用我在这个屏幕录制中介绍的代码:http://openxmldeveloper.org/blog/b/openxmldeveloper/archive/2011/08/04/introducing-textreplacer-a-new-class-for-powertools-for-open-xml.aspx 实际上,你可能可以直接使用该代码来处理你的用例,我相信。
另一种更灵活、更强大的方法详见:

http://openxmldeveloper.org/blog/b/openxmldeveloper/archive/2011/06/13/open-xml-presentation-generation-using-a-template-presentation.aspx

虽然这个屏幕录像是关于PresentationML的,但同样的原则也适用于WordprocessingML。

但更好的是,如果你正在使用WordprocessingML,可以使用内容控件。对于文档生成的一种方法,请参见:

http://ericwhite.com/blog/map/generating-open-xml-wordprocessingml-documents-blog-post-series/

如果您想了解有关使用内容控件的更多信息,请参阅以下内容:

http://www.ericwhite.com/blog/content-controls-expanded

-埃里克-

实际上,我已经使用了.ToList() ,因为使用前一个解决方案出现了其他一些复杂情况。此外,我知道将其拆分成多个运行时的单词拆分(这里是一个糟糕的例子),因此我的占位符没有'_' 。我的占位符是硬编码的,所以尽管我知道内容控件的优点,但我没有使用它们,因为我对它们不够了解,并且有一个短期(小型)项目时间表。感谢您的答案,它非常有洞察力,更加完整。 - edin-m

5

你需要使用两个循环,第一个循环用于存储想要删除的项目,第二个循环用于删除这些项目。类似于以下代码:

List<Paragraph> paragraphsToDelete = new List<Paragraph>();
foreach(OpenXmlElement elem in elems){
    if(elem is Text && elem.InnerText == "##MY_PLACE_HOLDER##")
    {
        Run run = (Run)elem.Parent;
        Paragraph p = (Paragraph)run.Parent;
        paragraphsToDelete.Add(p);
    }
}

foreach (var p in paragraphsToDelete)
{
        p.RemoveAllChildren();
        p.Remove();
}

1
天啊,我真是太蠢了。谢谢。但是为什么它在第一次循环中就会出错呢?(如果有人知道,请告诉我,我会留一些时间接受答案;抱歉不能投票,声望太低) - edin-m
https://dev59.com/OkzSa4cB1Zd3GeqPqeJG - Denis Palnitsky
谢谢。找到另一个好的:https://dev59.com/9HRB5IYBdhLWcg3wgHWr - edin-m

0
Dim elems As IEnumerable(Of OpenXmlElement) = MainPart.Document.Body.Descendants().ToList()
        For Each elem As OpenXmlElement In elems
            If elem.InnerText.IndexOf("fullname") > 0 Then
                elem.RemoveAllChildren()
            End If

        Next

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接