Track Changes
Contracts come back from MS Word and from reviewers in the Document Editor with every edit marked: inserted and deleted text, each with its author and time. On the server, your code lists these tracked changes, accepts the changes of trusted authors, applies the decisions reviewers made in a web page and produces a clean final document without any markup.
Recording changes while users type is described in Track Changes in the Document Editor section, and reviewing them in the browser in Tracked Changes in the Document Viewer section. The examples below are console programs that load Documents/contract.docx.
How Tracked Changes Work
A TrackedChange is a piece of text that was inserted or deleted while tracking was turned on. It stays marked until it's accepted, which keeps the change, or rejected, which undoes it:
| Property | Contains |
|---|---|
| ChangeKind | InsertedText or DeletedText |
| UserName | The author; null or an empty string for an unknown author |
| ChangeTime | The time of the change in UTC |
| Text | The inserted or deleted text |
| Start, Length | The one-based position of the first character and the number of characters |
| Number | The position of the change in the text, starting with 1 |
ServerTextControl.TrackedChanges returns the TrackedChangeCollection of the main text. Headers, footers and text frames have a collection of their own, IFormattedText.TrackedChanges. Keep these points in mind:
- Accept or reject. TrackedChangeCollection.Remove accepts a change with
trueand rejects it withfalse, and it's the only way to remove a tracked change. Rejecting removes inserted text and restores deleted text. - Formats. Tracked changes are saved and loaded with DOCX, RTF and the TX Text Control format, and with DOC on the Classic engine. They're compatible with the tracked changes of MS Word.
- Comments. A comment attached to a tracked change is removed when the change is accepted or rejected.
List the Tracked Changes of a Document
The following program prints every tracked change of the main text, the text frames and the headers and footers:
using TXTextControl;
using (ServerTextControl tx = new ServerTextControl())
{
tx.Create();
tx.Load("Documents/contract.docx", StreamType.WordprocessingML);
// The main text, text frames, headers and footers each have their own changes
foreach (IFormattedText textPart in tx.TextParts)
{
foreach (TrackedChange change in textPart.TrackedChanges)
{
string author = string.IsNullOrEmpty(change.UserName) ? "Unknown author" : change.UserName;
Console.WriteLine($"{textPart.GetType().Name} {change.Number}: {change.ChangeKind} by {author}, " +
$"{change.ChangeTime:yyyy-MM-dd HH:mm} UTC, characters {change.Start} to {change.Start + change.Length - 1}: \"{change.Text}\"");
}
}
}
ServerTextControl.TextParts contains the main text first, then the text frames and then the headers and footers.
Accept and Reject Changes by Author
A typical rule accepts the changes of trusted reviewers and rejects all others. The following program accepts the changes of one author, rejects the rest and saves the reviewed document:
using TXTextControl;
string trustedAuthor = "Alice Johnson";
using (ServerTextControl tx = new ServerTextControl())
{
tx.Create();
tx.Load("Documents/contract.docx", StreamType.WordprocessingML);
// Copy the changes to a list first, because Remove changes the collection
List<TrackedChange> changes = tx.TrackedChanges.Cast<TrackedChange>().ToList();
int accepted = 0;
int rejected = 0;
foreach (TrackedChange change in changes)
{
bool accept = change.UserName == trustedAuthor;
if (tx.TrackedChanges.Remove(change, accept))
{
if (accept) accepted++; else rejected++;
}
}
Console.WriteLine($"{accepted} accepted, {rejected} rejected");
tx.Save("Documents/contract-reviewed.docx", StreamType.WordprocessingML);
}
The same list can be filtered by any property, for example change.ChangeKind == ChangeKind.DeletedText for deletions only or change.ChangeTime for changes made before a date. The blog article Inspect and Process Track Changes in DOCX Documents with TX Text Control with .NET C# filters by author, kind and time with LINQ.
Apply the Decisions of a Review
In a review application, users decide in a web page which changes to accept, and the page sends the numbers of these changes to the server. The following program accepts the changes with the numbers it receives:
using TXTextControl;
// The numbers of the changes a reviewer accepted
int[] acceptedNumbers = { 2, 5 };
using (ServerTextControl tx = new ServerTextControl())
{
tx.Create();
tx.Load("Documents/contract.docx", StreamType.WordprocessingML);
// The highest number first, so the lower numbers still refer to the same changes
foreach (int number in acceptedNumbers.Distinct().OrderByDescending(n => n))
{
if (number >= 1 && number <= tx.TrackedChanges.Count)
{
tx.TrackedChanges.Remove(tx.TrackedChanges[number], true);
}
}
tx.Save("Documents/contract.docx", StreamType.WordprocessingML);
}
The indexer Item takes the number of a change from 1 to Count. Removing a change doesn't change the numbers of the changes in front of it, which is why the program starts with the highest number. The blog article Building a Modern Track Changes Review Workflow in ASP.NET Core C# builds the complete review application, including page previews.
Accept All Changes Before Publishing
To archive a document or send it as PDF without markup, accept all its changes. The following program accepts the changes of all text parts and saves the result as DOCX and PDF:
using TXTextControl;
using (ServerTextControl tx = new ServerTextControl())
{
tx.Create();
tx.Load("Documents/contract.docx", StreamType.WordprocessingML);
foreach (IFormattedText textPart in tx.TextParts)
{
// The number 1 is always the first remaining change
while (textPart.TrackedChanges.Count > 0)
{
if (!textPart.TrackedChanges.Remove(textPart.TrackedChanges[1], true))
{
break; // the change couldn't be removed
}
}
}
tx.Save("Documents/contract-final.docx", StreamType.WordprocessingML);
tx.Save("Documents/contract-final.pdf", StreamType.AdobePDF);
}
SaveSettings.OmittedContent also has a value TrackedChanges. It leaves out only the markup of the changes when a document is saved, while the text and formatting that belong to the changes are saved, and it can't be used for PDF. To decide what happens to inserted and deleted text, accept or reject the changes with Remove as shown above.
Save the Changed Text and React to Changes
- The text of a change with its formatting. TrackedChange.Save writes the changed text to a string in a
StringStreamTypeformat, such as HTML, or to a byte array in aBinaryStreamTypeformat. Select selects the changed part of the document. - Events. TrackedChangeCreated occurs for each tracked change of a document that is loaded, and TrackedChangeDeleted when a change is accepted or rejected. React to Removed Elements logs every accepted change with it.
Find the Comments of a Change
A reviewer can explain a change in a comment on the same text. No property connects a comment to a tracked change, but their positions do: a comment belongs to a change when the ranges of their characters overlap. The following program lists each change with the comments on it:
using TXTextControl;
using (ServerTextControl tx = new ServerTextControl())
{
tx.Create();
tx.Load("Documents/contract.docx", StreamType.WordprocessingML);
foreach (TrackedChange change in tx.TrackedChanges)
{
int changeEnd = change.Start + change.Length - 1;
Console.WriteLine($"{change.Number} {change.ChangeKind} by {change.UserName}: \"{change.Text}\"");
foreach (CommentedText comment in tx.Comments)
{
int commentEnd = comment.Start + comment.Length - 1;
// Comments that aren't replies and whose range overlaps the change
if (comment.RepliedComment == null && comment.Start <= changeEnd && commentEnd >= change.Start)
{
Console.WriteLine($" {comment.UserName}: {comment.Comment}");
}
}
}
}
Comments describes comments and their replies. The sample of the blog article Explaining Contract Tracked Changes Automatically Using .NET C# and AI pairs changes and comments this way.
Record Changes on the Server
ServerTextControl reads, accepts and rejects tracked changes, but it has no setting that records new ones. The class ServerVisualization.TextViewGenerator, which is derived from ServerTextControl, has one: with IsTrackChangesEnabled set to true, changes made to the document are marked up, and the first name of UserNames becomes their author. The blog article Transforming Legal Review with AI and TX Text Control: A Smarter, Faster Approach to Legal Document Processing in .NET C# uses it to insert text suggestions as tracked changes that users then accept or reject in the Document Editor.
Next Steps
- Comments: read, add and remove comments.
- Document Comparison: find the differences between two versions of a document.
- Track Changes in the Document Editor: let users record and review changes.
- Tracked Changes in the Document Viewer: let users accept and reject changes in the browser.