How to Work with Nodes in .NET

How to Work with Nodes in .NET

This guide shows how to walk, query, and modify a Word document’s node tree directly in Aspose.Words FOSS for .NET, using the Node and CompositeNode base classes that Document, Section, Paragraph, Table, and every other document element are built on. It covers scoping a search by NodeType, adding and removing nodes programmatically, reacting to structural changes, and using DocumentVisitor for type-dispatched traversal.

Step-by-Step Guide

Step 1: Install the Package

Aspose.Words FOSS for .NET does not yet publish a NuGet package – build the library from source instead. Clone https://github.com/aspose-words-foss/Aspose.Words-FOSS-for-.NET.git, then build Aspose.Words.sln in Release configuration with the .NET SDK and reference the resulting Aspose.Words assembly from your project. The library targets .NET Standard 2.0, so it runs on .NET Framework 4.6.2+ and .NET 6/8/10.


Step 2: Import Required Classes

Add a using Aspose.Words; directive – Node, CompositeNode, NodeType, NodeCollection, NodeList, INodeChangingCallback, and DocumentVisitor all live in the Aspose.Words namespace alongside Document.


Step 3: Understand the Node and CompositeNode Base Classes

Every object in a document tree derives from the abstract Node class, which exposes NodeType (identifying what kind of node it is), ParentNode (the enclosing CompositeNode), Document (the owning document), and PreviousSibling / NextSibling for moving across nodes at the same level. Check Node.IsComposite to find out whether a given node can contain children before casting it. CompositeNode extends Node for container elements – FirstChild, LastChild, and Count describe its immediate children, and HasChildNodes reports whether any exist.


Step 4: Query Children by Type

Call CompositeNode.GetChildNodes(nodeType, isDeep) to retrieve every child of a given NodeType, passing isDeep: true to search the entire subtree rather than only direct children, or NodeType.Any to match every node type regardless of kind. CompositeNode.GetChild(nodeType, index, isDeep) retrieves a single matching child by index. For structural node types (Document, Section, Body, Paragraph, Table, Row, Cell), inline content types (Run, Shape, Comment), and field-machinery types (FieldStart, FieldSeparator, FieldEnd, which mark a Word field’s boundaries and switch text in the run stream), pass the specific NodeType value that identifies what you’re looking for.


Step 5: Run XPath Queries Across a Subtree

CompositeNode also implements SelectNodes(xpath) and SelectSingleNode(xpath) for XPath-based queries across the subtree, returning matches as a NodeList (or a single Node). CompositeNode.GetEnumerator() supports a plain foreach over a node’s direct children when an XPath query is more than the task needs.


Step 6: Add, Move, and Remove Nodes

To modify the tree, call AppendChild(newChild) or PrependChild(newChild) to add a node at the start or end of a container’s children, and InsertBefore(newChild, refChild) or InsertAfter(newChild, refChild) to insert relative to an existing child. RemoveChild(oldChild) removes one child, and RemoveAllChildren() clears every child at once; CompositeNode.IndexOf(child) reports a child’s position. On Node itself, Clone(isCloneChildren) copies a node – optionally including its children – and Remove() detaches the node from its parent.


Step 7: Materialize Nodes Before Mutating a Live Collection

Modifying a collection while a foreach loop is iterating over it – whether it’s a NodeCollection or the result of GetChildNodes – throws an error. Call ToArray() on the collection first to materialize the nodes into a fixed array, then iterate over that array while calling Remove() or making other changes to the tree.


Step 8: Scan a Subtree in Document Order

Node.NextPreOrder(rootNode) and PreviousPreOrder(rootNode) step through the tree in document order, bounded by a given root node – a common way to scan an entire subtree node-by-node without writing recursive traversal code.


Step 9: Process Every Node Type with DocumentVisitor

For processing many different node types in a structured way, subclass the abstract DocumentVisitor class and override the Visit*Start / Visit*End methods for the node types that matter – for example VisitParagraphStart / VisitParagraphEnd, VisitSectionStart / VisitSectionEnd, VisitBodyStart / VisitBodyEnd – then call Node.Accept(visitor) on the root of the subtree to run it. This is the standard pattern for exporting, transforming, or inspecting a document without writing custom recursive traversal for every use case.


Step 10: React to Structural Changes from Other Code

Implement INodeChangingCallback when code needs to react to insertions or removals made by other code – including built-in operations – not just changes made directly. Its NodeInserting(args), NodeInserted(args), NodeRemoving(args), and NodeRemoved(args) methods each receive a NodeChangingArgs describing the affected Node, its old and new parent, and the NodeChangingAction (Insert or Remove) that occurred.

Common Issues and Fixes

Casting a Node to CompositeNode throws an invalid-cast error The node is a leaf type – for example Run or FieldStart – that cannot hold children. Check Node.IsComposite first, or use NodeType to confirm the node is a container type before casting.

GetChildNodes returns direct children when isDeep is false isDeep was left false (or omitted where the overload defaults to shallow). Pass isDeep: true to GetChildNodes() to search the entire subtree instead of immediate children.

Modifying a collection while iterating over it throws an error Nodes were added or removed inside a foreach over a NodeCollection or the result of GetChildNodes. Call ToArray() first to materialize the nodes to modify, then iterate over that array while mutating the tree.

A DocumentVisitor subclass doesn’t get called for some content The visitor was invoked via Accept() on a node that doesn’t actually contain the target content, or the corresponding Visit* method wasn’t overridden. Call Accept() on an ancestor that contains the content, and override every Visit*Start / Visit*End pair relevant to the node types being processed.

INodeChangingCallback isn’t notified about a change The callback only reports changes on the Document it was registered against. Confirm the callback is attached to the correct Document instance before the change happens, not after.

Frequently Asked Questions

What’s the difference between Node and CompositeNode?

Node is the base class every document element derives from. CompositeNode extends Node for elements that can contain children (such as Body, Paragraph, and Table); leaf elements such as Run derive from Node directly and cannot hold children.

How do I find all nodes of a specific type in a document?

Call CompositeNode.GetChildNodes(nodeType, isDeep) on the node that roots the search – often the Document itself – passing the target NodeType and isDeep: true to search the whole subtree, not just direct children.

How do I safely remove nodes while iterating?

Collect the nodes to remove into an array first – for example with NodeCollection.ToArray() – then iterate over that array and call Remove() on each node, rather than modifying the live collection while a foreach is iterating it.

What are FieldStart, FieldSeparator, and FieldEnd nodes?

They are NodeType values marking the machinery of a Word field embedded in the run stream: FieldStart and FieldEnd bound the field, and FieldSeparator divides the field’s code from its currently displayed result.

How do I process every paragraph, table, and run in a document?

Subclass DocumentVisitor, override the Visit*Start / Visit*End methods for the node types needed, then call Accept(visitor) on the Document (or any subtree root) to run it.

See Also