Skip to main content

Document Viewer

Use FlowDocumentScrollViewer to display rich documents without editing. This guide covers setup, document loading, styling, layout, and common viewer patterns.

info

This control is available as part of Avalonia Pro or higher.

When to use FlowDocumentScrollViewer

Two controls can host a FlowDocument:

ControlPurposeSelection / CopyCaretUndoOverhead
FlowDocumentScrollViewerRead-only display with text selection / copyYesNoNoLow
RichTextEditorInteractive editingYesYesYesHigher

Use FlowDocumentScrollViewer for help panes, report previews, file browsers, and read-only summaries. It supports text selection and clipboard copy out of the box (powered by the same TextViewMouse / TextViewKeyboard components used by the editor) but exposes no insertion caret, no editing actions, and no undo manager.

Use RichTextEditor with IsReadOnly="True" only when you need an insertion caret on otherwise-read-only content (e.g. for placing a cursor without allowing edits). This pulls in the full editing infrastructure (caret element, undo manager, editing components).

Installation

# Core package (includes FlowDocument, FlowDocumentScrollViewer, PlainTextSerializer)
dotnet add package Avalonia.Controls.RichTextEditor

# Add serializers for the formats you need
dotnet add package Avalonia.Controls.Documents.Serialization.Rtf # RTF
dotnet add package Avalonia.Controls.Documents.Serialization.Docx # DOCX (Open XML)
dotnet add package Avalonia.Controls.Documents.Serialization.Xaml # XAML round-trip

