Как работать с узлами в .NET

Как работать с узлами в .NET

Это руководство показывает, как обходить, выполнять запросы и изменять дерево узлов Word-документа напрямую в Aspose.Words FOSS for .NET, используя Node and CompositeNode базовые классы, которые Document, Section, Paragraph, Table, и все остальные элементы документа построены на их основе. Охватывается ограничение поиска по NodeType, добавление и удаление узлов программно, реагирование на структурные изменения и использование DocumentVisitor для обхода с диспетчеризацией по типу.

Пошаговое руководство

Шаг 1: Установите пакет

Aspose.Words FOSS for .NET пока не публикует пакет NuGet – вместо этого соберите библиотеку из исходного кода. Клонируйте https://github.com/aspose-words-foss/Aspose.Words-FOSS-for-.NET.git, затем соберите Aspose.Words.sln в конфигурации Release с помощью SDK .NET и подключите полученную сборку Aspose.Words к вашему проекту. Библиотека нацелена на .NET Standard 2.0, поэтому она работает на .NET Framework 4.6.2+ и .NET 6/8/10.


Шаг 2: Импортируйте необходимые классы

Добавьте директиву using Aspose.Words; – Node, CompositeNode, NodeType, NodeCollection, NodeList, INodeChangingCallback и DocumentVisitor находятся в пространстве имён Aspose.Words вместе с Document.


Шаг 3: Поймите узел и базовые классы CompositeNode

Каждый объект в дереве документа наследуется от абстрактного класса Node, который предоставляет NodeType (определяющий, какого типа узел), ParentNode (вложенный CompositeNode), Document (документ-владелец) и PreviousSibling / NextSibling для перемещения по узлам одного уровня. Проверьте Node.IsComposite, чтобы узнать, может ли данный узел содержать дочерние элементы, прежде чем приводить его к типу. CompositeNode расширяет Node для контейнерных элементов — FirstChild, LastChild и Count описывают его непосредственных детей, а HasChildNodes сообщает, существуют ли они.


Шаг 4: Запрос дочерних элементов по типу

Вызовите CompositeNode.GetChildNodes(nodeType, isDeep), чтобы получить всех дочерних элементов заданного NodeType, передавая isDeep: true для поиска во всём поддереве, а не только среди прямых детей, или NodeType.Any для сопоставления любого типа узла независимо от его рода. CompositeNode.GetChild(nodeType, index, isDeep) возвращает один подходящий дочерний элемент по индексу. Для структурных типов узлов (Document, Section, Body, Paragraph, Table, Row, Cell), встроенных типов контента (Run, Shape, Comment) и типов механизмов полей (FieldStart, FieldSeparator, FieldEnd, которые обозначают границы поля Word и переключают текст в потоке выполнения), передайте конкретное значение NodeType, которое определяет, что вы ищете.


Шаг 5: Выполняйте XPath-запросы по поддереву

CompositeNode также реализует SelectNodes(xpath) и SelectSingleNode(xpath) для XPath-запросов по поддереву, возвращая совпадения в виде NodeList (или одного Node). CompositeNode.GetEnumerator() поддерживает простой foreach по прямым дочерним элементам узла, когда XPath-запрос избыточен.


Шаг 6: Добавление, перемещение и удаление узлов

Чтобы изменить дерево, вызовите AppendChild(newChild) или PrependChild(newChild), чтобы добавить узел в начало или конец списка дочерних элементов контейнера, и InsertBefore(newChild, refChild) или InsertAfter(newChild, refChild) для вставки относительно существующего дочернего элемента. RemoveChild(oldChild) удаляет один дочерний элемент, а RemoveAllChildren() очищает все дочерние элементы сразу; CompositeNode.IndexOf(child) сообщает позицию дочернего элемента. На самом Node Clone(isCloneChildren) копирует узел – опционально включая его дочерние элементы – и Remove() отсоединяет узел от его родителя.


Шаг 7: Материализовать узлы перед изменением живой коллекции

Модификация коллекции во время итерации цикла foreach — будь то NodeCollection или результат GetChildNodes — приводит к ошибке. Сначала вызовите ToArray() для коллекции, чтобы материализовать узлы в фиксированный массив, затем итерируйте этот массив, вызывая Remove() или внося другие изменения в дерево.


Шаг 8: Сканировать поддерево в порядке документа

