Avalonia.Controls.Documents 12.2.0

Prefix Reserved
dotnet add package Avalonia.Controls.Documents --version 12.2.0
                    
NuGet\Install-Package Avalonia.Controls.Documents -Version 12.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Avalonia.Controls.Documents" Version="12.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Avalonia.Controls.Documents" Version="12.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Avalonia.Controls.Documents" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Avalonia.Controls.Documents --version 12.2.0
                    
#r "nuget: Avalonia.Controls.Documents, 12.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Avalonia.Controls.Documents@12.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Avalonia.Controls.Documents&version=12.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Avalonia.Controls.Documents&version=12.2.0
                    
Install as a Cake Tool

Avalonia.Controls.Documents

Core document model library for Avalonia rich text controls. Provides rope-based text storage, a hierarchical document tree, text pointers, undo/redo, and snapshotting.

Features

  • Rope-based text storage (RopeTextStore) with O(log n) insert and delete
  • Document tree model (TextDocument, TextDocumentNode) for structural editing
  • Text pointer API (TextPointer, TextRange) with generation-based invalidation
  • Undo/Redo (UndoManager) with structural snapshot support
  • FlowDocument model with blocks (paragraphs, lists, tables, sections) and inlines
  • Document snapshots for thread-safe background serialization

Quick Start

var document = new FlowDocument();
var paragraph = new Paragraph();
paragraph.Inlines.Add(new Run("Hello, world!"));
document.Blocks.Add(paragraph);

Documentation

See the documentation for more details.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (6)

Showing the top 5 NuGet packages that depend on Avalonia.Controls.Documents:

Package Downloads
Avalonia.Controls.Markdown

Markdown rendering control for Avalonia applications

Avalonia.Controls.Documents.Serialization.Html

HTML serializer for Avalonia.Controls.Documents

Avalonia.Controls.Documents.Serialization.Rtf

RTF serializer for Avalonia.Controls.Documents with high-fidelity import and export support

Avalonia.Controls.RichTextEditor

Interactive rich text editor control for Avalonia with selection, formatting, and undo/redo support

Avalonia.Controls.Documents.Serialization.Docx

DOCX serializer for Avalonia.Controls.Documents based on Open XML

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
12.2.0 150 7/23/2026
12.1.5 8,721 6/29/2026
12.1.4 2,570 6/22/2026
12.1.3 743 6/9/2026
12.1.2 779 6/2/2026
12.1.1 1,174 5/14/2026
12.1.0 648 5/12/2026

## 12.2.0

