Tutorial: Layout Persistence
In this tutorial you will add layout persistence to an AvalonDock application so that the user’s panel arrangement is saved on exit and restored on the next launch. You’ll implement both the basic code-behind approach and the MVVM-friendly approach.
This tutorial is inspired by the MVVMTestApp and TestApp sample projects, which both demonstrate layout serialization with the XmlLayoutSerializer.
What You’ll Build
An application that:
- Saves the complete docking layout (panel positions, sizes, floating windows) when the window closes
- Restores the layout when the application starts
- Properly reconnects view models to deserialized layout items (MVVM)
- Provides a Reset Layout command to return to the default arrangement
- Supports both XML and JSON serialization
Prerequisites
dotnet add package Dirkster.AvalonDock
dotnet add package Dirkster.AvalonDock.Serializer.Xml
# Or for JSON:
# dotnet add package Dirkster.AvalonDock.Serializer.Json
Part 1: Basic Layout Persistence (Code-Behind)
This approach works without MVVM and is the simplest way to get started.
Step 1: Define a Layout File Path
private static string LayoutFilePath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MyApp", "layout.xml");
Step 2: Save on Window Close
using AvalonDock.Serializer.Xml;
protected override void OnClosing(CancelEventArgs e)
{
// Ensure the directory exists
Directory.CreateDirectory(Path.GetDirectoryName(LayoutFilePath));
var serializer = new XmlLayoutSerializer(DockManager);
using (var writer = new StreamWriter(LayoutFilePath))
{
serializer.Serialize(writer);
}
base.OnClosing(e);
}
Step 3: Restore on Window Load
private void DockManager_Loaded(object sender, RoutedEventArgs e)
{
if (!File.Exists(LayoutFilePath))
return;
var serializer = new XmlLayoutSerializer(DockManager);
// The callback reconnects content to deserialized layout items
serializer.LayoutSerializationCallback += (s, args) =>
{
args.Content = args.Model.ContentId switch
{
"explorer" => FindOrCreateControl("explorer"),
"properties" => FindOrCreateControl("properties"),
"output" => FindOrCreateControl("output"),
_ => null // Unknown items are skipped
};
};
using (var reader = new StreamReader(LayoutFilePath))
{
serializer.Deserialize(reader);
}
}
The LayoutSerializationCallback is called for every content item in the saved layout. You must provide the actual UI content (or view model) for each item, matched by ContentId. If you set args.Content = null or args.Cancel = true, that item is removed from the restored layout.
Step 4: Wire the Loaded Event
<avalonDock:DockingManager x:Name="DockManager" Loaded="DockManager_Loaded">
Part 2: MVVM Layout Persistence
When using MVVM with DocumentsSource and AnchorablesSource, the serialization callback must return view models instead of UI controls.
Step 1: Add Save/Restore to Your View Model
File: ViewModels/WorkspaceViewModel.cs (additions)
using System.Windows.Input;
public class WorkspaceViewModel
{
// ... existing code ...
public ICommand SaveLayoutCommand => new RelayCommand(_ => SaveLayout());
public ICommand RestoreLayoutCommand => new RelayCommand(_ => RestoreLayout());
public ICommand ResetLayoutCommand => new RelayCommand(_ => ResetLayout());
/// <summary>
/// Called by the view to provide the DockingManager reference for serialization.
/// </summary>
public Action<string> OnSaveLayout { get; set; }
public Action<string> OnRestoreLayout { get; set; }
public Action OnResetLayout { get; set; }
private void SaveLayout() => OnSaveLayout?.Invoke(LayoutFilePath);
private void RestoreLayout() => OnRestoreLayout?.Invoke(LayoutFilePath);
private void ResetLayout() => OnResetLayout?.Invoke();
private static string LayoutFilePath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MyApp", "layout.xml");
}
Step 2: Implement the Serialization in Code-Behind
Even with MVVM, layout serialization requires a reference to the DockingManager control, so the actual serialize/deserialize calls live in the view’s code-behind.
File: MainWindow.xaml.cs
using AvalonDock.Serializer.Xml;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
Closing += OnWindowClosing;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (DataContext is WorkspaceViewModel vm)
{
// Wire up layout callbacks
vm.OnSaveLayout = SaveLayout;
vm.OnRestoreLayout = path => RestoreLayout(path, vm);
vm.OnResetLayout = () =>
{
// Remove and re-add the DockingManager to reset
// Or simply delete the layout file and restart
};
// Auto-restore on startup
RestoreLayout(WorkspaceViewModel.LayoutFilePath, vm);
}
}
private void OnWindowClosing(object sender, CancelEventArgs e)
{
SaveLayout(WorkspaceViewModel.LayoutFilePath);
}
private void SaveLayout(string path)
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
var serializer = new XmlLayoutSerializer(DockManager);
using var writer = new StreamWriter(path);
serializer.Serialize(writer);
}
private void RestoreLayout(string path, WorkspaceViewModel vm)
{
if (!File.Exists(path)) return;
var serializer = new XmlLayoutSerializer(DockManager);
serializer.LayoutSerializationCallback += (sender, args) =>
{
// Match by ContentId and return the VIEW MODEL (not a control)
var contentId = args.Model.ContentId;
// Try to find among existing tool view models
var tool = vm.Tools.FirstOrDefault(t => t.ContentId == contentId);
if (tool != null)
{
args.Content = tool;
return;
}
// Try to find among existing documents
var doc = vm.Documents.FirstOrDefault(d => d.ContentId == contentId);
if (doc != null)
{
args.Content = doc;
return;
}
// Unknown content — skip it
args.Cancel = true;
};
using var reader = new StreamReader(path);
serializer.Deserialize(reader);
}
}
Part 3: JSON Serialization
The JSON serializer works identically but produces smaller, more portable files.
dotnet add package Dirkster.AvalonDock.Serializer.Json
using AvalonDock.Serializer.Json;
// Save
var serializer = new JsonLayoutSerializer(DockManager);
using (var writer = new StreamWriter("layout.json"))
{
serializer.Serialize(writer);
}
// Restore
var serializer = new JsonLayoutSerializer(DockManager);
serializer.LayoutSerializationCallback += (s, args) => { /* same as XML */ };
using (var reader = new StreamReader("layout.json"))
{
serializer.Deserialize(reader);
}
XML vs JSON
| Aspect | XML | JSON |
|---|---|---|
| File size | Larger | Smaller |
| Human-readable | Yes (verbose) | Yes (compact) |
| Legacy support | Works with older configs | Modern apps only |
| Performance | Slightly slower | Slightly faster |
| Package | Serializer.Xml |
Serializer.Json |
Part 4: What Gets Serialized (and What Doesn’t)
Understanding what is and isn’t persisted is critical for a correct implementation.
✅ Serialized (Layout State)
| Data | Example |
|---|---|
| Panel positions | Explorer is docked left, Properties is docked right |
| Panel sizes | Explorer has DockWidth=”250” |
| Tab order | Document1 is before Document2 |
| Floating window positions | Properties window is at (400, 200) with size 300×400 |
| Auto-hide state | Output is auto-hidden on the bottom side |
| Active/selected state | Document2 is the active tab |
| ContentId | Each item’s unique identifier |
| Title | Each item’s display title |
| Panel orientation | Horizontal or vertical splits |
❌ Not Serialized (Application State)
| Data | How to Persist |
|---|---|
| Document text content | Save separately (e.g., to files or a database) |
| View model property values | Serialize your view models independently |
| Event handlers | Re-attach during deserialization callback |
| Runtime UI state | Rebuild in the callback |
| Undo/redo history | Not applicable to layout |
Part 5: Reset to Default Layout
Users often want a “Reset Layout” option. Here are two approaches:
Approach A: Embed a Default Layout
Save your default layout as an embedded resource and restore from it:
public void ResetLayout()
{
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream("MyApp.DefaultLayout.xml");
if (stream == null) return;
var serializer = new XmlLayoutSerializer(DockManager);
serializer.LayoutSerializationCallback += OnLayoutCallback;
serializer.Deserialize(stream);
}
Approach B: Delete and Restart
The simpler approach — delete the saved layout file so the XAML-defined layout is used on next launch:
public void ResetLayout()
{
if (File.Exists(LayoutFilePath))
File.Delete(LayoutFilePath);
MessageBox.Show("Layout will be reset on next restart.");
}
Part 6: Error Handling
Layout files can become corrupted or incompatible after application updates. Always wrap deserialization in a try-catch:
private void RestoreLayoutSafe(string path)
{
if (!File.Exists(path)) return;
try
{
var serializer = new XmlLayoutSerializer(DockManager);
serializer.LayoutSerializationCallback += OnLayoutCallback;
using var reader = new StreamReader(path);
serializer.Deserialize(reader);
}
catch (Exception ex)
{
// Log the error
System.Diagnostics.Debug.WriteLine($"Failed to restore layout: {ex.Message}");
// Delete the corrupt file so the default layout is used
try { File.Delete(path); } catch { }
}
}
Always handle deserialization errors gracefully. A corrupt layout file should never prevent the application from starting. Delete the file and fall back to the XAML-defined default layout.
How It Works
Serialization Flow
Save:
DockingManager → XmlLayoutSerializer.Serialize()
→ Walks the LayoutRoot tree
→ Writes each element's position, size, state, ContentId
→ Produces layout.xml
Restore:
layout.xml → XmlLayoutSerializer.Deserialize()
→ Parses layout tree
→ For each content item: calls LayoutSerializationCallback
→ You provide the content (VM or control) matched by ContentId
→ DockingManager rebuilds the visual tree
The ContentId Contract
The ContentId is the key that links serialized layout items to your application content. Follow these rules:
- Every dockable item must have a unique
ContentId— duplicates cause unpredictable behavior ContentIdmust be stable — don’t use GUIDs that change every session (for tools). Documents may use GUIDs if you track them separately.- Tools should use descriptive IDs — e.g.,
"explorer","properties","output" - Documents can use file paths — e.g.,
"/path/to/file.txt"or a database key
Next Steps
- See the Layout Serialization Guide for a complete API reference
- Combine with MVVM for a full IDE experience
- Explore the
MVVMTestAppsample for a working implementation with save/restore - Explore the
TestAppsample for layout reload/unload patterns