Controls

RichTextDocument

This page is the model-side reference for the structured document tree hosted by RichTextBox. In current InkkSlinger code, the concrete document type is FlowDocument; this guide uses the term RichTextDocument as shorthand for that tree of blocks, inlines, tables, lists, hyperlinks, and hosted UI. For the editing surface itself, see RichTextBox.

Mental model

RichTextBox is not a dressed-up TextBox. It is an editor for a tree-structured document. That distinction affects almost everything: what you bind, what gets copied to the clipboard, how formatting is preserved, how selection offsets are interpreted, and what happens when you insert or remove content.

The host control

RichTextBox owns focus, caret, selection, commands, scrolling, rendering, and routed events. It is the editing surface.

The document

FlowDocument is the content model. It stores paragraphs, runs, lists, tables, hyperlinks, and containers that survive editing and serialization.

Use the control when you need editing. Use the document model when you need to reason about content structure, import or export, selection semantics, or rich transformations.

Quick start

The smallest useful setup is a RichTextBox instance plus a FlowDocument. The control will coerce null back to a default document, so the surface always has a valid model.

Minimal editor

<RichTextBox Width="480"
             Height="240"
             Padding="8"
             BorderThickness="1"
             TextWrapping="Wrap" />

Create and assign a document

var document = new FlowDocument();

var paragraph = new Paragraph();
paragraph.Inlines.Add(new Run("Rich document editing starts here."));

document.Blocks.Add(paragraph);
editor.Document = document;

Read-only rich viewer

<RichTextBox Width="560"
             Height="280"
             IsReadOnly="True"
             TextWrapping="Wrap"
             VerticalScrollBarVisibility="Auto" />

RichTextDocument structure

The document is hierarchical. Top-level blocks define major layout regions. Inlines define styled text and inline content inside paragraphs or spans. Structural fidelity matters because editing operations try to preserve it instead of flattening everything into plain text.

Common block types

Paragraph, Section, List, Table, and BlockUIContainer.

Common inline types

Run, Bold, Italic, Underline, Hyperlink, LineBreak, and InlineUIContainer.

Example document tree

FlowDocument
  Paragraph
    Run("Status: ")
    Bold
      Run("Ready")
  List
    ListItem
      Paragraph
        Run("Validate commands")
    ListItem
      Paragraph
        Run("Export to Flow XML")
  Table
    TableRowGroup
      TableRow
        TableCell
          Paragraph
            Run("Owner")
        TableCell
          Paragraph
            Run("Controls Catalog")

Build that shape in code

var document = new FlowDocument();

var header = new Paragraph();
header.Inlines.Add(new Run("Status: "));
var bold = new Bold();
bold.Inlines.Add(new Run("Ready"));
header.Inlines.Add(bold);
document.Blocks.Add(header);

var list = new InkkSlinger.List();
list.Items.Add(CreateItem("Validate commands"));
list.Items.Add(CreateItem("Export to Flow XML"));
document.Blocks.Add(list);

var table = new Table();
var group = new TableRowGroup();
var row = new TableRow();
row.Cells.Add(CreateCell("Owner"));
row.Cells.Add(CreateCell("Controls Catalog"));
group.Rows.Add(row);
table.RowGroups.Add(group);
document.Blocks.Add(table);

editor.Document = document;

static ListItem CreateItem(string text)
{
    var item = new ListItem();
    var paragraph = new Paragraph();
    paragraph.Inlines.Add(new Run(text));
    item.Blocks.Add(paragraph);
    return item;
}

static TableCell CreateCell(string text)
{
    var cell = new TableCell();
    var paragraph = new Paragraph();
    paragraph.Inlines.Add(new Run(text));
    cell.Blocks.Add(paragraph);
    return cell;
}

Blocks in detail

Block elements define flow regions and vertical progression. If you are designing document templates, import pipelines, or structure-aware commands, block boundaries are the first thing to think about.

Paragraph

The default text block. Most typing, selection, formatting, and hyperlink work happens inside paragraphs.

Section

A grouping block for nested block collections. Useful when you want a document subtree that can move as a unit.

