diff --git a/components/Extensions/samples/FrameworkElementAncestorSample.xaml b/components/Extensions/samples/FrameworkElementAncestorSample.xaml
new file mode 100644
index 00000000..62322a79
--- /dev/null
+++ b/components/Extensions/samples/FrameworkElementAncestorSample.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/components/Extensions/samples/FrameworkElementAncestorSample.xaml.cs b/components/Extensions/samples/FrameworkElementAncestorSample.xaml.cs
new file mode 100644
index 00000000..e8038428
--- /dev/null
+++ b/components/Extensions/samples/FrameworkElementAncestorSample.xaml.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace ExtensionsExperiment.Samples;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+[ToolkitSample(id: nameof(FrameworkElementAncestorSample), nameof(FrameworkElementAncestorSample), description: $"A sample for showing how to use the FrameworkElementExtensions.Ancestor attached property.")]
+public sealed partial class FrameworkElementAncestorSample : Page
+{
+ public FrameworkElementAncestorSample()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Extensions/samples/FrameworkElementExtensions.md b/components/Extensions/samples/FrameworkElementExtensions.md
index 6990a932..a6f9358a 100644
--- a/components/Extensions/samples/FrameworkElementExtensions.md
+++ b/components/Extensions/samples/FrameworkElementExtensions.md
@@ -102,11 +102,9 @@ The `AncestorType` attached property will walk the visual tree from the attached
Here is an example of how this can be used:
-```xaml
-
-```
+> [!SAMPLE FrameworkElementAncestorSample]
+
+While this example is trivial, it shows you how to properly setup and bind to the parent element's property, in this case `Spacing`.
## Cursor
diff --git a/components/Extensions/src/Element/FrameworkElementExtensions.Mouse.cs b/components/Extensions/src/Element/FrameworkElementExtensions.Mouse.cs
index 6cbf3eb1..2866f666 100644
--- a/components/Extensions/src/Element/FrameworkElementExtensions.Mouse.cs
+++ b/components/Extensions/src/Element/FrameworkElementExtensions.Mouse.cs
@@ -14,7 +14,7 @@
namespace CommunityToolkit.WinUI;
// TODO: Note: Windows App SDK doesn't support this (need to use Protected Cursor), but we still use this extension for Sizer controls.
-// For now rather than not porting, we'll just exclude on Windows App SDK platforms. Fenced other blocks below and support both equivelent types, but don't have a general way to set cursor on window yet in PointerEntered/Exited. If in end, FrameworkElement gets non-protected Cursor property like WPF, then this extension also isn't needed.
+// For now rather than not porting, we'll just exclude on Windows App SDK platforms. Fenced other blocks below and support both equivalent types, but don't have a general way to set cursor on window yet in PointerEntered/Exited. If in end, FrameworkElement gets non-protected Cursor property like WPF, then this extension also isn't needed.
// See https://github.com/microsoft/microsoft-ui-xaml/issues/4834
#if !WINAPPSDK
diff --git a/components/Extensions/src/Element/FrameworkElementExtensions.RelativeAncestor.cs b/components/Extensions/src/Element/FrameworkElementExtensions.RelativeAncestor.cs
index 701036cb..84930b7d 100644
--- a/components/Extensions/src/Element/FrameworkElementExtensions.RelativeAncestor.cs
+++ b/components/Extensions/src/Element/FrameworkElementExtensions.RelativeAncestor.cs
@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System;
-
namespace CommunityToolkit.WinUI;
///
@@ -39,7 +37,7 @@ public static void SetAncestor(DependencyObject obj, object value)
/// Gets the Type of Ancestor to look for from this element.
///
/// Type of Ancestor to look for from this element
- public static Type GetAncestorType(DependencyObject obj)
+ public static Type GetAncestorType(FrameworkElement obj)
{
return (Type)obj.GetValue(AncestorTypeProperty);
}
@@ -47,7 +45,7 @@ public static Type GetAncestorType(DependencyObject obj)
///
/// Sets the to look for from this element and place in the .
///
- public static void SetAncestorType(DependencyObject obj, Type value)
+ public static void SetAncestorType(FrameworkElement obj, Type value)
{
obj.SetValue(AncestorTypeProperty, value);
}
@@ -80,6 +78,19 @@ private static void FrameworkElement_Loaded(object sender, RoutedEventArgs e)
if (sender is FrameworkElement fe)
{
SetAncestor(fe, fe.FindAscendant(GetAncestorType(fe))!);
+
+ fe.Unloaded -= FrameworkElement_Unloaded;
+ fe.Unloaded += FrameworkElement_Unloaded;
+ }
+ }
+
+ private static void FrameworkElement_Unloaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement fe)
+ {
+ fe.Unloaded -= FrameworkElement_Unloaded;
+
+ SetAncestor(fe, null!);
}
}
}
diff --git a/components/Extensions/tests/BitmapIconExtensionTestPage.xaml b/components/Extensions/tests/BitmapIconExtensionTestPage.xaml
index 00098253..0a205fc0 100644
--- a/components/Extensions/tests/BitmapIconExtensionTestPage.xaml
+++ b/components/Extensions/tests/BitmapIconExtensionTestPage.xaml
@@ -1,4 +1,4 @@
-
/// An empty page that can be used on its own or navigated to within a Frame.
diff --git a/components/Extensions/tests/BitmapIconExtensionTests.cs b/components/Extensions/tests/BitmapIconExtensionTests.cs
index 774c12bc..3193c2a6 100644
--- a/components/Extensions/tests/BitmapIconExtensionTests.cs
+++ b/components/Extensions/tests/BitmapIconExtensionTests.cs
@@ -4,8 +4,6 @@
using CommunityToolkit.Tests;
using CommunityToolkit.Tooling.TestGen;
-using CommunityToolkit.WinUI;
-using ExtensionsExperiment.Tests;
namespace ExtensionsComponent.Tests;
diff --git a/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs b/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs
new file mode 100644
index 00000000..3a2eeca6
--- /dev/null
+++ b/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs
@@ -0,0 +1,154 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Tests;
+using CommunityToolkit.Tooling.TestGen;
+
+namespace ExtensionsComponent.Tests;
+
+[TestClass]
+public partial class FrameworkElementExtensionsTests : VisualUITestBase
+{
+ [TestCategory("FrameworkElementExtension")]
+ [UIThreadTestMethod]
+ public void FrameworkElementExtension_RelativeAncestor_InDataTemplate(FrameworkElementRelativeAncestorDataTemplateTestPage page)
+ {
+ var list = page.FindDescendant();
+
+ Assert.IsNotNull(list, "Couldn't find listview");
+
+ int count = 0;
+ foreach (var item in list.FindDescendants().OfType())
+ {
+ count++;
+ Assert.AreEqual("Hello", item.Text, "Text didn't match binding of ancestor tag property");
+ }
+
+ Assert.AreEqual(3, count, "Didn't find three textblocks");
+ }
+
+ [TestCategory("FrameworkElementExtension")]
+ [UIThreadTestMethod]
+ public async Task FrameworkElementExtension_RelativeAncestor_FreeParentBaseline(FrameworkElementRelativeAncestorDataTemplateTestPage page)
+ {
+ var text = page.FindDescendant();
+
+ Assert.IsNotNull(text, "Couldn't find TextBox");
+
+ // Grab a hold of a weak reference for TextBox so we can detect when it unloads.
+ WeakReference textRef = new(text);
+ text = null;
+
+ var parent = page.FindDescendant();
+
+ Assert.IsNotNull(parent, "Couldn't find parent Grid");
+
+ // Remove all the children from the grid to simulate it unloading.
+ VisualTreeHelper.DisconnectChildrenRecursive(parent);
+ parent.Children.Clear();
+ parent = null;
+
+ // Wait for the Visual Tree to perform removals and clean-up
+ await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { });
+
+ // Wait for the .NET Garbage Collector to clean up references
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ Assert.IsFalse(textRef.IsAlive, "TextBox is still alive...");
+ }
+
+ [TestCategory("FrameworkElementExtension")]
+ [UIThreadTestMethod]
+ public async Task FrameworkElementExtension_RelativeAncestor_FreeParent(FrameworkElementRelativeAncestorDataTemplateTestPage page)
+ {
+ var list = page.FindDescendant();
+
+ Assert.IsNotNull(list, "Couldn't find listview");
+
+ // Grab a hold of a weak reference for ListView so we can detect when it unloads.
+ WeakReference listRef = new(list);
+ list = null;
+
+ // Remove all the children from the grid to simulate it unloading.
+ VisualTreeHelper.DisconnectChildrenRecursive(page);
+ page.Content = null;
+
+ // Wait for the Visual Tree to perform removals and clean-up
+ await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { });
+
+ // Wait for the .NET Garbage Collector to clean up references
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ Assert.IsFalse(listRef.IsAlive, "ListView is still alive...");
+ }
+
+ [TestCategory("FrameworkElementExtension")]
+ [UIThreadTestMethod]
+ public async Task FrameworkElementExtension_RelativeAncestor_FreePageNavigation()
+ {
+ TaskCompletionSource taskCompletionSource = new();
+ var frame = new Frame();
+ frame.Navigated += OnNavigated;
+
+ await LoadTestContentAsync(frame);
+
+ // Navigate to the new page.
+ frame.Navigate(typeof(FrameworkElementRelativeAncestorDataTemplateTestPage), null, new SuppressNavigationTransitionInfo());
+
+ async void OnNavigated(object sender, NavigationEventArgs e)
+ {
+ frame.Navigated -= OnNavigated;
+
+ // Wait for first Render pass
+ await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { });
+
+ taskCompletionSource.SetResult(true);
+ }
+
+ // Wait for frame to navigate/load
+ var result = await taskCompletionSource.Task;
+ Assert.IsTrue(result, "Navigation didn't complete");
+
+ // Find the ListView we want to track
+
+ var list = frame.FindDescendant();
+
+ Assert.IsNotNull(list, "Couldn't find listview");
+
+ // Grab a hold of a weak reference for ListView so we can detect when it unloads.
+ WeakReference listRef = new(list);
+ list = null;
+
+ TaskCompletionSource taskCompletionSource2 = new();
+ frame.Navigated += OnNavigated2;
+
+ async void OnNavigated2(object sender, NavigationEventArgs e)
+ {
+ frame.Navigated -= OnNavigated2;
+
+ // Wait for first Render pass
+ await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { });
+
+ taskCompletionSource2.SetResult(true);
+ }
+
+ // Navigate to any other page to unload our other one
+ frame.Navigate(typeof(BitmapIconExtensionTestPage), null, new SuppressNavigationTransitionInfo());
+
+ // Wait for navigation to complete
+ result = await taskCompletionSource2.Task;
+ Assert.IsTrue(result, "Navigation didn't complete 2");
+
+ // Wait for the Visual Tree to perform removals and clean-up
+ await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { });
+
+ // Wait for the .NET Garbage Collector to clean up references
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ Assert.IsFalse(listRef.IsAlive, "ListView is still alive...");
+ }
+}
diff --git a/components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml b/components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml
new file mode 100644
index 00000000..8eaeec51
--- /dev/null
+++ b/components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+ 1
+ 2
+ 3
+
+
+
+
+
diff --git a/components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml.cs b/components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml.cs
new file mode 100644
index 00000000..1c2447bb
--- /dev/null
+++ b/components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace ExtensionsComponent.Tests;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+public sealed partial class FrameworkElementRelativeAncestorDataTemplateTestPage : Page
+{
+ public FrameworkElementRelativeAncestorDataTemplateTestPage()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Extensions/tests/Extensions.Tests.projitems b/components/Extensions/tests/Extensions.Tests.projitems
index 9c300eab..e77c23bd 100644
--- a/components/Extensions/tests/Extensions.Tests.projitems
+++ b/components/Extensions/tests/Extensions.Tests.projitems
@@ -15,6 +15,10 @@
+
+
+ FrameworkElementRelativeAncestorDataTemplateTestPage.xaml
+
@@ -22,5 +26,9 @@
DesignerMSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
\ No newline at end of file