Memory leak in nested DockSites

Docking/MDI for WPF Forum

The latest build of this product (v25.1.1) was released 2 months ago, which was before this thread was created.
Posted 1 days ago by Denys
Version: 25.1.1
Platform: .NET 9
Environment: Windows 11 (64-bit)
Avatar

Hi developers.

In my project, I actively use nested DockSites.

I discovered that some of my view models are not being destroyed when closing DocumentWindows that contain nested DockSites.

I started testing it in your SampleBrowser application, under DockingSamples/QuickStart/NestedDockSites, and found the same issue.

When I close a DocumentWindow that contains a nested DockSite, the DockSite is not destroyed. It doesn’t matter whether there are any ToolWindows inside or not (see this reference https://www.actiprosoftware.com/community/thread/3997/memory-leak-in-docksite).

When I first close the DocumentWindows inside the nested DockSite, I can see that those DocumentWindows are destroeyd correctly. However, the DockSite itself is still not destroyed.

For testing I used ObjectTracker based on ConditionalWeakTable and DockSite1 based on DockSite, DocumentWindow1 based on DocumentWindow, ToolWindow1 based on ToolWindow. ConditionalWeakTable stores its keys as weak references and automatically removes them when the object used as the key is garbage-collected.

If I switch to another sample and then return to NestedDockSites, all the old DockSites, DocumentWindows, and ToolWindows are destroyed, and new objects are created.

In my project it is very important to use nested DockSites in both DocumentWindows and DockSite.Workspace.

Thanks in advance.

My MainControl.xaml for NestedDockSites:

<sampleBrowser:ProductItemControl 
x:Class="ActiproSoftware.ProductSamples.DockingSamples.QuickStart.NestedDockSites.MainControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:docking="http://schemas.actiprosoftware.com/winfx/xaml/docking"
xmlns:sampleBrowser="clr-namespace:ActiproSoftware.SampleBrowser"
xmlns:docking1="clr-namespace:ActiproSoftware.Windows.Controls.Docking">
 
<sampleBrowser:ProductItemControl.SideBarContent>
<StackPanel>
 
<Expander Style="{StaticResource SampleHeaderDescriptionExpanderStyle}">
<TextBlock Style="{StaticResource SampleHeaderDescriptionTextBlockStyle}">
    Dock sites are completely self-contained and can even be nested.
    This allows you to put a child DockSite within a document window or tool window.
    <LineBreak /><LineBreak />
    In this sample we have an outer dock site that just has a couple tool windows and MDI.
    The MDI contains two document windows, each with their own nested dock sites.
    Those two dock sites have their own docking windows.
    <LineBreak /><LineBreak />
    When you drag tool windows around from any of the dock sites, notice how they remain self-contained within their ancestor dock site.  
    Also try to float one of the tool windows from within a document window's dock site.
    Then switch to the other document window.
    Notice how the floating tool window automatically closes.
    When you switch back to the original document window, the floating window automatically reappears.
    <LineBreak /><LineBreak />
    It's features like this that make Actipro Docking/MDI stand above the competition.
</TextBlock>
</Expander>
            <Button Click="ButtonGC_OnClick" Content="GC"/>
            <TextBox x:Name="TextBox" IsReadOnly="True" AcceptsReturn="True" Height="200" TextWrapping="Wrap"/>
 
</StackPanel>
</sampleBrowser:ProductItemControl.SideBarContent>
 
    <!-- Outer DockSite -->
    <docking1:DockSite1 x:Name="outerDockSite" AreDocumentWindowsDestroyedOnClose="True">
        <docking:SplitContainer>
            <docking:Workspace>
                <docking:TabbedMdiHost>
                    <docking:TabbedMdiContainer>
 
                        <docking1:DocumentWindow1 Title="Document1">
                            <!-- Inner DockSite 1 -->
                            <docking1:DockSite1 x:Name="innerDockSite1" AreDocumentWindowsDestroyedOnClose="True">
                                <docking:ToolWindowContainer>
                                    <docking1:ToolWindow1 Title="Inner DockSite 1-1">
                                        <TextBox BorderThickness="0" AcceptsReturn="True" />
                                    </docking1:ToolWindow1>
                                    <docking1:ToolWindow1 Title="Inner DockSite 1-2">
                                        <TextBox BorderThickness="0" AcceptsReturn="True" />
                                    </docking1:ToolWindow1>
                                </docking:ToolWindowContainer>
                            </docking1:DockSite1>
                        </docking1:DocumentWindow1>
 
                        <docking1:DocumentWindow1 Title="Document2">
                            <!-- Inner DockSite 2 -->
                            <docking1:DockSite1 x:Name="innerDockSite2" AreDocumentWindowsDestroyedOnClose="True">
                                <docking:SplitContainer>
                                    <docking:ToolWindowContainer>
                                        <docking1:ToolWindow1 Title="Inner DockSite 2-1">
                                            <TextBox BorderThickness="0" AcceptsReturn="True" />
                                        </docking1:ToolWindow1>
                                        <docking1:ToolWindow1 Title="Inner DockSite 2-2">
                                            <TextBox BorderThickness="0" AcceptsReturn="True" />
                                        </docking1:ToolWindow1>
                                    </docking:ToolWindowContainer>
 
                                    <docking:Workspace>
                                        <docking:TabbedMdiHost>
                                            <docking:TabbedMdiContainer>
                                                <docking1:DocumentWindow1 Title="Inner Document 1">
                                                    <TextBox BorderThickness="0" AcceptsReturn="True" />
                                                </docking1:DocumentWindow1>
                                                <docking1:DocumentWindow1 Title="Inner Document 2">
                                                    <TextBox BorderThickness="0" AcceptsReturn="True" />
                                                </docking1:DocumentWindow1>
                                            </docking:TabbedMdiContainer>
                                        </docking:TabbedMdiHost>
                                    </docking:Workspace>
                                </docking:SplitContainer>
                            </docking1:DockSite1>
                        </docking1:DocumentWindow1>
 
                    </docking:TabbedMdiContainer>
                </docking:TabbedMdiHost>
            </docking:Workspace>
 
            <docking:ToolWindowContainer>
                <docking1:ToolWindow1 Title="Outer DockSite 1">
                    <TextBox BorderThickness="0" AcceptsReturn="True" />
                </docking1:ToolWindow1>
                <docking1:ToolWindow1 Title="Outer DockSite 2">
                    <TextBox BorderThickness="0" AcceptsReturn="True" />
                </docking1:ToolWindow1>
            </docking:ToolWindowContainer>
        </docking:SplitContainer>
    </docking1:DockSite1>
 
</sampleBrowser:ProductItemControl>

My MainControl.xaml.cs for NestedDockSites:

using ActiproSoftware.Windows.Controls.Docking;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows;
 
namespace ActiproSoftware.ProductSamples.DockingSamples.QuickStart.NestedDockSites {
 
/// <summary>
/// Provides the main user control for this sample.
/// </summary>
public partial class MainControl {
 
/////////////////////////////////////////////////////////////////////////////////////////////////////
// OBJECT
/////////////////////////////////////////////////////////////////////////////////////////////////////
 
/// <summary>
/// Initializes an instance of the <c>MainControl</c> class.
/// </summary>
public MainControl() {
InitializeComponent();
}
 
        private void ButtonGC_OnClick(object sender, RoutedEventArgs e)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
 
            var sb = new StringBuilder();
            Dictionary<Type, int> res = ObjectTracker.GetCounts();
            foreach (KeyValuePair<Type, int> kvp in res.OrderBy(d => d.Key.FullName))
            {
                sb.AppendLine($"{kvp.Key}: {kvp.Value}");
                sb.AppendLine($"\t{string.Join(", ", ObjectTracker.GetObjects(kvp.Key).OfType<int>())}");
            }
 
            TextBox.Text = sb.ToString();
        }
    }
}
 
