Change tracking
Rivoli PDF can tell you, in detail, every time the document tree changes. Change tracking turns the DOM into a live model: you subscribe to an event, mutate the tree, and receive a description of what happened, which node, what kind of change, and enough context to reverse it. This is the foundation for collaborative editing, undo/redo, synchronization, and audit logging.
The event types live in the Rivoli.Pdf.ChangeTracking namespace.
Opt-in, zero cost when off
Section titled “Opt-in, zero cost when off”Change tracking is disabled by default. A document you never opt in pays nothing: no event allocation, no change objects, no overhead.
var doc = Document.Create("Test");doc.AddSection().Add(new Paragraph("Content"));// No events fired.
doc.EnableChangeTracking();doc.Changed += (sender, e) => Console.WriteLine($"Change: {e.ChangeType} on {e.NodeId}");doc.AddSection(); // Now an event fires.Document exposes the whole surface:
public bool IsChangeTrackingEnabled { get; }public event EventHandler<DocumentChangedEventArgs>? Changed;public void EnableChangeTracking();public void DisableChangeTracking();public IDisposable BeginBatch();The four kinds of change
Section titled “The four kinds of change”Every mutation is one of four fundamental change types, plus a Batch wrapper:
ChangeType | Meaning | Example |
|---|---|---|
NodeAdded | A node was added to a collection | Add a section, block, or inline |
NodeRemoved | A node was removed | Remove a paragraph, clear blocks |
NodeUpdated | A node property changed | Change text, update a style |
NodeMoved | A node was reordered | Move a block up, reorder sections |
Batch | Multiple changes grouped together | A bulk operation (see batching) |
The event payload
Section titled “The event payload”Every event derives from DocumentChangedEventArgs, which carries the common
context:
public class DocumentChangedEventArgs : EventArgs{ public ChangeType ChangeType { get; init; } public string NodePath { get; init; } // e.g. "sections[0].blocks[2]" public string NodeId { get; init; } // identity of the changed node public string NodeType { get; init; } // e.g. "Paragraph" public DateTimeOffset Timestamp { get; init; } public string? UserId { get; init; } // optional session/user attribution}NodeId ties straight back to the stable
node identity, so a listener always knows exactly
which node moved or changed.
Specific change types add the details needed to reverse the change:
// NodeUpdatedEventArgs: what property changed, and from/to whatpublic string PropertyName { get; init; }public object? OldValue { get; init; }public object? NewValue { get; init; }
// NodeAddedEventArgs / NodeRemovedEventArgs: where in which parentpublic int Index { get; init; }public string ParentId { get; init; }public string ParentType { get; init; }
// NodeMovedEventArgs: old and new positionspublic int OldIndex { get; init; }public int NewIndex { get; init; }public string ParentId { get; init; }public string ParentType { get; init; }Because each event captures OldValue/NewValue (or old/new index, or the
removed/added node and its position), you have everything you need to build an
undo stack: an update can be reversed by setting the property back to OldValue, a
move by swapping the indices, and so on.
doc.Changed += (sender, e) =>{ if (e is NodeUpdatedEventArgs upd) Console.WriteLine($"{upd.NodeType} '{upd.PropertyName}': " + $"{upd.OldValue} -> {upd.NewValue}");};Batching
Section titled “Batching”Bulk edits would otherwise flood listeners with one event per change. Wrap them in a
batch and the document emits a single BatchEventArgs when the batch disposes:
using (doc.BeginBatch()){ for (int i = 0; i < 1000; i++) section.Add(new Paragraph($"Para {i}"));} // One BatchEventArgs fires here, carrying all 1000 changes.BeginBatch() returns an IDisposable, so a using block scopes the batch
naturally. The resulting event exposes the collected changes:
public IReadOnlyList<DocumentChangedEventArgs> Changes { get; init; }public int Count => Changes.Count;A listener can react once to the whole batch, or iterate Changes to process each
individual mutation.
What you can build with it
Section titled “What you can build with it”- Collaborative editing: broadcast each change (or batch) to other clients and
apply it there, matching nodes by
NodeId. - Undo/redo: keep a stack of events; reverse them using the captured old/new state.
- Synchronization: keep multiple in-memory copies of a document in step.
- Audit logging: record who (
UserId) changed what (NodePath,PropertyName) and when (Timestamp). - Live UI: re-render only the affected node when the tree changes.
Change tracking pairs naturally with the JSON document model: a serialized snapshot plus a stream of change events is enough to reconstruct any intermediate state of the document.
- The JSON document model: persisting the tree the events describe.
- Dynamic content: generating tree mutations from data.