TX Text Control .NET for WPF

TextControlDocument

Topic

Inside the template of the WPF TextControl, the document is drawn on an element of its own: a TextControlDocument, a Canvas exactly the size of the document. Code that needs a WPF element for the document, for example to bind another control to its size, finds it here. It is not a data object, though: it holds neither the text nor the formatting, and binding it doesn't bind the content.

What TextControlDocument Is

The TextControl is a System.Windows.Controls.Control whose template consists of named parts. Its class declaration lists them with the TemplatePart attribute:

PartTypeRole
TXPART_BorderBorderThe border of the control
TXPART_ScrollViewerScrollViewerShows the visible part of the document
TXPART_DocumentTextControlDocumentThe document itself

TextControlDocument derives from System.Windows.Controls.Canvas. Its size is the size of the whole document, and the ScrollViewer around it decides which part of it is on screen. The class was introduced with version 22.0 and has no members of its own: everything it offers comes from Canvas, Panel and FrameworkElement, such as ActualWidth and ActualHeight.

What the document contains is reached through the TextControl, not through this element: Load, Save, Selection and the collections of tables, images and fields. Binding Document Content connects the content to a view model.

Get the Part in Code

A template part exists once WPF has applied the template, which has happened when the Loaded event of the control occurs. Template.FindName then returns the part by its name. The window in MainWindow.xaml has a TextBlock below the editor:

<Window x:Class="MyWPFApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:WPF="clr-namespace:TXTextControl.WPF;assembly=TXTextControl.WPF"
        Title="MainWindow" Height="450" Width="800">
    <DockPanel>
        <TextBlock x:Name="sizeTextBlock" DockPanel.Dock="Bottom" Margin="4" />
        <WPF:TextControl x:Name="textControl1" Loaded="TextControl1_Loaded" />
    </DockPanel>
</Window>

The handler in MainWindow.xaml.cs finds the document part and binds the TextBlock to its height:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using TXTextControl.WPF;

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

        private void TextControl1_Loaded(object sender, RoutedEventArgs e)
        {
            // The template is applied: find the canvas that shows the document
            TextControlDocument document =
                textControl1.Template.FindName("TXPART_Document", textControl1) as TextControlDocument;

            if (document == null)
            {
                return;
            }

            // Show the height of the canvas below the editor
            Binding heightBinding = new Binding("ActualHeight")
            {
                Source = document,
                StringFormat = "Document height: {0:F0} device-independent units"
            };
            sizeTextBlock.SetBinding(TextBlock.TextProperty, heightBinding);
        }
    }
}

The code works as follows:

  • Template.FindName searches the template of textControl1 for the name TXPART_Document. The as cast returns null if the part isn't there, for example in a template of your own that leaves it out.
  • ActualHeight is a read-only dependency property of every WPF element, measured in device-independent units of 1/96 inch. A Binding with the canvas as its Source updates the TextBlock whenever the height changes, for example when the text grows by a page.
  • The binding is created in code because the part has no name in the scope of the window: ElementName=TXPART_Document in XAML doesn't find an element inside the template of another control.

Run the application: the text below the editor shows the height of the document and changes when the document grows or shrinks.

Get the Part in a Derived Control

A class derived from TextControl gets its parts at the moment the template is applied, by overriding OnApplyTemplate and calling GetTemplateChild:

using TXTextControl.WPF;

namespace MyWPFApp
{
    public class DocumentTextControl : TextControl
    {
        // The canvas of the document, available after the template is applied
        public TextControlDocument DocumentPart { get; private set; }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            DocumentPart = GetTemplateChild("TXPART_Document") as TextControlDocument;
        }
    }
}

Call base.OnApplyTemplate() first, so that the TextControl handles its template as before. The derived control is declared in XAML with the prefix of your project's namespace, such as <local:DocumentTextControl x:Name="textControl1" /> with xmlns:local="clr-namespace:MyWPFApp".

Positions in the Document

Positions in a document are measured in twips, a twentieth of a point, from its top-left corner. The TextControl converts between these document coordinates and its own coordinate system, and reports which part of the document is visible:

MemberWhat it does
PointToDocumentConverts a System.Windows.Point in the coordinate system of the TextControl into document coordinates in twips
PointFromDocumentConverts a point in document coordinates into the coordinate system of the TextControl
ScrollLocationThe top-left corner of the visible part, in twips from the top-left corner of the document; available at run time only
VScroll, HScrollOccur when the vertical or horizontal scroll position has changed

Use these members of the TextControl for positions in the text rather than the offsets of the ScrollViewer part: they are documented in twips, the unit of every other position in TX Text Control. The following handler shows how far the user has scrolled, in inches (1,440 twips each):

private void TextControl1_VScroll(object sender, System.EventArgs e)
{
    double inches = textControl1.ScrollLocation.Y / 1440.0;
    sizeTextBlock.Text = $"Scrolled {inches:F1} inches down";
}

Connect it with VScroll="TextControl1_VScroll" on the TextControl element in XAML.

Replace the Template

The three names are the parts the TextControl looks for in its template. No source of Text Control describes a template of your own for the control, so keep the default template and change the look through the properties and styles described in Styles, Resources, and Themes, such as DisplayColors for the colors around the pages.

Next Steps