namespace ActiproSoftware.Windows.Controls.Docking
{
    public class DockSite1 : DockSite
    {
        private static int _id;
        public DockSite1()
        {
            ObjectTracker.Register(this, _id++);
        }
    }
 
    public class DocumentWindow1 : DocumentWindow
    {
        private static int _id;
        public DocumentWindow1()
        {
            ObjectTracker.Register(this, _id++);
        }
    }
 
    public class ToolWindow1 : ToolWindow
    {
        private static int _id;
        public ToolWindow1()
        {
            ObjectTracker.Register(this, _id++);
        }
    }
 
    public static class ObjectTracker
    {
        private static readonly ConcurrentDictionary<Type, ConditionalWeakTable<object, object>> Tables = [];
 
        public static void Register(object obj, object id)
        {
            ArgumentNullException.ThrowIfNull(obj);
            Type type = obj.GetType();
            ConditionalWeakTable<object, object> table = Tables.GetOrAdd(type, _ => new ConditionalWeakTable<object, object>());
            table.Add(obj, id);
        }
 
        public static Dictionary<Type, int> GetCounts()
        {
            var counts = new Dictionary<Type, int>();
            foreach (KeyValuePair<Type, ConditionalWeakTable<object, object>> kvp in Tables)
                counts[kvp.Key] = kvp.Value.Count();
            return counts;
        }
 
        public static IEnumerable<object> GetObjects(Type type)
        {
            foreach (KeyValuePair<Type, ConditionalWeakTable<object, object>> kvp in Tables.Where(t => t.Key == type))
            {
                foreach (KeyValuePair<object, object> k in kvp.Value)
                {
                    yield return k.Value;
                }
            }
        }
    }
}

Comments (2)

Posted 1 days ago by Denys
Avatar

I active use DocumentItemsSource and ToolItemsSource with ViewModels but some time I use definition DocumentWindow and ToolWindow in Xaml

Posted 22 hours ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hello,

I used Jetbrains dotMemory to track down what is causing the nested DockSite reference.  I found that using x:Name in XAML of the MainControl.xaml file is the cause there.  For instance, if you change this:

<docking:DockSite x:Name="innerDockSite1">

to this:

<docking:DockSite>

Then there is no reference retained when I close the Document1 that contains that innerDockSite1.  That being said you may also need to force a garbage collection to get something else internal to WPF to remove a weak ref as well.

To sum up, if I removed the x:Name in the containing XAML, ran the sample, closed Document1, and in dotMemory clicked "Force GC", when I compared the snapshots before and after, there were two DockSites that survived and one (the one I closed) was "dead".


Actipro Software Support

Add Comment

Please log in to a validated account to post comments.