### Features
- Inline images: new `RichImage` inline element whose content is a `RichImageSource` (display size in DIPs — intrinsic when unset — and alt text). It occupies a single object-replacement character and renders in text flow (decode-on-measure, intrinsic or explicit sizing, placeholder on decode failure).
- `RichImageSource` abstraction — the single source of truth for an image's content, rendering, and capture: `EmbeddedImageSource` (encoded bytes; decodes once and owns its bitmap, `IDisposable`), `DeferredImageSource` (URI resolved on demand), and `PixelImageSource` (raw pixels). `PixelHolder` is the immutable, thread-safe pixel-buffer type behind capture and transcoding.
- XAML authoring of image sources: a `RichImageSourceConverter` (`TypeConverter` on `RichImageSource`) lets `RichImage.Source` accept a URI string directly — absolute (`Source="avares://App/Assets/pic.png"`) or relative to the XAML base URI (`Source="/Assets/pic.png"`, resolved like `Image.Source`/`FontFamily`). Local (`avares:`/`file:`) sources embed the bytes; `http(s):` records the URL as a `DeferredImageSource`.
- Automatic image transcoding: formats a target can't embed (e.g. WebP/GIF/TIFF when writing RTF, or WebP when writing DOCX) are re-encoded to PNG by a built-in codec, so images are no longer silently dropped. `DocumentSerializerOptions.ImageEncodingPolicy` selects re-encode-from-pixels (default) vs. preserve-encoded-only.
- `TextRange.InsertImage(data, mimeType, width, height, altText)` and `DocumentSnapshotBuilder.AddImage(...)` / `ImageInlineDraft` for programmatic and serializer-driven insertion; the editor's insert action sniffs MIME from the bytes with a file-extension fallback.
- `DocumentSerializerOptions.MaxImageBytes` caps the decoded size of a single embedded image accepted on read (oversized images are skipped; the document still loads).
- Right-clicking in a text view now moves the caret to the pressed position (a press inside the current selection keeps the selection), so the context menu opens with the block/table/list/image context under the pointer instead of the stale caret context.
- `TableCell.VerticalAlignment` (Top by default, plus Center and Bottom) aligns a cell's content within the row height, matching Word's `w:vAlign`. It is carried through `BlockFormatting.VerticalAlignment` and set on the snapshot via `DocumentSnapshotBuilder.SetTableCellVerticalAlignment`.
- `TableRow.Height` sets a row's minimum height in device-independent pixels (Word's `w:trHeight`); zero (the default) sizes the row to its content. It is carried through `BlockFormatting.MinHeight` and set on the snapshot via `DocumentSnapshotBuilder.SetTableRowHeight`.
- `Table.InsideBorderThickness` and `Table.InsideBorderBrush` describe the interior gridlines drawn between cells (Word's `w:insideH` / `w:insideV`). They are carried through `TableFormatting.InsideBorderWidth` / `InsideBorderArgb` and set on the snapshot via `DocumentSnapshotBuilder.SetTableInteriorBorders`.
- A table cell's column and row span now survive the snapshot. `BlockFormatting.ColumnSpan` and `BlockFormatting.RowSpan` carry `TableCell.ColumnSpan` / `RowSpan` (Word's `w:gridSpan` / `w:vMerge`) through capture and restore, and `DocumentSnapshotBuilder.SetTableCellSpan(int columnSpan, int rowSpan)` sets them on the snapshot. The span properties already existed on `TableCell` but were dropped by the snapshot, so a merged cell lost its span on the first undo, save, or reload; they now round-trip. This is the plumbing that full merged-cell import and rendering builds on.
- `ListFormatting.MarkerOffset` and `DocumentSnapshotBuilder.SetListMarkerOffset(double)` carry a list's hanging indent (how far the marker sits back from the content) through the snapshot, so importers can place it on the `List` rather than on each item.
- `List.MarkerAlignment` (Left by default, or Right) controls how the marker aligns within its column, matching Word's `w:lvlJc`. It is carried through `ListFormatting.MarkerAlignment` and set on the snapshot via `DocumentSnapshotBuilder.SetListMarkerAlignment`.