Node.NextPreOrder(rootNode) и PreviousPreOrder(rootNode) проходят по дереву в порядке документа, ограниченном заданным корневым узлом — обычный способ сканировать всё поддерево узел за узлом без написания рекурсивного кода обхода.


Шаг 9: Обрабатывать каждый тип узла с помощью DocumentVisitor

Для обработки множества разных типов узлов структурированным способом создайте подкласс от абстрактного класса DocumentVisitor и переопределите методы Visit*Start / Visit*End для интересующих типов узлов — например VisitParagraphStart / VisitParagraphEnd, VisitSectionStart / VisitSectionEnd, VisitBodyStart / VisitBodyEnd — затем вызовите Node.Accept(visitor) на корне поддерева, чтобы запустить его. Это стандартный шаблон для экспорта, трансформации или инспекции документа без написания пользовательского рекурсивного обхода для каждого случая.


Шаг 10: Реагировать на структурные изменения, вызванные другим кодом

Реализуйте INodeChangingCallback, когда коду необходимо реагировать на вставки или удаления, выполненные другим кодом — включая встроенные операции — а не только на изменения, сделанные напрямую. Его методы NodeInserting(args), NodeInserted(args), NodeRemoving(args) и NodeRemoved(args) каждый получают NodeChangingArgs, описывающий затронутый Node, его старый и новый родитель, а также произошедшее NodeChangingAction (Insert или Remove).

Распространённые проблемы и решения

Преобразование Node к CompositeNode вызывает ошибку неверного приведения типа Узел является листовым типом — например Run or FieldStart – который не может содержать дочерние элементы. Проверьте Node.IsComposite сначала, или используйте NodeType чтобы подтвердить, что узел является типом контейнера, перед приведением типа.

GetChildNodes возвращает непосредственные дочерние элементы, когда isDeep равно false isDeep был оставлен false (или опущено, где перегрузка по умолчанию работает поверхностно). Передайте isDeep: true to GetChildNodes() для поиска по всему поддереву вместо непосредственных дочерних элементов.

Изменение коллекции во время её перебора вызывает ошибку Элементы были добавлены или удалены внутри foreach над NodeCollection или результатом GetChildNodes. Вызовите ToArray() сначала, чтобы материализовать узлы для изменения, затем перебрать этот массив, изменяя дерево.

Подкласс DocumentVisitor не вызывается для некоторого контента Посетитель был вызван через Accept() на узле, который фактически не содержит целевого содержимого, или соответствующий Visit* метод не был переопределён. Вызовите Accept() на предке, который содержит содержимое, и переопределите каждый Visit*Start / Visit*End набор, относящийся к типам узлов, обрабатываемым.

INodeChangingCallback не получает уведомление об изменении Обратный вызов сообщает об изменениях только на Document объекте, к которому он был зарегистрирован. Убедитесь, что обратный вызов прикреплён к правильному Document экземпляру до того, как произойдёт изменение, а не после.

Часто задаваемые вопросы

В чём разница между Node и CompositeNode?

Node — базовый класс, от которого наследуются все элементы документа. CompositeNode расширяет Node для элементов, которые могут содержать дочерние (например, Body, Paragraph и Table); листовые элементы, такие как Run, наследуются напрямую от Node и не могут содержать детей.

Как найти все узлы определённого типа в документе?

Вызовите CompositeNode.GetChildNodes(nodeType, isDeep) на узле, от которого начинается поиск – часто это сам Document – передавая целевой NodeType и isDeep: true, чтобы искать во всём поддереве, а не только среди прямых дочерних элементов.

Как безопасно удалять узлы во время итерации?

Сначала соберите узлы, которые нужно удалить, в массив – например с помощью NodeCollection.ToArray() – затем пройдитесь по этому массиву и вызовите Remove() для каждого узла, вместо того чтобы изменять живую коллекцию, пока foreach её итерирует.

Что такое узлы FieldStart, FieldSeparator и FieldEnd?

Это значения NodeType, отмечающие механизм поля Word, встроенного в run stream: FieldStart и FieldEnd ограничивают поле, а FieldSeparator разделяют код поля и его текущее отображаемое значение.

Как обработать каждый абзац, таблицу и run в документе?

Создайте подкласс DocumentVisitor, переопределите методы Visit*Start / Visit*End для необходимых типов узлов, затем вызовите Accept(visitor) на Document (или любом корне поддерева), чтобы выполнить его.

См. также:

 Русский