Working with the Document Node Tree
Working with the Document Node Tree
This guide shows how to navigate and manipulate a document’s structure directly through its node tree — the object model that Document, Section, Paragraph, Run, Table, and every other document element are built on. Every element in a document derives from Node, and every element capable of holding children derives from CompositeNode, so understanding these two base classes explains how to work with content that no dedicated helper API already covers.
The Node and CompositeNode Base Classes
Node is the abstract base class for every object in a document tree. It exposes NodeType (an enum value identifying what kind of node this is), ParentNode (the enclosing CompositeNode), Document (the owning document), and PreviousSibling/NextSibling for moving across nodes at the same level. Node.IsComposite reports whether a given node can contain children. CompositeNode extends Node for container elements — FirstChild, LastChild, and Count describe its immediate children, and HasChildNodes reports whether any exist. CompositeNode.GetChildNodes(nodeType, isDeep) retrieves all children of a given NodeType, optionally searching the full subtree (isDeep) rather than only direct children, and CompositeNode.GetChild(nodeType, index, isDeep) retrieves a single one by index.
The NodeType Enum
NodeType enumerates every kind of node the document model defines — structural types such as Document, Section, Body, Paragraph, Table, Row, and Cell; inline content types such as Run, Shape, and Comment; and field-machinery types such as FieldStart, FieldSeparator, and FieldEnd, which mark the boundaries and switch text of a Word field within the run stream. Passing a NodeType value to GetChildNodes or GetChild scopes the search to nodes of exactly that type, or pass NodeType.Any to match every node type.
Traversing and Modifying the Tree
CompositeNode.GetEnumerator() lets you iterate over a node’s direct children, and CompositeNode also implements SelectNodes(xpath) and SelectSingleNode(xpath) for XPath-based queries across the subtree. To modify the tree, AppendChild(newChild) and PrependChild(newChild) add a node at the start or end of the children, InsertBefore(newChild, refChild) and InsertAfter(newChild, refChild) insert relative to an existing child, and RemoveChild(oldChild) or RemoveAllChildren() remove nodes. CompositeNode.IndexOf(child) returns a child’s position. On Node itself, Clone(isCloneChildren) makes a copy — optionally deep — and Remove() detaches the node from its parent. Node.NextPreOrder(rootNode) and PreviousPreOrder(rootNode) step through the tree in document order, bounded by a given root node, which is a common way to scan an entire subtree node-by-node without recursive code.
Node Collections and Change Notifications
NodeCollection is a concrete, mutable collection of nodes supporting Add(node), Insert(index, node), Remove(node), RemoveAt(index), Clear(), Contains(node), IndexOf(node), and ToArray(), alongside a Count property. NodeList and NodeEnumerator support read-style iteration over query results, such as the nodes returned by SelectNodes. For observing structural changes as they happen, implement INodeChangingCallback — its NodeInserting(args), NodeInserted(args), NodeRemoving(args), and NodeRemoved(args) methods each receive a NodeChangingArgs describing the affected Node, its OldParent/NewParent, and the NodeChangingAction that occurred.
Visiting Nodes with DocumentVisitor
For processing every node type in a structured way without manually walking the tree, subclass DocumentVisitor and override the Visit*Start/Visit*End methods for the node types you care about — VisitParagraphStart/VisitParagraphEnd, VisitTableStart/VisitTableEnd, VisitRun, and so on — 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.
Tips and Best Practices
- Use
NodeType.AnywithGetChildNodes(NodeType.Any, true)when you need every descendant regardless of type, rather than callingGetChildNodesonce per type. - Prefer
Node.NextPreOrder(rootNode)/PreviousPreOrder(rootNode)for a flat, non-recursive scan of a subtree instead of writing manual recursive descent. - Implement
DocumentVisitorrather than ad hoc tree-walking code when the same processing needs to run against many different node types. - Check
Node.IsCompositebefore casting toCompositeNodewhen working with a node of unknown type. - Register an
INodeChangingCallbackwhen you need to react to insertions or removals made by other code (including built-in operations), not just changes your own code makes directly.
Common Issues
| Issue | Cause | Fix |
|---|---|---|
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 only returns direct children | isDeep was left false (or omitted where the overload defaults to shallow) | Pass isDeep: true to search the entire subtree |
| Modifying a collection while iterating over it throws an error | Nodes were added or removed inside a foreach over NodeCollection or the result of GetChildNodes | Materialize the nodes to modify with ToArray() first, then iterate over the 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 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 |
FAQ
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 your search (often the Document itself), passing the NodeType you want 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 VisitParagraphStart/VisitParagraphEnd, VisitTableStart/VisitTableEnd, and VisitRun for the node types you need, then call Accept(visitor) on the Document (or any subtree root) to run it.
API Reference Summary
| Class/Method | Description |
|---|---|
Node | Abstract base class for every node in a document tree |
CompositeNode | Base class for nodes that can contain children |
CompositeNode.GetChildNodes(nodeType, isDeep) | Retrieve all children of a given type, optionally across the whole subtree |
CompositeNode.AppendChild(newChild) / InsertBefore(newChild, refChild) | Add a node to a container at the end, or before/after an existing child |
NodeType | Enum identifying the kind of a node (Paragraph, Table, Run, FieldStart, and so on) |
NodeCollection | Concrete, mutable collection of Node objects |
Node.Clone(isCloneChildren) | Copy a node, optionally including its children |
Node.NextPreOrder(rootNode) / PreviousPreOrder(rootNode) | Step through a subtree in document order |
INodeChangingCallback | Callback interface notified when nodes are inserted or removed |
DocumentVisitor | Base class for structured, type-dispatched traversal via Node.Accept(visitor) |