List

Represents ordered or unordered sequences. List commands adjust level and can convert selected paragraphs into list items.

Table

Represents grid-like content using row groups, rows, and cells. Table editing commands insert, split, and merge around the active cell.

Ordered list example

var list = new InkkSlinger.List
{
    IsOrdered = true
};

list.Items.Add(CreateItem("Draft structure"));
list.Items.Add(CreateItem("Review formatting"));
list.Items.Add(CreateItem("Publish payload examples"));

document.Blocks.Add(list);

Table example

var table = new Table();
var group = new TableRowGroup();

group.Rows.Add(CreateRow("Format", "Purpose"));
group.Rows.Add(CreateRow("Flow XML", "Preserves rich structure"));
group.Rows.Add(CreateRow("Text", "Plain text interchange"));

table.RowGroups.Add(group);
document.Blocks.Add(table);

static TableRow CreateRow(string left, string right)
{
    var row = new TableRow();
    row.Cells.Add(CreateCell(left));
    row.Cells.Add(CreateCell(right));
    return row;
}

Inlines in detail

Inlines are where rich text semantics actually live. A single visible sentence may be composed of many inline nodes with different style, command, or navigation meaning.

  • Run stores literal text.
  • Bold, Italic, and Underline wrap nested inline collections.
  • Hyperlink combines text with navigation or command activation.
  • LineBreak inserts a soft line transition inside a paragraph without creating a new block.
  • InlineUIContainer reserves inline space for a hosted UIElement.

Inline formatting example

var paragraph = new Paragraph();

paragraph.Inlines.Add(new Run("Deploy status: "));

var italic = new Italic();
italic.Inlines.Add(new Run("warming up"));
paragraph.Inlines.Add(italic);

paragraph.Inlines.Add(new Run(". Review the "));

var link = new Hyperlink
{
    NavigateUri = "https://example.com/build/42"
};
link.Inlines.Add(new Run("build report"));
paragraph.Inlines.Add(link);

document.Blocks.Add(paragraph);

Line break example

var paragraph = new Paragraph();
paragraph.Inlines.Add(new Run("First line"));
paragraph.Inlines.Add(new LineBreak());
paragraph.Inlines.Add(new Run("Second line"));

Hosted UI inside the document

The document model can host actual UI elements, both inline and as blocks. This is useful for document-embedded buttons, badges, status chips, or custom interactive widgets. Hosted children become part of the RichTextBox visual tree and are measured and arranged from document layout results.

Inline UI

Use InlineUIContainer when the hosted element should flow with surrounding text.

Block UI

Use BlockUIContainer when the hosted element should stand as its own block between paragraphs or sections.

Inline hosted button

var button = new Button
{
    Content = "Run",
    Width = 72f,
    Height = 18f
};

var paragraph = new Paragraph();
paragraph.Inlines.Add(new Run("Action: "));
paragraph.Inlines.Add(new InlineUIContainer
{
    Child = button
});

document.Blocks.Add(paragraph);

Block hosted button

document.Blocks.Add(new BlockUIContainer
{
    Child = new Button
    {
        Content = "Apply",
        Width = 84f,
        Height = 20f
    }
});

Selection, caret, and document offsets

The editor exposes both a rich document view and a flattened text-offset view. That is deliberate. Many input and command operations need fast integer offsets, while more advanced APIs work with document pointers and ranges.

Offset-based state

CaretIndex, SelectionStart, and SelectionLength are integer offsets over the flattened document text.

Pointer-based state

CaretPosition, Selection, DocumentSelection, and SelectionRange expose richer document-aware views.

Select by offset

editor.Select(12, 5);

Console.WriteLine(editor.SelectionStart);   // 12
Console.WriteLine(editor.SelectionLength);  // 5
Console.WriteLine(editor.CaretIndex);       // 17

Select by TextPointer

var start = DocumentPointers.CreateAtDocumentOffset(editor.Document, 0);
var end = DocumentPointers.CreateAtDocumentOffset(editor.Document, 11);

editor.Select(start, end);

Hit test into the document

