Tutorial: Build an MVVM IDE Application (v5)
In this tutorial you will build a Visual Studio Code-style IDE application using AvalonDock v5’s first-class MVVM and Dependency Injection architecture. Everything is controlled via view models — no code-behind layout manipulation, no singletons.
This tutorial follows the exact patterns used in the AvalonDockCodeApp sample project, which is the reference implementation for AvalonDock v5.
What You’ll Build
A docking IDE with:
- Sidebar toggle buttons for tool panels (like VS Code)
- Explorer panel — a file tree browser (toolbox, docked left)
- Search panel — a search input (toolbox, docked left)
- Output panel — log output (toolbox, docked bottom)
- Document tabs — file editor tabs, opened from the explorer
- Everything via DI —
IDockLayoutServicemanages the layout tree, view models are injected - Zero code-behind for layout logic
Prerequisites
- .NET 9 or later SDK
- Windows (WPF is Windows-only)
dotnet new wpf -n MvvmIdeApp
cd MvvmIdeApp
dotnet add package Dirkster.AvalonDock
dotnet add package Dirkster.AvalonDock.Mvvm
dotnet add package Dirkster.AvalonDock.Mvvm.CommunityToolkit
dotnet add package Dirkster.AvalonDock.DependencyInjection
dotnet add package Dirkster.AvalonDock.Themes.Arc
dotnet add package CommunityToolkit.Mvvm
The v5 Architecture
Before diving in, here is how the pieces fit together:
App.xaml.cs (Composition Root)
├── AddDockLayoutService(dock => { ← single entry point for dock configuration
│ dock.ConfigureToggleDock(...) ← configures sidebar button size, dock sizes
│ dock.AddToolbox<ExplorerToolbox>() ← registers tool VMs
│ dock.AddToolbox<SearchToolbox>()
│ dock.AddToolbox<OutputToolbox>()
│ })
└── AddSingleton<MainViewModel>() ← receives IDockLayoutService via DI
MainViewModel
├── DockLayout (IRootDock) ← bound to ToggleDockingManager.DockLayout
├── OpenFile() → _dockService.OpenOrActivateDocument(...)
└── CloseDocument() → _dockService.CloseDocument(...)
ToggleDockingManager (XAML)
├── DockLayout="{Binding DockLayout}" ← drives the entire layout from the VM
├── DataTemplates per VM type ← renders each toolbox/document
└── PanesStyleSelector ← binds Title, ContentId, IconSource
Key Base Classes
| Class | Package | Purpose |
|---|---|---|
ObservableToolboxBase |
AvalonDock.Mvvm.CommunityToolkit |
Base for tool panel VMs with [ObservableProperty] support. Has Zone, Icon, IsOpenByDefault. |
ObservableDocument |
AvalonDock.Mvvm.CommunityToolkit |
Base for document tab VMs with [ObservableProperty] support. |
ToolboxBase |
AvalonDock.Mvvm |
Lightweight base for tool panels (no external dependencies). |
DockableBase |
AvalonDock.Mvvm |
Abstract base for all dockables (no external dependencies). |
IDockLayoutService |
AvalonDock.Core |
Manages the layout tree. Open/close documents, get anchorables by type. |
IToolbox |
AvalonDock.Core |
Interface for toolbox VMs. DockZone controls placement. |
Step 1: Create Toolbox View Models
Each tool panel inherits from ObservableToolboxBase (from AvalonDock.Mvvm.CommunityToolkit) and declares its placement via DockZone. This enables [ObservableProperty] and [RelayCommand] source generators.
If you don’t need CommunityToolkit.Mvvm source generators, use ToolboxBase from AvalonDock.Mvvm instead — it has no external dependencies.
File: ViewModels/ExplorerToolbox.cs
using System.Collections.ObjectModel;
using AvalonDock.Core;
using AvalonDock.Mvvm.CommunityToolkit;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MvvmIdeApp.ViewModels;
public partial class ExplorerToolbox : ObservableToolboxBase
{
private Action<string> _openFileCallback = _ => { };
[ObservableProperty]
private ObservableCollection<string> _files = new()
{
"Program.cs", "MainWindow.xaml", "App.xaml", "README.md"
};
public ExplorerToolbox()
{
Id = "Explorer";
Title = "Explorer";
ToolTipText = "Explorer (Ctrl+Shift+E)";
Zone = DockZone.LeftTop; // Docked left, top section
}
public void SetOpenFileCallback(Action<string> callback)
=> _openFileCallback = callback;
public void OpenSelectedFile(string fileName)
=> _openFileCallback(fileName);
}
File: ViewModels/SearchToolbox.cs
using AvalonDock.Core;
using AvalonDock.Mvvm.CommunityToolkit;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MvvmIdeApp.ViewModels;
public partial class SearchToolbox : ObservableToolboxBase
{
[ObservableProperty]
private string _searchText = string.Empty;
public SearchToolbox()
{
Id = "Search";
Title = "Search";
ToolTipText = "Search (Ctrl+Shift+F)";
Zone = DockZone.LeftTop; // Same side as Explorer
}
}
File: ViewModels/OutputToolbox.cs
using AvalonDock.Core;
using AvalonDock.Mvvm.CommunityToolkit;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MvvmIdeApp.ViewModels;
public partial class OutputToolbox : ObservableToolboxBase
{
[ObservableProperty]
private string _outputText = "Ready.\n";
public OutputToolbox()
{
Id = "Output";
Title = "Output";
ToolTipText = "Output (Ctrl+Shift+U)";
Zone = DockZone.BottomLeft; // Docked at bottom
IsOpenByDefault = true; // Open when app starts
}
public void AppendLine(string text) => OutputText += text + "\n";
}
DockZone Options
The DockZone enum controls where each toolbox appears:
| Zone | Position |
|---|---|
LeftTop |
Left sidebar, upper section |
LeftBottom |
Left sidebar, lower section |
RightTop |
Right sidebar, upper section |
RightBottom |
Right sidebar, lower section |
BottomLeft |
Bottom panel, left section |
BottomRight |
Bottom panel, right section |
Step 2: Create the Document View Model
Documents inherit from ObservableDocument (from AvalonDock.Mvvm.CommunityToolkit). Each open file gets its own tab.
File: ViewModels/EditorTabViewModel.cs
using System.IO;
using AvalonDock.Mvvm.CommunityToolkit;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MvvmIdeApp.ViewModels;
public partial class EditorTabViewModel : ObservableDocument
{
[ObservableProperty]
private string _filePath = string.Empty;
[ObservableProperty]
private string _content = string.Empty;
[ObservableProperty]
private string _toolTip = string.Empty;
public void LoadFile(string path)
{
FilePath = path;
Title = Path.GetFileName(path);
Id = path; // Unique ID for this document
ToolTip = path;
try
{
Content = File.ReadAllText(path);
IsModified = false;
}
catch (Exception ex)
{
Content = $"Error: {ex.Message}";
}
}
}
Step 3: Create the Main View Model
The MainViewModel receives IDockLayoutService via constructor injection. It uses the service to open/close documents and access tool panels — no direct layout manipulation needed.
File: ViewModels/MainViewModel.cs
using AvalonDock.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MvvmIdeApp.ViewModels;
public partial class MainViewModel : ObservableObject
{
private readonly IDockLayoutService _dockService;
/// <summary>The MVVM layout tree — bind to DockLayout on the DockingManager.</summary>
public IRootDock DockLayout => _dockService.Layout;
/// <summary>Typed access to the Explorer toolbox via the layout service.</summary>
public ExplorerToolbox? Explorer => _dockService.GetAnchorable<ExplorerToolbox>();
/// <summary>Typed access to the Output toolbox.</summary>
public OutputToolbox? Output => _dockService.GetAnchorable<OutputToolbox>();
public MainViewModel(IDockLayoutService dockService)
{
_dockService = dockService;
// Wire up file-open callback from Explorer
Explorer?.SetOpenFileCallback(OpenFile);
Output?.AppendLine("Application initialized via DI.");
}
/// <summary>Opens a file as a document tab, or activates it if already open.</summary>
public void OpenFile(string filePath)
{
_dockService.OpenOrActivateDocument(
existing => existing.FilePath == filePath, // find existing
() => // or create new
{
var tab = new EditorTabViewModel();
tab.LoadFile(filePath);
return tab;
});
Output?.AppendLine($"Opened: {filePath}");
}
[RelayCommand]
private void CloseDocument(EditorTabViewModel? tab)
{
if (tab != null)
{
_dockService.CloseDocument(tab);
Output?.AppendLine($"Closed: {tab.Title}");
}
}
}
IDockLayoutService API
The service manages all layout operations from your view model:
| Method | Description |
|---|---|
Layout |
The IRootDock tree — bind to DockLayout in XAML |
OpenDocument(doc) |
Add a document and make it active |
CloseDocument(doc) |
Remove a document from the layout |
OpenOrActivateDocument(predicate, factory) |
Find existing or create new document |
FindDocument<T>(predicate) |
Find an open document by predicate |
GetAnchorable<T>() |
Get a registered toolbox by type |
Documents |
All currently open documents |
Anchorables |
All registered toolboxes |
Step 4: Configure the DI Composition Root
The App.xaml.cs is the composition root. This is where toolboxes are registered, the layout service is configured, and everything is wired together.
File: App.xaml.cs
using System.Windows;
using AvalonDock.Core;
using AvalonDock.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using MvvmIdeApp.ViewModels;
namespace MvvmIdeApp;
public partial class App : Application
{
private IServiceProvider? _serviceProvider;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
ConfigureServices(services);
_serviceProvider = services.BuildServiceProvider();
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
private static void ConfigureServices(IServiceCollection services)
{
// Configure dock layout: toggle dock options + toolboxes in one call
services.AddDockLayoutService(dock =>
{
dock.ConfigureToggleDock(opts =>
{
opts.ButtonSize = 28;
opts.DefaultDockWidth = 280;
opts.DefaultDockHeight = 220;
});
// Register toolbox VMs — order determines sidebar button order
dock.AddToolbox<ExplorerToolbox>();
dock.AddToolbox<SearchToolbox>();
dock.AddToolbox<OutputToolbox>();
});
// Main view model — receives IDockLayoutService
services.AddSingleton<MainViewModel>();
// Main window — receives MainViewModel + ToggleDockOptions
services.AddSingleton<MainWindow>();
}
protected override void OnExit(ExitEventArgs e)
{
(_serviceProvider as IDisposable)?.Dispose();
base.OnExit(e);
}
}
Remove StartupUri="MainWindow.xaml" from App.xaml — the window is now created via DI.
File: App.xaml
<Application x:Class="MvvmIdeApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
ShutdownMode="OnMainWindowClose">
<Application.Resources />
</Application>
Step 5: Create the Style Selector
The style selector binds layout properties (Title, ContentId, IconSource) from view models to layout items. It differentiates between toolboxes and documents.
File: PanesStyleSelector.cs
using System.Windows;
using System.Windows.Controls;
using AvalonDock.Core;
using MvvmIdeApp.ViewModels;
namespace MvvmIdeApp;
public class PanesStyleSelector : StyleSelector
{
public Style? ToolboxStyle { get; set; }
public Style? DocumentStyle { get; set; }
public override Style? SelectStyle(object item, DependencyObject container)
{
if (item is IToolbox)
return ToolboxStyle;
if (item is EditorTabViewModel)
return DocumentStyle;
return base.SelectStyle(item, container);
}
}
Step 6: Build the XAML Layout
The XAML uses ToggleDockingManager (the v5 sidebar-based docking control) and binds its DockLayout property to the view model.
File: MainWindow.xaml
<Window x:Class="MvvmIdeApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonDock="https://github.com/Dirkster99/AvalonDock"
xmlns:avalonDockControls="clr-namespace:AvalonDock.Controls;assembly=AvalonDock"
xmlns:themes="clr-namespace:AvalonDock.Themes;assembly=AvalonDock.Themes.Arc"
xmlns:local="clr-namespace:MvvmIdeApp"
xmlns:vm="clr-namespace:MvvmIdeApp.ViewModels"
Title="MVVM IDE — AvalonDock v5 Tutorial"
Width="1000" Height="700"
Background="#252729">
<Window.Resources>
<!-- DataTemplate: how to render each toolbox VM's content area -->
<DataTemplate DataType="{x:Type vm:ExplorerToolbox}">
<ListBox ItemsSource="{Binding Files}" Background="#252526"
Foreground="#CCCCCC" BorderThickness="0" Margin="4" />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SearchToolbox}">
<Border Background="#252526" Padding="12">
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
Background="#3C3C3C" Foreground="#CCCCCC"
BorderThickness="0" Padding="6,4" />
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:OutputToolbox}">
<TextBox Text="{Binding OutputText, Mode=OneWay}"
IsReadOnly="True" AcceptsReturn="True"
FontFamily="Consolas" FontSize="12"
VerticalScrollBarVisibility="Auto"
Background="#1E1E1E" Foreground="#DCDCDC" />
</DataTemplate>
<!-- DataTemplate: how to render document content -->
<DataTemplate DataType="{x:Type vm:EditorTabViewModel}">
<TextBox Text="{Binding Content, UpdateSourceTrigger=PropertyChanged}"
AcceptsReturn="True" AcceptsTab="True"
FontFamily="Consolas" FontSize="13"
VerticalScrollBarVisibility="Auto"
Background="#1E1E1E" Foreground="#DCDCDC" />
</DataTemplate>
<!-- LayoutItem style selector -->
<local:PanesStyleSelector x:Key="PanesStyleSelector">
<local:PanesStyleSelector.ToolboxStyle>
<Style TargetType="{x:Type avalonDockControls:LayoutAnchorableItem}">
<Setter Property="Title" Value="{Binding Model.Title}" />
<Setter Property="ContentId" Value="{Binding Model.ContentId}" />
</Style>
</local:PanesStyleSelector.ToolboxStyle>
<local:PanesStyleSelector.DocumentStyle>
<Style TargetType="{x:Type avalonDockControls:LayoutDocumentItem}">
<Setter Property="Title" Value="{Binding Model.Title}" />
<Setter Property="ToolTip" Value="{Binding Model.ToolTip}" />
<Setter Property="ContentId" Value="{Binding Model.ContentId}" />
</Style>
</local:PanesStyleSelector.DocumentStyle>
</local:PanesStyleSelector>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Menu -->
<Menu Grid.Row="0" Background="#252729" Foreground="#CCCCCC" Padding="4">
<MenuItem Header="_File" Foreground="#CCCCCC">
<MenuItem Header="E_xit" Click="OnExit" />
</MenuItem>
</Menu>
<!-- ToggleDockingManager — the v5 sidebar-based docking control -->
<avalonDock:ToggleDockingManager x:Name="dockManager" Grid.Row="1"
Background="#252729" BorderThickness="0"
DockLayout="{Binding DockLayout}"
LayoutItemContainerStyleSelector="{StaticResource PanesStyleSelector}">
<avalonDock:ToggleDockingManager.Theme>
<themes:ArcDarkTheme />
</avalonDock:ToggleDockingManager.Theme>
<!-- Initial document area (toolboxes are auto-placed by DockLayoutService) -->
<avalonDock:LayoutRoot>
<avalonDock:LayoutPanel Orientation="Horizontal">
<avalonDock:LayoutDocumentPaneGroup>
<avalonDock:LayoutDocumentPane>
<avalonDock:LayoutDocument Title="Welcome" ContentId="welcome">
<Border Background="#1E1E1E" Padding="40">
<StackPanel VerticalAlignment="Center"
HorizontalAlignment="Center">
<TextBlock Text="MVVM IDE" FontSize="28"
FontWeight="Light" Foreground="#CCCCCC"
HorizontalAlignment="Center" />
<TextBlock Text="AvalonDock v5 Tutorial" FontSize="14"
Foreground="#808080"
HorizontalAlignment="Center" Margin="0,8,0,24" />
<TextBlock Foreground="#808080" FontSize="13"
TextAlignment="Center" LineHeight="24">
<Run Text="Click sidebar icons to toggle panels" />
<LineBreak />
<Run Text="Powered by AvalonDock.Mvvm + DependencyInjection" />
</TextBlock>
</StackPanel>
</Border>
</avalonDock:LayoutDocument>
</avalonDock:LayoutDocumentPane>
</avalonDock:LayoutDocumentPaneGroup>
</avalonDock:LayoutPanel>
</avalonDock:LayoutRoot>
</avalonDock:ToggleDockingManager>
</Grid>
</Window>
Step 7: Wire Up the Code-Behind
The code-behind receives the view model and dock options via constructor injection. Layout options are applied here — the only code-behind needed.
File: MainWindow.xaml.cs
using System.Windows;
using AvalonDock.DependencyInjection;
using MvvmIdeApp.ViewModels;
namespace MvvmIdeApp;
public partial class MainWindow : Window
{
public MainWindow(MainViewModel viewModel, ToggleDockOptions? dockOptions = null)
{
DataContext = viewModel;
InitializeComponent();
// Apply DI-configured dock options
if (dockOptions != null)
{
dockManager.ButtonSize = dockOptions.ButtonSize;
dockManager.DefaultDockWidth = dockOptions.DefaultDockWidth;
dockManager.DefaultDockHeight = dockOptions.DefaultDockHeight;
}
}
private void OnExit(object sender, RoutedEventArgs e) => Close();
}
How It Works
The v5 Data Flow
DI Container
├── ExplorerToolbox (IToolbox, Zone=LeftTop)
├── SearchToolbox (IToolbox, Zone=LeftTop)
├── OutputToolbox (IToolbox, Zone=BottomLeft)
│
└── DockLayoutService
├── Collects all IToolbox instances
├── Auto-builds IRootDock layout tree
└── Exposes: Layout, OpenDocument(), CloseDocument(), GetAnchorable<T>()
MainViewModel
├── DockLayout → bound to ToggleDockingManager.DockLayout
├── OpenFile() → _dockService.OpenOrActivateDocument(...)
└── Explorer → _dockService.GetAnchorable<ExplorerToolbox>()
ToggleDockingManager (XAML)
├── Reads DockLayout to know what panels exist and where
├── Creates sidebar toggle buttons from toolbox icons
├── Uses DataTemplates to render each VM's content
└── Uses PanesStyleSelector to bind Title/ContentId
What Makes v5 Different
| v4 / Legacy Pattern | v5 Pattern |
|---|---|
Static Workspace.This singleton |
IDockLayoutService via constructor injection |
Manual DocumentsSource / AnchorablesSource binding |
DockLayout property binds the entire layout tree |
ILayoutUpdateStrategy to control placement |
DockZone enum on each ToolboxBase |
ActiveDocumentConverter for active tracking |
IDockLayoutService.ActiveDockable |
Manual ObservableCollection management |
OpenOrActivateDocument() / CloseDocument() |
DockingManager with manual layout |
ToggleDockingManager with sidebar toggles |
Adding a New Toolbox
To add a new tool panel in the v5 architecture:
- Create the VM — inherit from
ObservableToolboxBase(orToolboxBase), setZone:public class ProblemsToolbox : ObservableToolboxBase { public ProblemsToolbox() { Id = "Problems"; Title = "Problems"; Zone = DockZone.BottomLeft; } } - Register in DI — add one line inside the
AddDockLayoutServicebuilder:dock.AddToolbox<ProblemsToolbox>(); - Add a DataTemplate in XAML:
<DataTemplate DataType="{x:Type vm:ProblemsToolbox}"> <TextBlock Text="No problems detected" Foreground="#808080" Margin="12" /> </DataTemplate>
That’s it — DockLayoutService automatically places it in the layout based on its DockZone.
Next Steps
- Add Layout Persistence to save and restore the user’s panel arrangement
- Apply a Custom Theme to match your application’s branding
- See the
AvalonDockCodeAppproject in the repository for the full reference implementation with a terminal, file icons, and syntax highlighting - Review the MVVM Guide for additional patterns (template selectors, style selectors)
- Review the DI Guide for all available DI extension methods