Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f4087f4
Add comprehensive Avalonia leak tests
wieslawsoltes Feb 3, 2026
b4b4e9e
Add model leak tests for MVVM and ReactiveUI
wieslawsoltes Feb 3, 2026
8df0ca2
Add leak test CI workflow and solution entries
wieslawsoltes Feb 3, 2026
67146d8
Leak tests: harden headless infra
wieslawsoltes Feb 4, 2026
cf75a02
Leak tests: split Avalonia cases by category
wieslawsoltes Feb 4, 2026
f5bab4a
Leak tests: add MVVM and ReactiveUI dockables
wieslawsoltes Feb 4, 2026
d430550
DockControl: prune factory defaults and caches
wieslawsoltes Feb 4, 2026
490d4ad
Leak tests: expand Avalonia cleanup hooks
wieslawsoltes Feb 4, 2026
80f4162
Leak tests: add layout/overlay coverage
wieslawsoltes Feb 4, 2026
604abed
DockControl: add factory cleanup service
wieslawsoltes Feb 4, 2026
b8a0ef1
DockControl: delegate recycling and pruning
wieslawsoltes Feb 4, 2026
9448422
Fix OverlayHost provider change handling
wieslawsoltes Feb 4, 2026
f399ed7
Avoid scrubbing delegate targets in leak tests
wieslawsoltes Feb 4, 2026
5d7701e
Add DockControl factory event leak test
wieslawsoltes Feb 4, 2026
6a6696b
Add DockableControl tracking leak tests
wieslawsoltes Feb 4, 2026
59768f6
Add ToolTabStrip leak test
wieslawsoltes Feb 4, 2026
402aefd
Add OverlayHost leak tests
wieslawsoltes Feb 4, 2026
9b4d7bd
Add window leak tests
wieslawsoltes Feb 4, 2026
ffd978c
Dispose drag helper subscriptions on detach
wieslawsoltes Feb 4, 2026
8592b1a
Detach managed window layer subscriptions on unload
wieslawsoltes Feb 4, 2026
5c715b9
Add DocumentTabStrip drag helper leak test
wieslawsoltes Feb 4, 2026
cee4c7b
Add ManagedWindowLayer detach leak test
wieslawsoltes Feb 4, 2026
8fad50e
Add pinned dock detach leak test
wieslawsoltes Feb 4, 2026
b0d0f97
Fix item drag auto-scroll cleanup
wieslawsoltes Feb 4, 2026
1555c8e
Add tab strip detach leak tests
wieslawsoltes Feb 4, 2026
93141c6
Add item auto-scroll leak tests
wieslawsoltes Feb 4, 2026
ce3f315
Add tool chrome and pin item leak tests
wieslawsoltes Feb 4, 2026
5d99ae2
Add command bar manager leak test
wieslawsoltes Feb 4, 2026
ec1a126
Add input cleanup leak tests
wieslawsoltes Feb 4, 2026
6d91f85
Add detach leak tests for dock/content controls
wieslawsoltes Feb 4, 2026
120f51d
Add leak tests for dock targets and selector overlay
wieslawsoltes Feb 4, 2026
9f7eb34
Add layout control and splitter leak tests
wieslawsoltes Feb 4, 2026
7b8fb82
Add DocumentDock items source and managed window document leak tests
wieslawsoltes Feb 4, 2026
34e5fea
Add host window drag and managed window layer leak tests
wieslawsoltes Feb 4, 2026
bb9c5a2
Extend dockable and command bar leak tests
wieslawsoltes Feb 4, 2026
edcda00
Add overlay window drag preview leak tests
wieslawsoltes Feb 4, 2026
f474006
Add tab strip item action leak tests
wieslawsoltes Feb 4, 2026
a0245ed
Add themed interaction leak tests for menus and overlays
wieslawsoltes Feb 5, 2026
48a90a2
Fix XAML bindings and leak test helpers
wieslawsoltes Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/leak-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Leak Tests
permissions:
contents: read

