Layout Serialization

AvalonDock can save and restore complex docking layouts, including panel positions, sizes, floating windows, and auto-hide state. This lets users arrange their workspace once and have it restored on the next launch.


Choose a Serializer

Format Package Class Best For
XML Dirkster.AvalonDock.Serializer.Xml XmlLayoutSerializer Human-readable configs, legacy compatibility
JSON Dirkster.AvalonDock.Serializer.Json JsonLayoutSerializer Modern apps, smaller file sizes
# Install one or both
dotnet add package Dirkster.AvalonDock.Serializer.Xml
dotnet add package Dirkster.AvalonDock.Serializer.Json

Save a Layout

XML

using AvalonDock.Serializer.Xml;

var serializer = new XmlLayoutSerializer(dockManager);

using (var writer = new StreamWriter("layout.xml"))
{
    serializer.Serialize(writer);
}

JSON

using AvalonDock.Serializer.Json;

var serializer = new JsonLayoutSerializer(dockManager);

using (var writer = new StreamWriter("layout.json"))
{
    serializer.Serialize(writer);
}

Restore a Layout

XML

var serializer = new XmlLayoutSerializer(dockManager);

// Handle content that needs to be recreated
serializer.LayoutSerializationCallback += (sender, args) =>
{
    // Match serialized content by ContentId
    args.Content = args.Model.ContentId switch
    {
        "explorer"   => new ExplorerControl(),
        "properties" => new PropertiesControl(),
        "output"     => new OutputControl(),
        _            => null  // Skip unknown content
    };
};

using (var reader = new StreamReader("layout.xml"))
{
    serializer.Deserialize(reader);
}

JSON

var serializer = new JsonLayoutSerializer(dockManager);

serializer.LayoutSerializationCallback += (sender, args) =>
{
    args.Content = ResolveContent(args.Model.ContentId);
};

using (var reader = new StreamReader("layout.json"))
{
    serializer.Deserialize(reader);
}

LayoutSerializationCallback

The LayoutSerializationCallback is critical for restoring layouts. When a layout is deserialized, AvalonDock recreates the layout structure but needs you to provide the actual content (UI controls or view models) for each content item.

The callback receives a LayoutSerializationCallbackEventArgs with:

Property Type Description
Model ISerializableLayoutContent The layout model being deserialized. Use ContentId to identify it.
Content object Set this to the UI content or view model. Leave it null to skip the item.
Cancel bool Set to true to drop this item entirely.
UnresolvedContentHandling UnresolvedContentHandling What happens to this item when no content is supplied: Remove (default) drops it, Hide parks it in LayoutRoot.Hidden so content supplied later can restore it in place.

A parked anchorable comes back with LayoutAnchorable.Show() once its content arrives — the usual case for a tool window that lives in a plugin loaded after the layout was restored:

var parked = dockingManager.Layout.Hidden.Single(a => a.ContentId == "pluginTool");
parked.Content = plugin.CreateView();
parked.Show();

Show() puts it back in the pane it was hidden from. If that pane is no longer part of the layout — a restored layout rebuilds the docked area from scratch — it docks to an existing tool window pane instead, creating one if the layout has none.


MVVM Serialization

When using MVVM with DocumentsSource and AnchorablesSource, the serialization callback should return view models instead of controls:

serializer.LayoutSerializationCallback += (sender, args) =>
{
    // Find or create the view model
    var vm = _viewModels.FirstOrDefault(v => v.ContentId == args.Model.ContentId);

    if (vm != null)
    {
        args.Content = vm;
    }
    else
    {
        args.Cancel = true; // Skip items that no longer exist
    }
};

What Gets Serialized

The serializer preserves:

  • ✅ Panel positions and orientations
  • ✅ Tab order within panes
  • ✅ Panel sizes (DockWidth, DockHeight)
  • ✅ Floating window positions and sizes
  • ✅ Auto-hide state and side placement
  • ✅ Active/selected state
  • ContentId for each content item
  • Title and other metadata

The serializer does not preserve:

  • ❌ Actual UI content (restored via callback)
  • ❌ View model state (you must persist this separately)
  • ❌ Runtime event handlers
  • ToolTip — it belongs to the content, not to the layout, and is pushed down from the view every time the view is attached

Architecture: DTO-Based Serialization

Under the hood, AvalonDock v5 uses a DTO (Data Transfer Object) layer to decouple the WPF layout tree from the serialization format. This enables both XML and JSON serializers to share the same mapping logic, and makes it possible to implement custom serializers.

Data Flow

Save:  LayoutRoot → LayoutDtoMapper.ToDto() → LayoutRootDto → Serializer → File
Load:  File → Serializer → LayoutRootDto → LayoutDtoMapper.FromDto() → LayoutRoot

The ILayoutDtoMapper interface defines the mapping:

public interface ILayoutDtoMapper
{
    LayoutRootDto ToDto(ISerializableLayoutRoot layout);
    ISerializableLayoutRoot FromDto(LayoutRootDto dto);
}

DTO Classes

All DTOs live in AvalonDock.Core.Serialization.Dto:

DTO Maps To
LayoutRootDto Root container (panels, sides, floating windows, hidden items)
LayoutPanelDto Panel with orientation and children
LayoutDocumentPaneDto Document tab container
LayoutAnchorablePaneDto Tool window container
LayoutDocumentPaneGroupDto Splitter group for document panes
LayoutAnchorablePaneGroupDto Splitter group for tool panes
LayoutDocumentDto Individual document
LayoutAnchorableDto Individual tool window
LayoutFloatingWindowDto Floating window (document or anchorable variant)
LayoutAnchorSideDto Auto-hide side
LayoutAnchorGroupDto Auto-hide group

Custom Serializer

To implement a custom serialization format, extend LayoutSerializerBase and override two methods:

using AvalonDock.Core.Serialization;
using AvalonDock.Core.Serialization.Dto;

public class YamlLayoutSerializer : LayoutSerializerBase
{
    public YamlLayoutSerializer(IDockingManager manager) : base(manager) { }

    protected override void SerializeCore(Stream stream, LayoutRootDto dto)
    {
        // Serialize the DTO tree to your format
    }

    protected override LayoutRootDto DeserializeCore(Stream stream)
    {
        // Deserialize your format back to a DTO tree
        return dto;
    }
}

The base class handles all fixup logic: reconnecting content via LayoutSerializationCallback, restoring previous containers, and cleaning up empty panes.


Auto-Save on Exit

A common pattern is to save the layout when the application closes:

protected override void OnClosing(CancelEventArgs e)
{
    var serializer = new XmlLayoutSerializer(dockManager);
    var layoutPath = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
        "MyApp", "layout.xml");

    Directory.CreateDirectory(Path.GetDirectoryName(layoutPath));

    using (var writer = new StreamWriter(layoutPath))
    {
        serializer.Serialize(writer);
    }

    base.OnClosing(e);
}

protected override void OnContentRendered(EventArgs e)
{
    var layoutPath = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
        "MyApp", "layout.xml");

    if (File.Exists(layoutPath))
    {
        var serializer = new XmlLayoutSerializer(dockManager);
        serializer.LayoutSerializationCallback += OnLayoutDeserialization;

        using (var reader = new StreamReader(layoutPath))
        {
            serializer.Deserialize(reader);
        }
    }

    base.OnContentRendered(e);
}