TX Text Control .NET for WPF

Subtext Parts

Topic

Knowing where a contract clause or a template block begins and ends lets your WPF application store it, replace it or check it later. A subtext part is exactly that: a named range of text in the TextControl that can carry data of your own, is highlighted on request and raises events when the user works with it.

The examples use a window with a TextControl named textControl1, as in Create Your First WPF Application. Subtext parts are a programming feature: the ribbon has no buttons for them.

What a Subtext Part Is

The main text, each header and footer and each text frame of a document is a text part. Inside a text part, your code can define subtext parts: ranges of characters that have

  • a name and an ID, both optional;
  • data, a string you choose, such as JSON;
  • a highlight mode and a highlight color.

Users edit the text of a subtext part like any other text, and the subtext part grows as they type in it. When its last character is deleted, the subtext part disappears. Subtext parts may be nested, but not overlap: a nested one must be shorter than the outer one, which has to exist first. Two subtext parts may follow each other directly, and the position between them belongs to the second.

TX Text Control uses subtext parts for its own features, too. In mail merge templates, a subtext part whose name begins with txmb_ is a merge block; see Merge Blocks. To let users edit only some parts of a protected document, use editable regions instead, as described in Document Protection and Editable Regions.

Subtext parts exist since version 23.0; their data was added in version 26.0.

Mark the Selected Text

Add buttons for the handlers of this page to MainWindow.xaml, and connect the event of the last section as an attribute of the TextControl:

<DockPanel>
    <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
        <Button Content="Mark Selection" Click="MarkSelection_Click" />
        <Button Content="List" Click="List_Click" />
        <Button Content="Save Clause" Click="SaveClause_Click" />
        <Button Content="Remove" Click="Remove_Click" />
    </StackPanel>
    <WPF:TextControl x:Name="textControl1"
                    SubTextPartEntered="textControl1_SubTextPartEntered" />
</DockPanel>

The first handler in MainWindow.xaml.cs marks the selection as a subtext part named "clause":

using System.IO;
using System.Text;
using System.Windows;
using TXTextControl;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void MarkSelection_Click(object sender, RoutedEventArgs e)
    {
        // Without a start and a length, the current selection is marked
        SubTextPart clause = new SubTextPart("clause", 1)
        {
            HighlightMode = HighlightMode.Always,
            // A System.Drawing.Color, not a System.Windows.Media.Color
            HighlightColor = System.Drawing.Color.FromArgb(60, System.Drawing.Color.Red),
            // Data of your own, stored with the document
            Data = "{\"clauseId\":\"C-100\",\"approved\":false}"
        };

        SubTextPartCollection.AddResult result = textControl1.SubTextParts.Add(clause);

        if (result != SubTextPartCollection.AddResult.Successful)
        {
            MessageBox.Show(this, "The subtext part wasn't added: " + result);
        }
    }
}

Keep the namespace of your project around the class. Select a paragraph and click Mark Selection: the paragraph is highlighted in transparent red.

A document in which one paragraph is marked as a subtext part and highlighted in transparent red.

  • The SubTextPart constructor with a name and an ID creates a subtext part without a position, so that the current selection defines it. Another overload takes a one-based start and a length, for example new SubTextPart("intro", 2, 1, 25) for the first 25 characters.
  • HighlightMode is Always, Never or Activated, which highlights the subtext part only while it contains the input position and the control has the focus.
  • HighlightColor is a System.Drawing.Color, not a WPF color, so the code names its namespace. Its alpha value must be below 255 to keep the text readable; the default is red with an alpha value of 60.
  • Data stores any string with the subtext part.
  • SubTextPartCollection.Add, reached through WPF.TextControl.SubTextParts, returns a SubTextPartCollection.AddResult. Values other than Successful explain the failure, such as NoSelection, Overlapping or SelectionTooComplex. Combined means that the new part directly follows one with the same name and ID and was joined with it.

The collection belongs to the text part with the input focus; each header, footer and text frame has its own.

List and Find Subtext Parts

private void List_Click(object sender, RoutedEventArgs e)
{
    StringBuilder list = new StringBuilder();

    foreach (SubTextPart subTextPart in textControl1.SubTextParts)
    {
        list.AppendLine(subTextPart.Number + " " + subTextPart.Name +
            " (level " + subTextPart.NestedLevel + "): " + subTextPart.Text);
    }

    MessageBox.Show(this, list.ToString());
}

To get one subtext part, call GetItem: without a parameter it returns the subtext part at the input position (with nested parts, the one with the highest nesting level), with a string or a number the first one with that name or ID. ScrollTo brings it into view.

Save and Remove a Subtext Part

private void SaveClause_Click(object sender, RoutedEventArgs e)
{
    SubTextPart clause = textControl1.SubTextParts.GetItem("clause");

    if (clause != null)
    {
        // Only the text of the subtext part, with its formatting
        clause.Save(out byte[] data, BinaryStreamType.InternalUnicodeFormat);
        File.WriteAllBytes("clause.tx", data);
    }
}

private void Remove_Click(object sender, RoutedEventArgs e)
{
    // The subtext part at the input position, or null
    SubTextPart subTextPart = textControl1.SubTextParts.GetItem();

    if (subTextPart != null)
    {
        // true: keep the text, true: keep nested subtext parts
        textControl1.SubTextParts.Remove(subTextPart, true, true);
    }
}
  • SubTextPart.Save saves only the content of the subtext part, formatted, as a byte array or a string. File needs the using System.IO; directive shown above, which WPF projects don't import by default.
  • SubTextPartCollection.Remove decides with two Boolean values whether the text and the nested subtext parts stay in the document. When the text is removed, the nested parts go with it.

Handle Subtext Part Events

The WPF TextControl raises SubTextPartCreated, SubTextPartDeleted, SubTextPartEntered, SubTextPartLeft, SubTextPartClicked and SubTextPartDoubleClicked. Each handler receives a SubTextPartEventArgs with the subtext part. The handler connected in the XAML above shows the data of the clause the cursor enters:

private void textControl1_SubTextPartEntered(object sender, SubTextPartEventArgs e)
{
    Title = e.SubTextPart.Name + ": " + e.SubTextPart.Data;
}

Subtext Parts in Files

  • Saved documents keep the names and IDs of their subtext parts, so that your code finds them again after loading.
  • DOCX, DOC and RTF bookmarks that span several characters become subtext parts when LoadSettings.LoadSubTextParts is true; by default, they are loaded as document targets at their first character.
  • SaveSettings.OmittedContent with OmittedContent.SubTextParts leaves the subtext parts out of a saved document and keeps their text and formatting. This setting isn't available for PDF.

Loading and Saving Documents covers load and save settings. The blog article The Power of SubTextParts: Typical Use Cases shows further uses, such as storing user information with a subtext part.

Next Steps