on:
push:
branches:
- master
- release/*
pull_request:
branches:
- master
- release/*

env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1

jobs:
leak-tests:
name: Leak Tests (Release)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Install workloads
run: dotnet workload install wasm-tools wasm-experimental
- name: Run Dock.Avalonia leak tests
run: dotnet test -c Release tests/Dock.Avalonia.LeakTests/Dock.Avalonia.LeakTests.csproj
- name: Run Dock.Model.Mvvm leak tests
run: dotnet test -c Release tests/Dock.Model.Mvvm.LeakTests/Dock.Model.Mvvm.LeakTests.csproj
- name: Run Dock.Model.ReactiveUI leak tests
run: dotnet test -c Release tests/Dock.Model.ReactiveUI.LeakTests/Dock.Model.ReactiveUI.LeakTests.csproj
3 changes: 3 additions & 0 deletions Dock.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
</Folder>
<Folder Name="/tests/">
<Project Path="tests/Dock.Avalonia.Diagnostics.UnitTests/Dock.Avalonia.Diagnostics.UnitTests.csproj" />
<Project Path="tests/Dock.Avalonia.LeakTests/Dock.Avalonia.LeakTests.csproj" />
<Project Path="tests/Dock.Avalonia.HeadlessTests/Dock.Avalonia.HeadlessTests.csproj" />
<Project Path="tests/Dock.Avalonia.Themes.UnitTests/Dock.Avalonia.Themes.UnitTests.csproj" />
<Project Path="tests/Dock.Avalonia.UnitTests/Dock.Avalonia.UnitTests.csproj" />
Expand All @@ -107,9 +108,11 @@
<Project Path="tests/Dock.MarkupExtension.UnitTests/Dock.MarkupExtension.UnitTests.csproj" />
<Project Path="tests/Dock.Model.Avalonia.UnitTests/Dock.Model.Avalonia.UnitTests.csproj" />
<Project Path="tests/Dock.Model.CaliburMicro.UnitTests/Dock.Model.CaliburMicro.UnitTests.csproj" />
<Project Path="tests/Dock.Model.Mvvm.LeakTests/Dock.Model.Mvvm.LeakTests.csproj" />
<Project Path="tests/Dock.Model.Mvvm.UnitTests/Dock.Model.Mvvm.UnitTests.csproj" />
<Project Path="tests/Dock.Model.Prism.UnitTests/Dock.Model.Prism.UnitTests.csproj" />
<Project Path="tests/Dock.Model.ReactiveProperty.UnitTests/Dock.Model.ReactiveProperty.UnitTests.csproj" />
<Project Path="tests/Dock.Model.ReactiveUI.LeakTests/Dock.Model.ReactiveUI.LeakTests.csproj" />
<Project Path="tests/Dock.Model.ReactiveUI.UnitTests/Dock.Model.ReactiveUI.UnitTests.csproj" />
<Project Path="tests/Dock.Model.UnitTests/Dock.Model.UnitTests.csproj" />
<Project Path="tests/Dock.Serializer.UnitTests/Dock.Serializer.UnitTests.csproj" />
Expand Down
13 changes: 10 additions & 3 deletions samples/DockFigmaSample/Views/Documents/DesignCanvasView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@

<Grid Grid.Row="1" ColumnDefinitions="2*,*" ColumnSpacing="32">
<StackPanel Spacing="16">
<TextBlock Text="Design tools with
real-time momentum" Classes="hero-title" />
<TextBlock Classes="hero-title">
<Run Text="Design tools with" />
<LineBreak />
<Run Text="real-time momentum" />
</TextBlock>
<TextBlock Text="Build interactive flows faster with components, tokens, and smart docking." Classes="hero-body" />
<StackPanel Orientation="Horizontal" Spacing="12">
<Border Background="{DynamicResource AccentBrush}" CornerRadius="14" Padding="16,10">
Expand Down Expand Up @@ -85,7 +88,11 @@ real-time momentum" Classes="hero-title" />
Padding="10,6"
CornerRadius="12"
Background="{DynamicResource CanvasOverlayBrush}">
<TextBlock Text="{Binding FrameName} | {Binding FrameSize}" Classes="muted" />
<TextBlock Classes="muted">
<Run Text="{Binding FrameName}" />
<Run Text=" | " />
<Run Text="{Binding FrameSize}" />
</TextBlock>
</Border>
</Grid>
</UserControl>
13 changes: 7 additions & 6 deletions samples/DockReactiveUIWindowRelationsSample/Views/MainView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="8" Spacing="8">
<Button Content="Reset Layout" Command="{Binding ResetLayout}" />
<StackPanel DataContext="{Binding Layout}" Orientation="Horizontal" Spacing="8" x:DataType="dmc:IRootDock">
<Button Content="Show Windows" Command="{Binding ShowWindows}" />
<Button Content="Exit Windows" Command="{Binding ExitWindows}" />
<StackPanel DataContext="{Binding Layout}">
<StackPanel Orientation="Horizontal" Spacing="8" x:DataType="dmc:IRootDock">
<Button Content="Show Windows" Command="{Binding ShowWindows}" />
<Button Content="Exit Windows" Command="{Binding ExitWindows}" />
</StackPanel>
</StackPanel>
</StackPanel>
<DockControl Layout="{Binding Layout}" Margin="8" />
<Border DockPanel.Dock="Bottom" Padding="8" Background="#1F000000">
<TextBlock DataContext="{Binding Layout}"
Text="{Binding FocusedDockable, FallbackValue=''}"
<Border DockPanel.Dock="Bottom" Padding="8" Background="#1F000000" DataContext="{Binding Layout}">
<TextBlock Text="{Binding FocusedDockable, FallbackValue=''}"
x:DataType="dm:IDock" />
</Border>
</DockPanel>
Expand Down
4 changes: 2 additions & 2 deletions src/Dock.Avalonia.Themes.Fluent/Controls/HostWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@

<Setter Property="Content" Value="{Binding}" />
<Setter Property="ContentTemplate">
<DataTemplate>
<DataTemplate x:DataType="controls:IRootDock">
<Panel Margin="{Binding $parent[HostWindow].OffScreenMargin}">
<Panel Margin="{Binding $parent[HostWindow].WindowDecorationMargin}">
<OverlayHost VisualTreeLifecycleBehavior.IsEnabled="True">
Expand Down Expand Up @@ -107,7 +107,7 @@

<Setter Property="Content" Value="{Binding}" />
<Setter Property="ContentTemplate">
<DataTemplate>
<DataTemplate x:DataType="controls:IRootDock">
<Panel Margin="{Binding $parent[HostWindow].OffScreenMargin}">
<OverlayHost VisualTreeLifecycleBehavior.IsEnabled="True">
<DockControl Layout="{Binding}" />
Expand Down
110 changes: 35 additions & 75 deletions src/Dock.Avalonia/Controls/DockControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Recycling;
using Avalonia.Controls.Recycling.Model;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
Expand All @@ -21,6 +18,7 @@
using Dock.Avalonia.Diagnostics;
using Dock.Avalonia.Internal;
using Dock.Avalonia.Selectors;
using Dock.Avalonia.Services;
using Dock.Model;
using Dock.Model.Controls;
using Dock.Model.Core;
Expand All @@ -38,10 +36,10 @@ namespace Dock.Avalonia.Controls;
[TemplatePart("PART_ManagedWindowLayer", typeof(ManagedWindowLayer))]
public class DockControl : TemplatedControl, IDockControl, IDockSelectorService
{
private static readonly ConditionalWeakTable<IFactory, IControlRecycling> s_controlRecycling = new();
private readonly DockManagerOptions _dockManagerOptions;
private readonly DockManager _dockManager;
private readonly DockControlState _dockControlState;
private readonly IDockControlFactoryService _factoryService;
private bool _isInitialized;
private ContentControl? _contentControl;
private ManagedWindowLayer? _managedWindowLayer;
Expand Down Expand Up @@ -238,6 +236,7 @@ public DockControl()
_dockManagerOptions = new DockManagerOptions();
_dockManager = new DockManager(new DockService(), _dockManagerOptions);
_dockControlState = new DockControlState(_dockManager, _dragOffsetCalculator);
_factoryService = new DockControlFactoryService();
AddHandler(PointerPressedEvent, PressedHandler, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
AddHandler(PointerReleasedEvent, ReleasedHandler, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
AddHandler(PointerMovedEvent, MovedHandler, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
Expand All @@ -259,7 +258,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
_selectorOverlay = e.NameScope.Find<DockSelectorOverlay>("PART_SelectorOverlay");
_managedWindowLayer = e.NameScope.Find<ManagedWindowLayer>("PART_ManagedWindowLayer");

InitializeControlRecycling();
_factoryService.InitializeControlRecycling(this);

if (_contentControl is not null)
{
Expand All @@ -270,57 +269,6 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
InitializeCommandBars();
}

private void InitializeControlRecycling()
{
if (Layout?.Factory is not { } factory)
{
return;
}

var controlRecycling = ControlRecyclingDataTemplate.GetControlRecycling(this);
if (controlRecycling is null)
{
return;
}

if (s_controlRecycling.TryGetValue(factory, out var shared))
{
if (ReferenceEquals(shared, controlRecycling))
{
return;
}

if (shared is ControlRecycling sharedRecycling && controlRecycling is ControlRecycling localRecycling)
{
if (sharedRecycling.TryToUseIdAsKey != localRecycling.TryToUseIdAsKey)
{
sharedRecycling.TryToUseIdAsKey = localRecycling.TryToUseIdAsKey;
}

ControlRecyclingDataTemplate.SetControlRecycling(this, sharedRecycling);
return;
}

if (controlRecycling is ControlRecycling)
{
ControlRecyclingDataTemplate.SetControlRecycling(this, shared);
}

return;
}

if (controlRecycling is ControlRecycling defaultRecycling)
{
controlRecycling = new ControlRecycling
{
TryToUseIdAsKey = defaultRecycling.TryToUseIdAsKey
};
ControlRecyclingDataTemplate.SetControlRecycling(this, controlRecycling);
}

s_controlRecycling.Add(factory, controlRecycling);
}

private void InitializeDefaultDataTemplates()
{
if (_contentControl?.DataTemplates is null)
Expand Down Expand Up @@ -386,38 +334,30 @@ private void Initialize(IDock? layout)

layout.Factory.DockControls.Add(this);

InitializeControlRecycling();
_factoryService.InitializeControlRecycling(this);
UpdateManagedWindowLayer(layout);

if (InitializeFactory)
{
layout.Factory.ContextLocator ??= new Dictionary<string, Func<object?>>();
layout.Factory.DockableLocator ??= new Dictionary<string, Func<IDockable?>>();
layout.Factory.DefaultContextLocator ??= GetContext;
layout.Factory.DefaultHostWindowLocator ??= GetHostWindow;
if (layout.Factory.DefaultContextLocator is null)
{
layout.Factory.DefaultContextLocator = ResolveDefaultContext;
}

if (layout.Factory.HostWindowLocator is null)
if (layout.Factory.DefaultHostWindowLocator is null)
{
layout.Factory.HostWindowLocator = new Dictionary<string, Func<IHostWindow?>>
{
[nameof(IDockWindow)] = GetHostWindow
};
layout.Factory.DefaultHostWindowLocator = ResolveDefaultHostWindow;
}

IHostWindow GetHostWindow()
if (layout.Factory.HostWindowLocator is null)
{
if (HostWindowFactory is { } factory)
layout.Factory.HostWindowLocator = new Dictionary<string, Func<IHostWindow?>>
{
return factory();
}

var hostMode = DockSettings.ResolveFloatingWindowHostMode(layout as IRootDock);
return hostMode == DockFloatingWindowHostMode.Managed
? new ManagedHostWindow(_dockManagerOptions)
: new HostWindow(_dockManagerOptions);
[nameof(IDockWindow)] = ResolveDefaultHostWindow
};
}

object? GetContext() => DefaultContext;
}

if (InitializeLayout)
Expand Down Expand Up @@ -446,6 +386,11 @@ private void DeInitialize(IDock? layout)

layout.Factory.DockControls.Remove(this);

_activationOrder.Clear();
_activationCounter = 0;

_factoryService.CleanupFactory(this, layout);

if (InitializeLayout)
{
if (layout.Close.CanExecute(null))
Expand Down Expand Up @@ -477,6 +422,21 @@ private void InitializeCommandBars()
}
}

internal object? ResolveDefaultContext() => DefaultContext;

internal IHostWindow? ResolveDefaultHostWindow()
{
if (HostWindowFactory is { } factory)
{
return factory();
}

var hostMode = DockSettings.ResolveFloatingWindowHostMode(Layout as IRootDock);
return hostMode == DockFloatingWindowHostMode.Managed
? new ManagedHostWindow(_dockManagerOptions)
: new HostWindow(_dockManagerOptions);
}

private void AttachFactoryEvents(IFactory? factory)
{
if (ReferenceEquals(_subscribedFactory, factory))
Expand Down
14 changes: 14 additions & 0 deletions src/Dock.Avalonia/Controls/ManagedWindowLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.VisualTree;
using Dock.Avalonia.Internal;
using Dock.Avalonia.Mdi;
using Dock.Model.Controls;
using Dock.Model.Core;
Expand Down Expand Up @@ -78,6 +79,19 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
}
}

/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);

DetachDock();

if (Dock?.Factory is { } factory)
{
ManagedWindowRegistry.UnregisterLayer(factory, this);
}
}

/// <summary>
/// Attempts to locate the managed window layer for a visual.
/// </summary>
Expand Down
Loading
Loading