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.
Runstores literal text.Bold,Italic, andUnderlinewrap nested inline collections.Hyperlinkcombines text with navigation or command activation.LineBreakinserts a soft line transition inside a paragraph without creating a new block.InlineUIContainerreserves inline space for a hostedUIElement.
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
Documentis 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.
DocumentChangedfires when the active document is replaced or the current document mutates.TextChangedtracks text-affecting edits.SelectionChangedtracks caret and selection transitions.HyperlinkNavigatebubbles 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.