Memory leak in nested DockSites

Docking/MDI for WPF Forum

The latest build of this product (v25.1.1) was released 3 months ago, which was before this thread was created.
Posted 1 month 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 (6)

Posted 1 month ago by Denys
Avatar

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

Posted 1 month 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

Posted 1 month ago by Denys
Avatar

Hello.
Thank you for your answer.

Yes. When I removed x:Name, objects began to be collected by the garbage collector.
However, occasionally, even in the SampleBrowser, some objects remained ubdestroyed. I have not yet been able to discern a pattern. I also have questions regarding the use of a combination of view models and separate DockingWindows.
For now, I will mark this message as an answer, but if you don't mind, and if I figure out the pattern, I will come back with this question and new information.
Have a good day.

Posted 1 month ago by Denys
Avatar

Hello

Unfortunately, the problem remains. If the nested DockSite uses binding to DocumentItemsSource/ToolItemsSource and the source property is ObservableCollection, then the nested DockSite is not unrsubscibed and remains in memory.
I checked this as follows:
I created a child class DockSite from ActiproSoftware.Windows.Controls.Docking.DockSite and overrode the OnToolItemsSourceChanged and OnDocumentItemsSourceChanged methods
When switching the external data context, the external DockSite is unsubscribed and these methods are called (both when subscribing and unsubscribing), but in the nested DockSite, these methods are called only once, when subscribing. When unsubscribing, these methods are not called.
If I change type on List<> then the problem is solved. But if it is still acceptable for Tools, it is not acceptable for Documents.

https://prnt.sc/nrUlRvOYJhFj
Best regards

[Modified 1 month ago]

Posted 1 month ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hello,

We can look into it further, but for complex scenarios like this, we really need to be able to debug with your configuration.  Please put together a new simple sample project that shows the issue occurring and send that to our support address, referencing this thread in your email.  Try to keep it as minimal as possible, and be sure to exclude the bin/obj folders from the .zip you send so it doesn't get spam blocked.  Thanks!


Actipro Software Support

Posted 1 month ago by Denys
Avatar

Hello

Project was sent on support@.

Thank you

[Modified 1 month ago]

Add Comment

Please log in to a validated account to post comments.