TextPointer? pointer = editor.GetPositionFromPoint(mousePosition, snapToText: true);
if (pointer.HasValue)
{
    var offset = DocumentPointers.GetDocumentOffset(pointer.Value);
    Console.WriteLine($"Hit offset: {offset}");
}

Editing model and mutations

Edits are structural, not just textual. Depending on the current content and command, RichTextBox may replace plain text, splice a rich fragment, adjust list depth, split or merge cells, or preserve inline wrappers while replacing only the selected text payload.

  • Typing can preserve active inline style or current paragraph structure.
  • Replacing the whole Document is different from loading into the current selection.
  • Undo and redo operate through the document undo manager, not a plain string buffer.
  • Read-only mode blocks mutations but keeps navigation, selection, scrolling, and hyperlink activation.

Whole-document replacement

var replacement = new FlowDocument();
DocumentEditing.ReplaceAllText(replacement, "New document body");
editor.Document = replacement;

Selection-scoped load

using var stream = new MemoryStream(Encoding.UTF8.GetBytes("Inserted text"));
editor.LoadSelection(stream, "Text");

Whole-document load

using var stream = new MemoryStream(Encoding.UTF8.GetBytes(payload));
editor.Load(stream, "Xaml");

Commands and shortcuts

The control registers a broad routed command surface. That matters because toolbar buttons, keyboard shortcuts, and direct command execution all go through the same behavior gates.

Text editing

Backspace, Delete, paragraph breaks, line breaks, cut, copy, paste, undo, redo, word deletion, and selection commands are all routed.

Rich commands

Formatting, list toggles, indent changes, table commands, page navigation, paragraph navigation, and hyperlink activation are part of the same command system.

Formatting example

CommandManager.Execute(EditingCommands.ToggleBold, null, editor);
CommandManager.Execute(EditingCommands.ToggleItalic, null, editor);
CommandManager.Execute(EditingCommands.ToggleUnderline, null, editor);

List and table example

CommandManager.Execute(EditingCommands.ToggleBullets, null, editor);
CommandManager.Execute(EditingCommands.ToggleNumbering, null, editor);
CommandManager.Execute(EditingCommands.InsertTable, null, editor);
CommandManager.Execute(EditingCommands.SplitCell, null, editor);
CommandManager.Execute(EditingCommands.MergeCells, null, editor);

Navigation example

CommandManager.Execute(EditingCommands.MoveToDocumentStart, null, editor);
CommandManager.Execute(EditingCommands.MoveDownByPage, null, editor);
CommandManager.Execute(EditingCommands.SelectToDocumentEnd, null, editor);

Clipboard and serialization formats

The document model supports both rich and plain interchange. This is one of the most important reasons to keep content as structured blocks and inlines instead of flattening early.

Rich formats

Flow XML, Xaml, XamlPackage, and Rich Text Format preserve more structure and formatting.

Plain format

Text and UnicodeText flatten the document to text while preserving line boundaries as normalized newlines.

Save the whole document

using var stream = new MemoryStream();
editor.Save(stream, FlowDocumentSerializer.ClipboardFormat);

stream.Position = 0;
var xml = new StreamReader(stream).ReadToEnd();

Save just the current selection

using var stream = new MemoryStream();
editor.SaveSelection(stream, "Xaml");

Round-trip plain text

using var stream = new MemoryStream(Encoding.UTF8.GetBytes("alpha\nbeta"));
editor.Load(stream, "Text");

Round-trip RTF

using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"{\rtf1\ansi line1\par line2}"));
editor.Load(stream, "Rich Text Format");

Hyperlinks, automation, and public events

Hyperlinks are document content, not overlay widgets. They participate in selection, read-only activation, and keyboard navigation. The editor also raises routed events for document, text, and selection changes and exposes automation patterns through its peer.

  • DocumentChanged fires when the active document is replaced or the current document mutates.
  • TextChanged tracks text-affecting edits.
  • SelectionChanged tracks caret and selection transitions.
  • HyperlinkNavigate bubbles the target URI when a hyperlink is activated.

Hyperlink event example

