TX Text Control .NET Server

Subtext Parts

Topic

A clause of a contract, an address block or a section that your application stores in a database on its own is a range of formatted text that code must find again later. A subtext part marks such a range with a name, an ID and optional data, and keeps the mark when the document is saved. With ServerTextControl, you add and nest subtext parts, find them in all parts of a document, read and save their content and remove them again.

Subtext parts are also the building block of merge blocks: a merge block is a subtext part whose name starts with txmb_, as Merge Blocks explains. How users work with subtext parts in the browser is the subject of Subtext Parts in the Document Editor section.

How Subtext Parts Work

A SubTextPart covers a continuous range of characters, which can contain any other element, such as tables, images, fields and further subtext parts. The following rules apply when you add one:

  • Selectable text. A subtext part must cover a part of the document that can be selected, and its length can't be 0.
  • No overlaps. Two subtext parts can follow each other directly, but they can't overlap. The position between them belongs to the second one.
  • Nesting. A subtext part can contain other subtext parts. A nested subtext part must be shorter than the outer one, but it can start or end at the same position. The outer subtext part must be added first.
  • Deletion with the text. When the last character of a subtext part is deleted, the subtext part is deleted too.

Each text part of a document has its own SubTextPartCollection. ServerTextControl.SubTextParts contains the subtext parts of the main text, and HeaderFooter.SubTextParts and TextFrame.SubTextParts those of a header, footer or text frame. A subtext part has these properties:

PropertyContains
NameA name of your choice; an empty string means no name
IDA one-based identifier of your choice; 0 means no ID
DataA string with additional data, for example JSON; an empty string means no data
Start, LengthThe one-based position of the first character and the number of characters, read only
TextThe text of the subtext part, read only
NumberThe position in the text flow, starting with 1; when an outer and a nested subtext part start at the same position, the outer one has the smaller number
NestedLevel1 for a subtext part that isn't nested, 2 for one nested in it and so on
OuterSubTextPart, OuterMostSubTextPartThe subtext part directly around it and the one at level 1, or null for a subtext part at level 1

Mark Text With Subtext Parts

The following program writes a short contract with three clauses, marks the whole contract with a subtext part that stores JSON data and marks each clause with a nested subtext part. It prints the result of each Add call and all subtext parts, then saves the document in the TX Text Control format:

using System.Text.Json;
using TXTextControl;

string[] clauseNames = { "scope", "payment", "liability" };

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();

    tx.Text = "Scope: Acme Corporation licenses TX Text Control .NET Server.\r\n" +
        "Payment: Invoice INV-2025-001 is due within 14 days.\r\n" +
        "Liability: The liability is limited to the license fee.";

    Paragraph first = tx.Paragraphs[1];
    Paragraph last = tx.Paragraphs[tx.Paragraphs.Count];

    // Paragraph.Length includes the paragraph end, which the subtext parts leave out
    int contractLength = last.Start + last.Length - 1 - first.Start;

    // The outer subtext part must be added before the nested ones
    SubTextPart contract = new SubTextPart("contract", 1, first.Start, contractLength)
    {
        Data = JsonSerializer.Serialize(new ContractInfo("Acme Corporation", "INV-2025-001"))
    };
    Console.WriteLine($"contract: {tx.SubTextParts.Add(contract)}");

    // One nested subtext part per clause, with the IDs 2 to 4
    for (int i = 1; i <= tx.Paragraphs.Count; i++)
    {
        Paragraph paragraph = tx.Paragraphs[i];
        SubTextPart clause = new SubTextPart(clauseNames[i - 1], i + 1, paragraph.Start, paragraph.Length - 1);

        SubTextPartCollection.AddResult result = tx.SubTextParts.Add(clause);
        Console.WriteLine($"{clause.Name}: {result}");
    }

    // The numbers follow the text flow, starting with 1
    for (int number = 1; number <= tx.SubTextParts.Count; number++)
    {
        SubTextPart part = tx.SubTextParts[number];
        string indent = new string(' ', (part.NestedLevel - 1) * 2);
        Console.WriteLine($"{indent}{part.Number} {part.Name} (ID {part.ID}): {part.Text}");
    }

    // Only the TX Text Control format keeps the data
    tx.Save("contract.tx", StreamType.InternalUnicodeFormat);
}

record ContractInfo(string Customer, string Invoice);

The code works as follows:

  • The SubTextPart constructor takes a name, an ID, the one-based start position and the length. Paragraph.Start is one-based as well, so it can be passed directly.
  • SubTextPartCollection.Add returns an AddResult. Successful means the subtext part was added. Overlapping, PositionInvalid and AlreadyExists name the reason it wasn't, and Combined means it directly followed a subtext part with the same name and ID and was merged with it.
  • Data holds any string. JsonSerializer of .NET turns a record into JSON, which the program reads back in the next example.
  • The indexer of the collection, Item, takes the number of a subtext part from 1 to Count.

A subtext part can also be created with only a name and an ID. Its start and length are then 0, and Add uses the current selection as its range. NoSelection and SelectionTooComplex are the results when no text is selected or the selection isn't a continuous sequence of characters.

Find Subtext Parts and Save Their Content

A subtext part is found by its name, its ID or its number, and its content can be saved without selecting it. The following program loads contract.tx, reads the data of the contract, lists the clauses nested in it and saves the liability clause as HTML and DOCX:

using System.Text.Json;
using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("contract.tx", StreamType.InternalUnicodeFormat);

    SubTextPart contract = tx.SubTextParts.GetItem("contract");

    if (contract == null)
    {
        Console.WriteLine("The document has no contract subtext part.");
        return;
    }

    // An empty string means that no data is stored
    if (!string.IsNullOrEmpty(contract.Data))
    {
        ContractInfo info = JsonSerializer.Deserialize<ContractInfo>(contract.Data);
        Console.WriteLine($"Customer: {info.Customer}, invoice: {info.Invoice}");
    }

    // All nested subtext parts in text order, or null
    foreach (SubTextPart clause in contract.GetChildren() ?? Array.Empty<SubTextPart>())
    {
        Console.WriteLine($"{clause.Name} (level {clause.NestedLevel}) in {clause.OuterSubTextPart.Name}");
    }

    // The subtext part with the ID 4
    SubTextPart liability = tx.SubTextParts.GetItem(4);

    if (liability != null)
    {
        liability.Save(out string html, StringStreamType.HTMLFormat);
        Console.WriteLine(html);

        liability.Save(out byte[] docx, BinaryStreamType.WordprocessingML);
        File.WriteAllBytes("liability.docx", docx);
    }
}

record ContractInfo(string Customer, string Invoice);

The code works as follows:

  • GetItem takes a name or an ID and returns the first matching subtext part, or null. Without a parameter, it returns the subtext part at the current input position.
  • GetChildren returns the nested subtext parts at all levels in the order of the text, or null if there are none.
  • SubTextPart.Save writes the content to a string for the formats of StringStreamType, such as HTML and RTF, or to a byte array for the formats of BinaryStreamType, such as DOCX. PDF and PDF/A can only be saved for a complete document. Further overloads take SaveSettings.

To save a range that isn't a subtext part, select it and use Selection.Save, as Saving and Converting Documents describes.

Find Subtext Parts in All Text Parts

Headers, footers and text frames can contain subtext parts too. To reach all of them, loop through ServerTextControl.TextParts, whose elements implement IFormattedText with its SubTextParts property. The following program loads an MS Word template, lists its subtext parts and the Word fields inside each of them:

using TXTextControl;

LoadSettings loadSettings = new LoadSettings()
{
    ApplicationFieldFormat = ApplicationFieldFormat.MSWord,
    LoadSubTextParts = true
};

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

    foreach (IFormattedText textPart in tx.TextParts)
    {
        foreach (SubTextPart subTextPart in textPart.SubTextParts)
        {
            Console.WriteLine($"{textPart.GetType().Name}: {subTextPart.Name} (level {subTextPart.NestedLevel})");

            // The fields completely inside the subtext part, or null
            TextField[] fields = subTextPart.GetTextFields(TextFieldType.ApplicationField);

            foreach (TextField field in fields ?? Array.Empty<TextField>())
            {
                if (field is ApplicationField applicationField)
                {
                    Console.WriteLine($"  {applicationField.TypeName} = \"{applicationField.Text}\"");
                }
            }
        }
    }
}

The code works as follows:

  • LoadSubTextParts turns bookmarks that span several characters into subtext parts when a DOCX, DOC or RTF file is loaded. Without it, such bookmarks become document targets at their first character.
  • ApplicationFieldFormat.MSWord imports the Word fields as application fields, as Fields explains.
  • GetTextFields returns the fields of the TextFieldType values you combine, including the fields of nested subtext parts, or null.

Remove Subtext Parts

SubTextPartCollection.Remove removes a subtext part with or without its text. The following program removes the liability clause with its text and then the markers of all remaining subtext parts, keeping their text:

using TXTextControl;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("contract.tx", StreamType.InternalUnicodeFormat);

    // keepText false: the text of the clause is removed as well
    SubTextPart liability = tx.SubTextParts.GetItem("liability");

    if (liability != null)
    {
        tx.SubTextParts.Remove(liability, false, false);
    }

    // Collect the subtext parts at level 1 before removing them
    List<SubTextPart> topLevelParts = new List<SubTextPart>();

    foreach (SubTextPart part in tx.SubTextParts)
    {
        if (part.NestedLevel == 1)
        {
            topLevelParts.Add(part);
        }
    }

    // keepText true, keepNested false: the text stays, the nested subtext parts are removed too
    foreach (SubTextPart part in topLevelParts)
    {
        tx.SubTextParts.Remove(part, true, false);
    }

    Console.WriteLine($"Subtext parts left: {tx.SubTextParts.Count}");
    tx.Save("contract-plain.docx", StreamType.WordprocessingML);
}

Remove always removes the nested subtext parts along with the outer one, unless keepText and keepNested are both true: then only the outer subtext part is removed, and the nested ones stay. It returns true if the subtext part was removed.

To keep the subtext parts in your document but leave them out of a copy you save, set SaveSettings.OmittedContent to OmittedContent.SubTextParts. The text and its formatting are saved without the markers, as Saving and Converting Documents shows. This setting can't be used for PDF.

Formats and Events

  • TX Text Control format. Only this format keeps the Data of subtext parts. Save templates and documents whose subtext parts carry data with StreamType.InternalUnicodeFormat.
  • Other formats. Subtext parts are kept, but without their data. MS Word and RTF files return their subtext parts when you load them with LoadSubTextParts set to true.
  • Events. SubTextPartCreated and SubTextPartDeleted occur when a subtext part is created or deleted. In a SubTextPartDeleted handler, only the Name, ID and highlight settings of the subtext part are still valid. Events lists all events of ServerTextControl.

The blog article The Power of SubTextParts: Typical Use Cases shows further uses, such as storing sections of a document in a database. Its code is written for the Windows Forms TextControl.

Next Steps