### Fixes
- Plain-text serialization no longer emits the U+FFFC object-replacement character for embedded objects (images, inline UI containers, Markdown images).
- Undo/redo of a structural snapshot paste now restores the document exactly and rebuilds grafted structure (bold runs, images) on redo, instead of drifting container lengths on undo and dropping structure on redo.
- A table outer edge declared by its cells rather than by the table now reserves layout space, so it is no longer painted outside the table and clipped away. The grid pass resolves every perimeter edge from the table's own `BorderThickness` or the border of the cells sitting on that edge, whichever is thicker, and draws the stroke just outside the content; but only the table's own border was reserved during measure, so a perimeter that came from the cells had no band to land in and went a border's width outside the table's box, which is invisible as soon as the table sits flush against the edge of the view. A markdown table lost its left border for exactly this reason: the theme gives each cell the left and top (`1,1,0,0`) and the table only the outer right and bottom (`0,0,1,1`), and a full-width table starts at x=0. Measure and arrange now reserve the same merged perimeter the grid draws, so it makes no difference whether an edge is declared on the table or on its cells. A table grows by up to a border's width on any side whose edge came only from its cells.
- A table cell boxed in its own border colour now keeps that colour on every side. Word writes a border onto both cells that share a gridline, so a boxed cell leaves each neighbour restating one edge at the same thickness; a cell carries a single brush, so a neighbour whose other three sides are the plain grid resolves its brush to the grid colour. The two cells then tied on thickness and the first one won, repainting the box's top and left edges in the grid colour while its right and bottom stayed correct. An equal-thickness edge now takes its colour from the cell whose border is uniform on all four sides, since that is the deliberate box; ties between two uniform (or two mixed) borders resolve as before.
- Deleting all content of a paragraph no longer loses its formatting. Holding Backspace until a line (or the whole document) was empty detached every inline formatting carrier with the deleted text, so the toolbar fell back to the theme font and the next typed character came out in it: type a few lines in Arial 12, delete them, and the editor reverted to the default font. A delete that empties a paragraph now stamps the effective inline formatting of the deleted content onto the surviving empty paragraph (Word's paragraph-mark semantics; an empty carrier run holds inline-only formatting such as superscript), where the toolbar, the formatting toggles, and the next typed character all read it. Undo fidelity improves with it: an emptying delete now records a structural snapshot, so undoing it restores the original runs and spans instead of re-inserting bare text stripped of its formatting. Deleting the exactly-selected content of the document's only paragraph keeps the formatted paragraph, and a selection delete that consumes every block recreates the implicit paragraph carrying the formatting when the deleted content had any (unformatted clears keep the historical block-less result).
- Applying formatting at a collapsed caret is no longer a silent no-op, and formatting chosen in an empty paragraph now sticks. `TextSelection.ApplyPropertyValue` on an empty selection previously did nothing; it now sets the format of the next typed character, written durably onto the paragraph element when the caret's paragraph is empty, so it survives selection changes (clicking into the editor between choosing a font and typing no longer discards the choice) and a document with no blocks materializes its implicit paragraph first. At a caret in non-empty content the value is springloaded as before. Formatting toggles at an empty-paragraph caret write to the paragraph the same way.
- Undoing an inline formatting change no longer blanks the affected content when it lives inside a list. The formatting undo restores the whole containing top-level block from a snapshot; for a selection inside list items that block is the List, and the restore rebuilt its items without telling the rendering layer, so the realized list visuals kept painting from the old, detached nodes: the text looked deleted although the document still held it, and the next layout walk tripped a detached-node assertion. The restore now bumps the document generation and raises the structural-content notification, the same contract the structural undo path already followed, so pointers re-resolve and the visuals re-sync.
- Redoing a structural table or list edit no longer silently does nothing when the edited container sits at the start of the document. Undoing such an edit rebuilds the container's content and detaches the old subtree, but the undo unit re-found its container through a stored position whose cached leaf lived in that detached subtree; a detached node still reports subtree-local offsets, so whenever the container's content began at offset 0 the position re-anchored into the discarded tree and the redo restored nothing — leaving the undo and redo stacks out of sync with the document. This hit inserting a table column or row, deleting a row, wrapping paragraphs in a list inside a table cell, and any other edit whose undo scope is a table, row group, or cell at the very start of the document. Structural undo units now re-locate their container by node identity first (the container node survives its own restores) and fall back to the recorded position only after an outer restore has replaced the container itself, re-anchoring after every replay. Independently, re-resolving a `TextPointer` against a detached cached leaf (`Refresh`, `UpdateInPlace`, and the hint-accelerated pointer factories) now falls back to a full walk from the document root as the hint contract always promised, which protects any code that holds a pointer across a structural undo. The materializing pointer APIs (`GetContainingElement`, `EnsureElement`, `EnsureParentElement`) also re-sync a stale pointer before dereferencing its leaf, so a selection-changed handler that runs right after a paste or undo has replaced the surrounding blocks reads the live block instead of materializing an orphaned element from the replaced subtree.
- A table now draws its whole grid in one pass instead of each cell painting its own border. Every gridline, interior and perimeter, is resolved from the two cells that meet at that edge: a cell's own border side wins, otherwise the table's interior grid fills an interior edge and its outer border fills the perimeter. This fixes three problems at once with partially bordered tables (such as a Word table where a few cells carry a coloured border on one side against the style's grid): a cell that set a border on only one side no longer loses the grid on its other sides, the strokes are centred on the gridline so the corners join, and a shared edge is drawn once instead of by both neighbouring cells (which had doubled its width). Gridlines are snapped to the device-pixel grid, so a thin stroke lands on one crisp row of pixels instead of blurring across two.
- Gridline segments now end flush on the line they meet instead of overhanging it. Every segment used to extend a raw half thickness past its boundary to close the corner joins, but the crossing line's snapped pixels can sit up to half a device pixel short of that overhang (rounding to even pulls a 1px line's span below an odd boundary), so the excess painted a half-intensity pixel beyond the crossing line — visible as small nubs poking into merged cells and past the table's outer edge, on exactly those boundaries whose parity lost the rounding. A segment end now reaches to the crossing line's snapped far edge (which also fills the corner pixel completely) and, where nothing crosses, stops on the device-snapped boundary itself.
- A shaded row or a coloured cell no longer paints over the gridlines that border it. Cell, row, and row-group backgrounds were drawn by the child nodes, which render above the table, so a fill covered any gridline it reached — most visibly the table's right and bottom outer border where a shaded header row or a filled last row met the edge, which looked like the border being clipped. The table now paints every background itself, beneath the grid, so the lines always sit on top of the fills.
- The table's outer border now lines up flush with the cell fills on every side. The right and bottom strokes were centred on the content edge, where the device-pixel rounding pulled them a pixel inward (the left and top happen to round the other way), so a coloured cell or shaded row looked like it extended a pixel past the border. Perimeter strokes are now placed in the border band just outside the content and snapped to the pixel grid, so all four sides align with the fills.
- A table cell's `ColumnSpan` is now laid out correctly. The row used a single index as both the cell and the grid column, so after a spanning cell every following cell took the wrong column's width and shifted one column left, and the last column was dropped when a row had fewer cells than columns. Cells are now placed with a column cursor that advances by each cell's span, so a merged cell covers its columns and the rest of the row stays aligned; the inferred column count also counts spans.
- A table cell's `RowSpan` (vertical merge) is now honoured. The table computes a logical grid — each cell walked into the slots it spans, skipping any slot a rowspan from an earlier row already occupies — so a merged cell takes the combined height of the rows it covers and reaches down into the gap the rows below leave for it, and those rows place their own cells in the remaining columns. The grid is drawn from that logical layout: a boundary inside a spanning cell (the same cell on both sides) is not drawn, so a rowspan reads as one tall cell and a colspan as one wide cell, and each border resolves from the cells that actually meet at the edge by grid column rather than by position.
- A row-spanning cell taller than the rows it covers now spreads its height across them instead of piling it all into its first row. The cell used to be charged entirely to its start row, so a tall merged cell made that one row swallow the height (stretching its other cells) while the rows below stayed short. A row's height now comes from its single-row cells, and the table grows the spanned rows evenly when the merged cell needs more room. Tables without row spans, and merged cells that already fit their rows, lay out exactly as before.
- Table rows now honour a minimum height, so a table sized to fixed row heights keeps its proportions instead of collapsing every row to its content. This also gives cell vertical alignment room to act — Center and Bottom cells had no visible effect when the row was only as tall as its content.
- A table cell now honours its vertical alignment. Because a cell consumes the full row height, content shorter than the row was always pinned to the top; Center and Bottom now offset the stacked content by half or all of the leftover height.
- Copy and cut now preserve the inline formatting and block structure of a partial selection. A selection inside a single paragraph previously lost its bold, italic, hyperlink, and other inline runs, so pasting (even through the in-process clipboard fast path) produced unformatted default text. A selection that started in one list item or table cell and ended in another was flattened into a single paragraph that glued the two fragments together with no separator, so copying across the boundary of "Item one" and "Item two" yielded "oneItem". Partially covered boundary blocks are now captured by trimming the covered slice through the same node-kind path used for whole-block capture, so runs, hyperlinks, and the enclosing list/table structure survive into the snapshot and the plain-text lowering keeps its separators.
- Copying a fully selected block that carries serializer round-trip metadata or custom node data now keeps both. Blocks loaded from a format such as RTF or Markdown but never realized (off-screen content in a viewer) previously lost their format metadata and custom data when the range snapshot re-based the node, so an in-process copy and paste of, for example, a Markdown task-list item or fenced code block dropped that state.
- A run draft's format metadata now survives into the snapshot. `DocumentSnapshotBuilder` dropped the `FormatMetadata` of a `RunInlineDraft` when building its run node — only line breaks kept theirs — so a serializer round-trip hint attached to an imported run (for example the DOCX highlight colour) never reached the node the writer reads.
- Fixed stale offset caches when blocks are removed inside an already-open structural change. Clearing a `Blocks` or `Inlines` collection, pasting content that replaces existing blocks, and applying a Markdown streaming delta all remove children within an outer scope; that path skipped the generation bump, so cached `ContentStart`, `ContentEnd`, and `ContentRange` offsets could report stale positions to later operations in the same scope, to undo capture, and to re-entrant callbacks. The bump is now unconditional.
- The public `Changed` event is now delivered safely. A handler that throws no longer drops the deferred paragraph render invalidations that follow the event (they are flushed in a finally block), so one failing subscriber can no longer leave the view visually stale until an unrelated edit. Re-entrant edits made from within a change notification no longer throw during the deferred-notification flush.
- List markers now take their font size, family, colour, and baseline from the first line's text, matching Word and browsers. Enlarging or colouring list text through an inline run (rather than the block) now updates the marker instead of leaving it at the document default, and a text marker sits on the first line's baseline rather than floating at the line-box centre, which drifts as the font grows.
- Built-in bullet markers (disc, circle, square, box) now render as synthesized shapes sized to the item's first-run font instead of Unicode glyphs chosen from a per-nesting-level table. A deeply nested bullet no longer picks up an oversized large-circle glyph that filled the em box; every bullet is a small, consistent fraction of the text at any nesting level and takes the first run's colour. The plain-text representation of a bullet is unchanged.
- A list marker now follows its paragraph's horizontal alignment. A centered or right-aligned bulleted item places the marker just left of where the text begins on the first line, matching Word, instead of stranding it at the left margin. Left-aligned and justified items are unchanged.
- A wide list marker no longer touches or overlaps the text when the list's hanging indent is narrower than the marker. Word's deepest roman-numeral level, for example, imports a hanging indent of only a few points, which a marker like "iii." or "iv." exceeds at the document font, leaving the number jammed against the text. The list now grows its shared hanging indent to keep a small gap between the widest marker and the content, so markers stay clear of the text at every nesting level.
- Numbered list markers now right-align by default: their numbers share a right edge with a consistent gap to the text (the conventional look), so a wider marker like "iii." or "10." extends left into the indent instead of overrunning the content or staggering. Word's marker alignment (`w:lvlJc`) is honoured when a document specifies it, and a caller can set `List.MarkerAlignment` explicitly. Bullets are unchanged — a single glyph stays left-aligned at the marker position.
- List indentation is now a box model owned entirely by the `List`: its left padding is the shared content indent (the marker column) and `List.MarkerOffset` is the hanging indent that positions the marker. A `ListItem` no longer carries the per-level indent, so a deeply nested list no longer double-counts it — an item margin plus a separately computed column had pushed the deepest content off the page or collapsed it to zero width. The marker hangs back from the content at its glyph origin, matching where Word starts a list marker (content minus the hanging).
- Outdenting a list item no longer throws `ArgumentOutOfRangeException`. When the item was the only entry of a nested list whose containing item had no other content (a state reachable by deleting and re-adding bullets at the deepest indent, since Backspace at the start of such an item outdents it), the operation captured the destination index and then cascade-removed the emptied ancestors before re-inserting, so the insertion ran past the end of the enclosing list — and could even remove that list out from under the item. The outdented item is now re-inserted before the cleanup, which keeps the index in range and keeps the enclosing list in place.