editor.HyperlinkNavigate += (_, args) =>
{
    Console.WriteLine($"Navigate: {args.NavigateUri}");
};

Document and selection events

editor.DocumentChanged += (_, _) => Console.WriteLine("Document changed");
editor.TextChanged += (_, _) => Console.WriteLine("Text changed");
editor.SelectionChanged += (_, _) => Console.WriteLine("Selection changed");

Scrolling, layout, and rendering

The control renders the document surface itself. Templates provide outer chrome, but the RichTextBox still owns document measurement, caret drawing, selection geometry, table borders, and hosted child placement.

Measurement

Desired size comes from the laid-out document plus padding and border thickness. With TextWrapping="NoWrap", available text width becomes effectively infinite.

Template contract

The default template expects a PART_ContentHost placeholder so RichTextBox aligns with the other text-input controls while still retaining control-owned rich rendering.

  • Mouse wheel scrolling is internal to the editor surface.
  • Caret visibility can move scroll offsets automatically.
  • Hosted document children are measured and arranged from layout results.
  • Selection and caret are clipped to the computed text rect.

Template example

<Style TargetType="{x:Type RichTextBox}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type RichTextBox}">
        <Border Background="{TemplateBinding Background}"
                BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}">
          <ScrollViewer x:Name="PART_ContentHost"
                        Margin="{TemplateBinding Padding}"
                        Focusable="False" />
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

Practical document recipes

Release notes document

var document = new FlowDocument();

var title = new Paragraph();
var titleBold = new Bold();
titleBold.Inlines.Add(new Run("Release 1.4"));
title.Inlines.Add(titleBold);
document.Blocks.Add(title);

var summary = new Paragraph();
summary.Inlines.Add(new Run("This release adds list toggles, table editing, and payload round-trips."));
document.Blocks.Add(summary);

var bullets = new InkkSlinger.List();
bullets.Items.Add(CreateItem("Toggle bullets and numbering from the toolbar."));
bullets.Items.Add(CreateItem("Export selections as XAML or Flow XML."));
bullets.Items.Add(CreateItem("Preserve hyperlinks and inline formatting."));
document.Blocks.Add(bullets);

editor.Document = document;

Status dashboard document

var document = new FlowDocument();
document.Blocks.Add(CreateParagraph("Service dashboard"));
document.Blocks.Add(CreateStatusTable(
    ("API", "Healthy"),
    ("Jobs", "Running"),
    ("Queue", "12 pending")));

static Paragraph CreateParagraph(string text)
{
    var paragraph = new Paragraph();
    paragraph.Inlines.Add(new Run(text));
    return paragraph;
}

static Table CreateStatusTable(params (string Left, string Right)[] rows)
{
    var table = new Table();
    var group = new TableRowGroup();
    foreach (var (left, right) in rows)
    {
        var row = new TableRow();
        row.Cells.Add(CreateCell(left));
        row.Cells.Add(CreateCell(right));
        group.Rows.Add(row);
    }

    table.RowGroups.Add(group);
    return table;
}

Embedded action document

var button = new Button
{
    Content = "Approve",
    Width = 84f,
    Height = 20f
};

button.Click += (_, _) => Console.WriteLine("Approved");

var paragraph = new Paragraph();
paragraph.Inlines.Add(new Run("Current step: "));
paragraph.Inlines.Add(new InlineUIContainer { Child = button });

var document = new FlowDocument();
document.Blocks.Add(paragraph);
editor.Document = document;

Rules and pitfalls

Think in structure first. If the content needs lists, links, tables, or inline styling, build and transform the document tree directly instead of reconstructing it from plain strings later.

The document is the source of truth. There is no simple Text dependency property. Plain text can be derived from the document, but the editor is not fundamentally string-based.

Whole-document and selection operations are different. Load replaces the document. LoadSelection replaces only the current selection and may preserve more surrounding structure.

Template chrome is not rendering ownership. Even with a PART_ContentHost template, RichTextBox still owns rich layout, scrolling, selection, caret, and hosted child placement.

Read-only is still interactive. It blocks mutation, but selection, scrolling, and hyperlink navigation remain live.