TX Text Control .NET Server

Comments

Topic

Reviewers leave remarks in a document as comments: a note on a piece of text, with its author, the time it was written and replies from other reviewers. When such a document reaches the server, your code reads the comments into a report, adds comments of its own to text it finds and removes all comments before the document goes to a customer.

Commenting in the browser is described in Comments in the Document Editor section. The examples below are console programs that load Documents/review.docx, a document with comments.

How Comments Work

A comment is a CommentedText object: the commented piece of text together with the comment. Its properties describe both:

PropertyContains
CommentThe text of the comment; it can be changed
Text, Start, LengthThe commented text, the one-based position of its first character and its number of characters
UserNameThe author; null or an empty string for an unknown author
CreationTimeThe time the comment was written, in UTC
NumberThe position of the comment in the text, starting with 1
Replies, RepliedCommentThe replies to the comment and the comment it replies to, each null if there is none

Keep these points in mind:

  • Replies are comments too. A reply belongs to the same piece of text as the comment it answers, but it can have another author and time. Replies are contained in the collection like all other comments, so check RepliedComment to tell them apart.
  • One collection per text part. ServerTextControl.Comments returns the CommentCollection of the main text. Headers, footers and text frames have their own, IFormattedText.Comments.
  • Formats. Comments 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 comments of MS Word.
  • Comments on tracked changes. A comment attached to a tracked change is removed when the change is accepted or rejected, as Track Changes describes.

A DOCX document with comments looks like this in MS Word: each comment is attached to a piece of text, and the first comment has a reply. The blog article Extracting Comments from DOCX Files in .NET C# reads this document with ServerTextControl:

A DOCX document in MS Word with three comments in the margin, each attached to a highlighted word; the first comment has a reply.

List Comments and Their Replies

The following program prints each comment of the main text with its author, time and commented text, followed by its replies:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/review.docx", StreamType.WordprocessingML);

    // The collection contains the comments and their replies
    Console.WriteLine($"Comments and replies: {tx.Comments.Count}");

    foreach (CommentedText comment in tx.Comments)
    {
        // Replies are printed below the comment they answer
        if (comment.RepliedComment != null)
        {
            continue;
        }

        Console.WriteLine($"{AuthorOf(comment)} ({comment.CreationTime:yyyy-MM-dd HH:mm} UTC) on \"{comment.Text}\": {comment.Comment}");

        if (comment.Replies != null)
        {
            foreach (CommentedText reply in comment.Replies)
            {
                Console.WriteLine($"    {AuthorOf(reply)}: {reply.Comment}");
            }
        }
    }
}

// UserName is null or empty for an unknown author
static string AuthorOf(CommentedText comment) =>
    string.IsNullOrEmpty(comment.UserName) ? "Unknown author" : comment.UserName;

The loop skips the replies, because each reply is also printed through the Replies of its comment; without the check, every reply would appear twice. The blog article Extracting Comments from DOCX Files in .NET C# filters comments by author and date with LINQ.

Add a Comment

A comment is added to a range of text given by its position and length. The following program finds a date in a contract and comments on it:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/contract.docx", StreamType.WordprocessingML);

    string searchText = "December 31";

    // Find returns a zero-based position, or -1
    int position = tx.Find(searchText);

    if (position == -1)
    {
        Console.WriteLine($"\"{searchText}\" wasn't found.");
        return;
    }

    // The CommentedText constructor expects a one-based start position
    CommentedText comment = new CommentedText("Please confirm the date.", position + 1, searchText.Length);
    CommentCollection.AddResult result = tx.Comments.Add(comment);

    Console.WriteLine($"Comment added: {result}");
    tx.Save("Documents/contract-commented.docx", StreamType.WordprocessingML);
}

The code works as follows:

  • The CommentedText constructor takes the comment, the one-based start position and the length of the commented text. A constructor with only the comment uses the current selection, and a constructor with a CommentedText as the second parameter creates a reply to that comment.
  • CommentCollection.Add returns an AddResult: PositionInvalid for an invalid position or length, SelectionTooComplex when the text isn't a continuous sequence of characters, NoSelection when no text is selected, Error for an unexpected error and otherwise Successful.

