From b2b95a6f7a7a492b0d56573315eed937ef6302ce Mon Sep 17 00:00:00 2001
From: michael-hawker <24302614+michael-hawker@users.noreply.github.com>
Date: Mon, 26 Jun 2023 16:36:57 -0700
Subject: [PATCH 1/5] Add sample for FrameworkElementExtensions.Ancestor/Type
---
.../FrameworkElementAncestorSample.xaml | 22 +++++++++++++++++++
.../FrameworkElementAncestorSample.xaml.cs | 17 ++++++++++++++
.../samples/FrameworkElementExtensions.md | 8 +++----
3 files changed, 42 insertions(+), 5 deletions(-)
create mode 100644 components/Extensions/samples/FrameworkElementAncestorSample.xaml
create mode 100644 components/Extensions/samples/FrameworkElementAncestorSample.xaml.cs
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
From 6468adee023cbb1174644f665cbaae86136954b8 Mon Sep 17 00:00:00 2001
From: michael-hawker <24302614+michael-hawker@users.noreply.github.com>
Date: Tue, 27 Jun 2023 16:39:38 -0700
Subject: [PATCH 2/5] Fixes #106 to change binding type of AncestorType in
FrameworkElementExtensions.RelativeAncestor to FrameworkElement over
DependencyObject
Code scoped to FrameworkElement in callback, so doesn't work with just DependencyObject as-is.
---
.../Element/FrameworkElementExtensions.RelativeAncestor.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/components/Extensions/src/Element/FrameworkElementExtensions.RelativeAncestor.cs b/components/Extensions/src/Element/FrameworkElementExtensions.RelativeAncestor.cs
index 701036cb..f260cfbb 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);
}
From 6854bb99e11de7d742488ce112680597584a8f6a Mon Sep 17 00:00:00 2001
From: michael-hawker <24302614+michael-hawker@users.noreply.github.com>
Date: Tue, 27 Jun 2023 17:00:16 -0700
Subject: [PATCH 3/5] Add initial test case for
FrameworkElementExtension.RelativeAncestor
Fixes malaligned namespaces in tests.
---
.../FrameworkElementExtensions.Mouse.cs | 2 +-
.../tests/BitmapIconExtensionTestPage.xaml | 2 +-
.../tests/BitmapIconExtensionTestPage.xaml.cs | 2 +-
.../tests/BitmapIconExtensionTests.cs | 2 --
...ElementExtensionsTests.RelativeAncestor.cs | 30 +++++++++++++++++++
...tRelativeAncestorDataTemplateTestPage.xaml | 24 +++++++++++++++
...lativeAncestorDataTemplateTestPage.xaml.cs | 16 ++++++++++
.../tests/Extensions.Tests.projitems | 8 +++++
8 files changed, 81 insertions(+), 5 deletions(-)
create mode 100644 components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs
create mode 100644 components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml
create mode 100644 components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml.cs
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/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..daa0f36a
--- /dev/null
+++ b/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs
@@ -0,0 +1,30 @@
+// 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");
+ }
+}
diff --git a/components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml b/components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml
new file mode 100644
index 00000000..4bf7ee5c
--- /dev/null
+++ b/components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+ 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
From 05ad5d1db74ae9fb14ca1467267a17d708ba8111 Mon Sep 17 00:00:00 2001
From: michael-hawker <24302614+michael-hawker@users.noreply.github.com>
Date: Thu, 29 Jun 2023 16:33:35 -0700
Subject: [PATCH 4/5] Add more test cases to try and track issue for #107
---
...ElementExtensionsTests.RelativeAncestor.cs | 124 ++++++++++++++++++
...tRelativeAncestorDataTemplateTestPage.xaml | 5 +-
2 files changed, 127 insertions(+), 2 deletions(-)
diff --git a/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs b/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs
index daa0f36a..3a2eeca6 100644
--- a/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs
+++ b/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs
@@ -27,4 +27,128 @@ public void FrameworkElementExtension_RelativeAncestor_InDataTemplate(FrameworkE
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
index 4bf7ee5c..8eaeec51 100644
--- a/components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml
+++ b/components/Extensions/tests/Element/FrameworkElementRelativeAncestorDataTemplateTestPage.xaml
@@ -8,8 +8,7 @@
mc:Ignorable="d">
-
+ 2
3
+
+
From 022b4bfde61f945ae6e4d1aeae9c61dd7a5190cc Mon Sep 17 00:00:00 2001
From: michael-hawker <24302614+michael-hawker@users.noreply.github.com>
Date: Thu, 29 Jun 2023 16:34:01 -0700
Subject: [PATCH 5/5] Add unloaded event, but didn't see impact tests pass
before and after for #107
---
.../FrameworkElementExtensions.RelativeAncestor.cs | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/components/Extensions/src/Element/FrameworkElementExtensions.RelativeAncestor.cs b/components/Extensions/src/Element/FrameworkElementExtensions.RelativeAncestor.cs
index f260cfbb..84930b7d 100644
--- a/components/Extensions/src/Element/FrameworkElementExtensions.RelativeAncestor.cs
+++ b/components/Extensions/src/Element/FrameworkElementExtensions.RelativeAncestor.cs
@@ -78,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!);
}
}
}