All document types (FlowDocument, Paragraph, RichRun, etc.) and FlowDocumentScrollViewer are mapped to the default Avalonia XML namespace (https://github.com/avaloniaui). No extra xmlns declarations are needed.

Minimal XAML example

FlowDocument is the [Content] property of FlowDocumentScrollViewer, so it can be written directly as a child element:

<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Document Viewer" Width="800" Height="600">

<FlowDocumentScrollViewer Padding="20">
<FlowDocument FontSize="14">
<Paragraph FontSize="24" FontWeight="Bold">
<RichRun Text="Welcome" />
</Paragraph>
<Paragraph>
<RichRun Text="This document is displayed in a read-only viewer." />
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>

</Window>

The viewer wraps a virtualized TextViewBase inside a ScrollViewer. Vertical scrolling is enabled by default; horizontal scrolling is disabled.

Loading documents from files

Async loading (preferred)

Use FlowDocument.LoadAsync to deserialize a file and assign the result to the viewer:

await using var stream = File.OpenRead("report.rtf");
var document = await FlowDocument.LoadAsync(stream, new RtfSerializer());
viewer.Document = document;

LoadAsync performs deserialization off the UI thread. The returned FlowDocument is ready to display immediately.

Synchronous loading

using var stream = File.OpenRead("report.rtf");
viewer.Document = FlowDocument.Load(stream, new RtfSerializer());

Prefer async loading for large files to avoid blocking the UI thread.

Choosing a serializer

Pick a serializer based on the file format:

ExtensionSerializerPackage
.rtfRtfSerializerAvalonia.Controls.Documents.Serialization.Rtf
.docxDocxSerializerAvalonia.Controls.Documents.Serialization.Docx
.xaml / .axamlXamlSerializerAvalonia.Controls.Documents.Serialization.Xaml
.txtPlainTextSerializerCore (included)

A helper method that maps extensions to serializers:

static IDocumentSerializer GetSerializer(string path)
{
return Path.GetExtension(path).ToLowerInvariant() switch
{
".rtf" => new RtfSerializer(),
".docx" => new DocxSerializer(),
".xaml" or ".axaml" => new XamlSerializer(),
_ => new PlainTextSerializer()
};
}

Loading from embedded resources

Use Avalonia's AssetLoader to open a stream from an assembly resource. Use a .xml extension for FlowDocument data files — .axaml and .xaml extensions trigger Avalonia's XAML compiler, which cannot compile FlowDocument root elements.

var uri = new Uri("avares://MyApp/Assets/Help.xml");
using var stream = AssetLoader.Open(uri);
viewer.Document = FlowDocument.Load(stream, new XamlSerializer());

Loading from a byte array

using var stream = new MemoryStream(rtfBytes);
viewer.Document = await FlowDocument.LoadAsync(stream, new RtfSerializer());

Building documents in code

Manual construction

var document = new FlowDocument();

// Heading
var heading = new Paragraph
{
FontSize = 24,
FontWeight = FontWeight.Bold,
Margin = new Thickness(0, 0, 0, 10)
};
heading.Inlines.Add(new RichRun { Text = "Report Title" });
document.Blocks.Add(heading);

// Body paragraph with mixed formatting
var body = new Paragraph();
body.Inlines.Add(new RichRun { Text = "Status: " });
body.Inlines.Add(new RichBold(new RichRun { Text = "Complete" }));
body.Inlines.Add(new RichRun { Text = ". See " });
body.Inlines.Add(new RichHyperlink(new RichRun { Text = "details" })
{
NavigateUri = new Uri("https://example.com")
});
body.Inlines.Add(new RichRun { Text = " for more information." });
document.Blocks.Add(body);

viewer.Document = document;

FlowDocumentBuilder (fluent API)

FlowDocumentBuilder provides a concise fluent interface for building documents:

using Avalonia.Controls.Documents.TextModel;

var document = FlowDocumentBuilder.Create()
.AddParagraph("Report Title")
.AddParagraph()
.AddText("Body text with ")
.AddBold("bold")
.AddText(" and ")
.AddItalic("italic")
.AddText(" formatting.")
.Build();

viewer.Document = document;

The builder supports lists and tables as well:

var document = FlowDocumentBuilder.Create()
.AddParagraph("Shopping List")
.StartList(TextMarkerStyle.Disc)
.AddListItem("Apples")
.AddListItem("Bread")
.AddListItem("Milk")
.EndList()
.AddParagraph("Price Table")
.StartTable()
.SetTableColumns(new double[] { 200, 100 })
.StartTableRow()
.AddTableCell("Item")
.AddTableCell("Price")
.EndTableRow()
.StartTableRow()
.AddTableCell("Apples")
.AddTableCell("$3.00")
.EndTableRow()
.EndTable()
.Build();

The InlineFactory class provides static factory methods for composing inlines:

var doc = FlowDocumentBuilder.Create()
.AddParagraph(
InlineFactory.Text("Normal "),
InlineFactory.Bold("bold "),
InlineFactory.Italic("italic"))
.Build();

The builder is best suited for linear documents. For deeply nested structures (tables within list items, sections with mixed content), manual construction gives more control.

Document structure reference

FlowDocument
├── Paragraph Block containing inline elements
│ ├── RichRun Text with uniform formatting
│ ├── RichBold Bold wrapper (RichSpan subclass)
│ ├── RichItalic Italic wrapper
│ ├── RichUnderline Underline wrapper
│ ├── RichSuperscript Superscript positioning
│ ├── RichSubscript Subscript positioning
│ ├── RichSpan Generic inline container
│ ├── RichHyperlink Clickable link (NavigateUri)
│ ├── RichLineBreak Explicit line break
│ └── RichInlineUIContainer Embedded control (inline)
├── Section Groups blocks together
├── List Bulleted or numbered list
│ └── ListItem Contains blocks (Paragraph, nested List, ...)
├── Table Grid layout
│ ├── TableColumn Column width definitions
│ └── TableRowGroup Header/body/footer grouping
│ └── TableRow
│ └── TableCell Contains blocks
└── BlockUIContainer Embedded control (full-width block)

Common block properties

All blocks inherit from Block and share these properties:

PropertyTypeDescription
MarginThicknessOuter spacing
PaddingThicknessInner spacing
BorderThicknessThicknessBorder width
BorderBrushIBrush?Border color
CornerRadiusCornerRadiusRounded corners
TextAlignmentTextAlignmentLeft, Center, Right, Justify (inherited)
LineHeightdoubleLine spacing
FlowDirectionFlowDirectionLTR or RTL (inherited)

Common inline properties

PropertyTypeAvailable On
TextstringRichRun
FontSizedoubleAll inlines (inherited)
FontWeightFontWeightAll inlines (inherited)
FontStyleFontStyleAll inlines (inherited)
FontFamilyFontFamilyAll inlines (inherited)
ForegroundIBrush?All inlines (inherited)
TextDecorationsTextDecorationCollection?All inlines
BaselineAlignmentBaselineAlignmentAll inlines

Styling and theming

Document-level defaults

FlowDocument properties cascade to all child elements:

<FlowDocumentScrollViewer>
<FlowDocument FontFamily="Segoe UI" FontSize="14"
Foreground="#333333" TextAlignment="Left">
<Paragraph>
<RichRun Text="Inherits font and color from FlowDocument." />
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>

Individual elements override the defaults:

<Paragraph FontSize="24" FontWeight="Bold" Foreground="DarkBlue">
<RichRun Text="This heading overrides the document defaults." />
</Paragraph>

Theming with styles

Use Avalonia styles to control the viewer's appearance:

<Window.Styles>
<Style Selector="FlowDocumentScrollViewer">
<Setter Property="Background" Value="{DynamicResource SystemRegionBrush}" />
<Setter Property="Padding" Value="24" />
</Style>
</Window.Styles>

RichHyperlink supports the NavigateUri property and raises a RequestNavigate routed event:

<Paragraph>
<RichRun Text="Visit the " />
<RichHyperlink NavigateUri="https://avaloniaui.net">
<RichRun Text="Avalonia website" />
</RichHyperlink>
<RichRun Text=" for more information." />
</Paragraph>

Handle navigation in code-behind:

viewer.AddHandler(RichHyperlink.RequestNavigateEvent, (sender, e) =>
{
if (e.Uri is { } uri)
{
Process.Start(new ProcessStartInfo(uri.AbsoluteUri) { UseShellExecute = true });
e.Handled = true;
}
});

RichHyperlink exposes :pointerover, :pressed, and :visited pseudo-classes for styling.

Page layout

PageWidth and PagePadding

By default, content fills the available width (PageWidth = NaN). Set a fixed PageWidth to simulate a fixed-width page:

<FlowDocumentScrollViewer>
<FlowDocument PageWidth="700" PagePadding="40">
<Paragraph>
<RichRun Text="This content is constrained to a 700 DIP wide page with 40 DIP padding." />
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>

When PageWidth is set, the page area is centered within the viewer and the background outside the page area remains visible.

ShowPageBounds

Enable ShowPageBounds to render visual indicators at the page boundary. This is useful for print-preview scenarios:

<FlowDocumentScrollViewer ShowPageBounds="True">
<FlowDocument PageWidth="700" PagePadding="40" PageHeight="900">
<!-- Content -->
</FlowDocument>
</FlowDocumentScrollViewer>

Embedding controls

BlockUIContainer

Embed any Avalonia control as a full-width block element:

<FlowDocumentScrollViewer>
<FlowDocument>
<Paragraph FontSize="20" FontWeight="Bold">
<RichRun Text="Monthly Revenue" />
</Paragraph>
<BlockUIContainer>
<Image Source="/Assets/revenue-chart.png" MaxHeight="300"
HorizontalAlignment="Center" />
</BlockUIContainer>
<Paragraph>
<RichRun Text="Figure 1: Revenue trends for the past 12 months." />
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>

In code:

var container = new BlockUIContainer(new Image
{
Source = bitmap,
MaxHeight = 300
});
document.Blocks.Add(container);

RichInlineUIContainer

Embed a small control inline with text:

<Paragraph>
<RichRun Text="Status: " />
<RichInlineUIContainer>
<Border Background="Green" CornerRadius="4" Padding="4,2">
<TextBlock Text="Active" Foreground="White" FontSize="11" />
</Border>
</RichInlineUIContainer>
<RichRun Text=" since January 2026." />
</Paragraph>
note

Embedded controls are live Avalonia controls. They participate in layout and rendering but are not captured in serialization snapshots.

Background loading and thread safety

Safe async pattern

FlowDocument.LoadAsync deserializes on a background thread and returns a document ready for UI-thread assignment:

async Task LoadDocumentAsync(string path)
{
IDocumentSerializer serializer = GetSerializer(path);

await using var stream = File.OpenRead(path);
viewer.Document = await FlowDocument.LoadAsync(stream, serializer);
}

Snapshot-based workflows

For conversion pipelines (load, display, re-export), call CreateSnapshot() once and share the result across operations. Snapshots are immutable and safe to use from any thread:

// UI thread: take a snapshot
var snapshot = viewer.Document.CreateSnapshot();

// Background thread: serialize to multiple formats from one snapshot
await Task.Run(async () =>
{
await using var rtfStream = File.Create("output.rtf");
await new RtfSerializer().SerializeAsync(snapshot, rtfStream);

await using var docxStream = File.Create("output.docx");
await new DocxSerializer().SerializeAsync(snapshot, docxStream);
});

SaveAsync is a convenience wrapper that creates a snapshot and serializes in one call. Use CreateSnapshot() directly when you need to serialize to multiple formats from the same document state.

For a detailed discussion of threading constraints, see the Thread Safety guide.

Performance considerations

Virtualization

FlowDocumentScrollViewer virtualizes rendering through its inner TextViewBase:

  • Only blocks within the viewport plus a buffer zone are realized and measured.
  • Unrealized blocks use an estimated height that starts at 24 DIPs and adapts dynamically as blocks are measured. The estimate is a running average of all measured block heights.
  • As the user scrolls, estimates are replaced by actual measurements. This can cause minor scroll-position adjustments on first scroll through unseen content.

This means documents with thousands of blocks remain responsive — rendering cost is proportional to visible content, not total document size.

Large documents

For documents with many blocks:

  • Use FlowDocument.LoadAsync to avoid blocking the UI thread during deserialization.
  • Avoid PageWidth values significantly wider than the viewport. Wider pages produce longer text lines, increasing line-breaking and rendering work.
  • If loading user-provided files, validate file size before opening.

Reuse snapshots

When a document is used in a preview-then-export pipeline, create a single DocumentSnapshot with CreateSnapshot() and reuse it. Each call traverses the document tree (O(n) for structure). One snapshot can be serialized to multiple formats without redundant tree walks.

For more optimization techniques, see the Performance Tuning guide.

Common patterns

File preview pane

A file browser that previews documents as the user selects files. Cancel in-flight loads when the selection changes:

public partial class FilePreviewPane : UserControl
{
private CancellationTokenSource? _loadCts;

public async Task PreviewFileAsync(string path)
{
// Cancel any previous load
_loadCts?.Cancel();
_loadCts = new CancellationTokenSource();
var token = _loadCts.Token;

try
{
IDocumentSerializer serializer = GetSerializer(path);
await using var stream = File.OpenRead(path);
var document = await FlowDocument.LoadAsync(stream, serializer, token);

token.ThrowIfCancellationRequested();
Viewer.Document = document;
}
catch (OperationCanceledException)
{
// Selection changed before load completed — expected
}
}
}

Help / about viewer

Load a static XAML document from an embedded resource:

public partial class HelpWindow : Window
{
public HelpWindow()
{
InitializeComponent();

var uri = new Uri("avares://MyApp/Assets/Help.xml");
using var stream = AssetLoader.Open(uri);
HelpViewer.Document = FlowDocument.Load(stream, new XamlSerializer());
}
}
<!-- HelpWindow.axaml -->
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyApp.HelpWindow"
Title="Help" Width="600" Height="500">
<FlowDocumentScrollViewer x:Name="HelpViewer" Padding="20" />
</Window>

Combine ShowPageBounds, a fixed PageWidth, and PageHeight to simulate a printed page:

<FlowDocumentScrollViewer ShowPageBounds="True"
Background="#F0F0F0"
Padding="40">
<FlowDocument PageWidth="612" PageHeight="792" PagePadding="72">
<!-- US Letter: 612 × 792 DIPs at 96 DPI = 8.5 × 11 inches -->
<!-- 72 DIP padding = 0.75 inch margins -->
<Paragraph FontSize="20" FontWeight="Bold">
<RichRun Text="Quarterly Report" />
</Paragraph>
<Paragraph>
<RichRun Text="Content laid out within print margins." />
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>

Dynamic report generation

Generate a report from a data model and display it:

FlowDocument BuildReport(IReadOnlyList<SalesRecord> records)
{
var builder = FlowDocumentBuilder.Create()
.AddParagraph("Sales Report");

builder.StartTable()
.SetTableColumns(new double[] { 200, 120, 120 })
.StartTableRow()
.AddTableCell("Product")
.AddTableCell("Quantity")
.AddTableCell("Revenue")
.EndTableRow();

foreach (var record in records)
{
builder.StartTableRow()
.AddTableCell(record.Product)
.AddTableCell(record.Quantity.ToString())
.AddTableCell(record.Revenue.ToString("C"))
.EndTableRow();
}

builder.EndTable();

builder.AddParagraph()
.AddText("Total revenue: ")
.AddBold(records.Sum(r => r.Revenue).ToString("C"));

return builder.Build();
}

// Usage
viewer.Document = BuildReport(salesData);

Limitations

Current limitations of FlowDocumentScrollViewer:

LimitationWorkaround
No built-in search/findImplement search against document text and scroll programmatically
No page-break renderingContinuous scroll only; ShowPageBounds shows boundaries visually
Embedded controls not serializedBlockUIContainer / RichInlineUIContainer children are excluded from snapshots
ITextView not publicly exposedThe TextView property on FlowDocumentScrollViewer is internal