Reply to a Comment

A reply is a CommentedText created with the comment it answers as the second parameter, and added to the same collection. The following program answers the first comment of Documents/review.docx that isn't a reply itself and changes the text of that comment:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/review.docx", StreamType.WordprocessingML);

    foreach (CommentedText comment in tx.Comments)
    {
        // Answer a comment, not a reply
        if (comment.RepliedComment != null)
        {
            continue;
        }

        CommentedText reply = new CommentedText("Checked on the server.", comment);
        CommentCollection.AddResult result = tx.Comments.Add(reply);
        Console.WriteLine($"Reply added: {result}");

        // Comment can be changed directly
        comment.Comment += " (reviewed)";
        break;
    }

    tx.Save("Documents/review-answered.docx", StreamType.WordprocessingML);
}

The loop ends with break after the first comment, because adding a reply changes the collection. EditComment and ReplyToComment do the same through the user interface: they open a comment input control in which a user edits the comment or writes a reply, and return false if it can't be opened. ReplyToComment also returns false for a comment that is a reply itself. Without a user, set Comment and use the constructor as above.

Export Comments to a Report

Collecting the comments of every part of a document gives a review report that another application can process. The following program writes the comments of all text parts with their replies to comments.json:

using System.Text.Json;
using TXTextControl;

List<object> report = new List<object>();

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/review.docx", StreamType.WordprocessingML);

    // The main text, text frames, headers and footers
    foreach (IFormattedText textPart in tx.TextParts)
    {
        foreach (CommentedText comment in textPart.Comments)
        {
            if (comment.RepliedComment != null)
            {
                continue;
            }

            CommentedText[] replies = comment.Replies ?? Array.Empty<CommentedText>();

            report.Add(new
            {
                Part = textPart.GetType().Name,
                Author = comment.UserName,
                Created = comment.CreationTime,
                Text = comment.Text,
                Comment = comment.Comment,
                Replies = replies
                    .Select(reply => new { Author = reply.UserName, Created = reply.CreationTime, Comment = reply.Comment })
                    .ToList()
            });
        }
    }
}

string json = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText("comments.json", json);
Console.WriteLine(json);

The values are copied into new objects inside the using block, while the ServerTextControl instance that the comments belong to still exists. ServerTextControl.TextParts contains the main text, the text frames and the headers and footers.

Remove Comments Before Sending a Document

The following program removes the comments of one author and then all remaining comments of all text parts before it saves the document:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/review.docx", StreamType.WordprocessingML);

    tx.CommentDeleted += (sender, e) =>
        Console.WriteLine($"Removed: \"{e.CommentedText.Comment}\" by {e.CommentedText.UserName}");

    // Removing a comment removes its replies too
    string author = "Tim Typer";

    while (true)
    {
        CommentedText? comment = tx.Comments.Cast<CommentedText>()
            .FirstOrDefault(c => c.RepliedComment == null && c.UserName == author);

        if (comment == null || !tx.Comments.Remove(comment))
        {
            break;
        }
    }

    // Remove all remaining comments of all text parts
    foreach (IFormattedText textPart in tx.TextParts)
    {
        while (textPart.Comments.Count > 0)
        {
            if (!textPart.Comments.Remove(textPart.Comments[1]))
            {
                break;
            }
        }
    }

    tx.Save("Documents/final.docx", StreamType.WordprocessingML);
}

The code works as follows:

  • CommentCollection.Remove removes the comment and its replies and returns true if it was removed. The commented text stays in the document.
  • The program searches the collection again after each removal, and the indexer Item with 1 always returns the first remaining comment.
  • CommentDeleted occurs for each removed comment. In the handler, only Comment, UserName, CreationTime, ID and the highlight settings of the comment are valid. CommentCreated occurs when text has been commented.

To keep the comments in your document and leave them out of a copy only, save the copy with SaveSettings.OmittedContent set to OmittedContent.Comments, as Saving and Converting Documents shows. This setting can't be used for PDF.

Next Steps

  • Track Changes: process the tracked changes that reviewers made.
  • Comments in the Document Editor: let users add and answer comments.
  • Text: find the text to comment on.