Browse Source

Merge pull request #5566 from AvaloniaUI/fixes/tabcontrol-leak

Fix TabControl/TabItem leak
Dan Walmsley 4 years ago
parent
commit
8f926a3cc4

+ 52 - 3
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@@ -1,4 +1,10 @@
+using System;
+using System.Collections.Generic;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.LogicalTree;
+using Avalonia.Reactive;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls.Generators
 {    
@@ -16,11 +22,15 @@ namespace Avalonia.Controls.Generators
         {
             var tabItem = (TabItem)base.CreateContainer(item);
 
-            tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty];
+            tabItem.Bind(TabItem.TabStripPlacementProperty, new OwnerBinding<Dock>(
+                tabItem,
+                TabControl.TabStripPlacementProperty));
 
             if (tabItem.HeaderTemplate == null)
             {
-                tabItem[~HeaderedContentControl.HeaderTemplateProperty] = Owner[~ItemsControl.ItemTemplateProperty];
+                tabItem.Bind(TabItem.HeaderTemplateProperty, new OwnerBinding<IDataTemplate>(
+                    tabItem,
+                    TabControl.ItemTemplateProperty));
             }
 
             if (tabItem.Header == null)
@@ -40,10 +50,49 @@ namespace Avalonia.Controls.Generators
 
             if (!(tabItem.Content is IControl))
             {
-                tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty];
+                tabItem.Bind(TabItem.ContentTemplateProperty, new OwnerBinding<IDataTemplate>(
+                    tabItem,
+                    TabControl.ContentTemplateProperty));
             }
 
             return tabItem;
         }
+
+        private class OwnerBinding<T> : SingleSubscriberObservableBase<T>
+        {
+            private readonly TabItem _item;
+            private readonly StyledProperty<T> _ownerProperty;
+            private IDisposable _ownerSubscription;
+            private IDisposable _propertySubscription;
+
+            public OwnerBinding(TabItem item, StyledProperty<T> ownerProperty)
+            {
+                _item = item;
+                _ownerProperty = ownerProperty;
+            }
+
+            protected override void Subscribed()
+            {
+                _ownerSubscription = ControlLocator.Track(_item, 0, typeof(TabControl)).Subscribe(OwnerChanged);
+            }
+
+            protected override void Unsubscribed()
+            {
+                _ownerSubscription?.Dispose();
+                _ownerSubscription = null;
+            }
+
+            private void OwnerChanged(ILogical c)
+            {
+                _propertySubscription?.Dispose();
+                _propertySubscription = null;
+
+                if (c is TabControl tabControl)
+                {
+                    _propertySubscription = tabControl.GetObservable(_ownerProperty)
+                        .Subscribe(x => PublishNext(x));
+                }
+            }
+        }
     }
 }

+ 1 - 0
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@@ -374,6 +374,7 @@ namespace Avalonia.Controls.UnitTests
                     new TextBlock { Tag = "bar", Text = x }),
                 Items = new[] { "Foo" },
             };
+            var root = new TestRoot(target);
 
             ApplyTemplate(target);
             ((ContentPresenter)target.ContentPart).UpdateChild();

+ 37 - 1
tests/Avalonia.LeakTests/ControlTests.cs

@@ -313,7 +313,6 @@ namespace Avalonia.LeakTests
             }
         }
 
-
         [Fact]
         public void Slider_Is_Freed()
         {
@@ -347,6 +346,43 @@ namespace Avalonia.LeakTests
             }
         }
 
+        [Fact]
+        public void TabItem_Is_Freed()
+        {
+            using (Start())
+            {
+                Func<Window> run = () =>
+                {
+                    var window = new Window
+                    {
+                        Content = new TabControl
+                        {
+                            Items = new[] { new TabItem() }
+                        }
+                    };
+
+                    window.Show();
+
+                    // Do a layout and make sure that TabControl and TabItem gets added to visual tree.
+                    window.LayoutManager.ExecuteInitialLayoutPass();
+                    var tabControl = Assert.IsType<TabControl>(window.Presenter.Child);
+                    Assert.IsType<TabItem>(tabControl.Presenter.Panel.Children[0]);
+
+                    // Clear the items and ensure the TabItem is removed.
+                    tabControl.Items = null;
+                    window.LayoutManager.ExecuteLayoutPass();
+                    Assert.Empty(tabControl.Presenter.Panel.Children);
+
+                    return window;
+                };
+
+                var result = run();
+
+                dotMemory.Check(memory =>
+                    Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TabItem>()).ObjectsCount));
+            }
+        }
+
         [Fact]
         public void RendererIsDisposed()
         {