Skip to content

Commit e5e4cc8

Browse files
author
reunion-maestro-bot
committed
Syncing content from committish 97ed2ed96398f86d4b859b9263bee9b35aaf8191
1 parent eb74b62 commit e5e4cc8

6 files changed

Lines changed: 228 additions & 15 deletions

File tree

src/controls/dev/Repeater/APITests/RepeaterTests.cs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,5 +755,132 @@ public void BringIntoViewOfExistingItemsDoesNotChangeScrollOffset()
755755
});
756756
}
757757

758+
// Verifies that an ItemsRepeater using a DataTemplate can be garbage
759+
// collected after elements are recycled to the RecyclePool and the
760+
// repeater is removed from the tree. The recycling step is critical —
761+
// it creates the RecyclePool on the DataTemplate and populates it,
762+
// which forms the reference cycle that must be breakable by the tracker.
763+
[TestMethod]
764+
public void VerifyRepeaterWithRecycledElementsDoesNotLeak()
765+
{
766+
WeakReference repeaterWeakRef = null;
767+
var data = new ObservableCollection<string>(
768+
Enumerable.Range(0, 5).Select(i => string.Format("Item #{0}", i)));
769+
770+
RunOnUIThread.Execute(() =>
771+
{
772+
var repeater = new ItemsRepeater()
773+
{
774+
ItemsSource = data,
775+
ItemTemplate = CreateDataTemplateWithContent(@"<TextBlock Text='{Binding}' Height='50' />"),
776+
};
777+
repeaterWeakRef = new WeakReference(repeater);
778+
Content = repeater;
779+
});
780+
781+
IdleSynchronizer.Wait();
782+
783+
RunOnUIThread.Execute(() =>
784+
{
785+
// Verify elements are realized, then clear the data source to
786+
// force all elements into the RecyclePool on the DataTemplate.
787+
Verify.IsNotNull(repeaterWeakRef.Target);
788+
var repeater = (ItemsRepeater)repeaterWeakRef.Target;
789+
Verify.IsNotNull(repeater.TryGetElement(0), "Element 0 should be realized.");
790+
data.Clear();
791+
});
792+
793+
IdleSynchronizer.Wait();
794+
795+
RunOnUIThread.Execute(() =>
796+
{
797+
// Remove from tree. The RecyclePool now holds recycled elements
798+
// that reference the DataTemplate and the repeater (as owner).
799+
Content = null;
800+
});
801+
802+
IdleSynchronizer.Wait();
803+
804+
// Force GC — the tracker_ref on ItemTemplateWrapper's DataTemplate
805+
// makes the reference visible to the XAML reference tracker, which
806+
// detects the RecyclePool cycle and breaks it during collection.
807+
for (int i = 0; i < 5 && repeaterWeakRef.IsAlive; i++)
808+
{
809+
GC.Collect();
810+
GC.WaitForPendingFinalizers();
811+
IdleSynchronizer.Wait();
812+
}
813+
814+
Verify.IsFalse(repeaterWeakRef.IsAlive,
815+
"ItemsRepeater should be collected after recycling elements and removal.");
816+
}
817+
818+
// Verifies that an ItemsRepeater using a RecyclingElementFactory (the
819+
// DataTemplateSelector equivalent) can be garbage collected after
820+
// elements are recycled and the repeater is removed from the tree.
821+
[TestMethod]
822+
public void VerifyRepeaterWithRecyclingElementFactoryDoesNotLeak()
823+
{
824+
WeakReference repeaterWeakRef = null;
825+
var data = new ObservableCollection<int>(Enumerable.Range(0, 6));
826+
827+
RunOnUIThread.Execute(() =>
828+
{
829+
var elementFactory = new RecyclingElementFactory()
830+
{
831+
RecyclePool = new RecyclePool(),
832+
};
833+
elementFactory.Templates["even"] = (DataTemplate)XamlReader.Load(
834+
@"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
835+
<TextBlock Text='even' Height='50' />
836+
</DataTemplate>");
837+
elementFactory.Templates["odd"] = (DataTemplate)XamlReader.Load(
838+
@"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
839+
<TextBlock Text='odd' Height='50' />
840+
</DataTemplate>");
841+
842+
elementFactory.SelectTemplateKey +=
843+
delegate (RecyclingElementFactory sender, SelectTemplateEventArgs args)
844+
{
845+
args.TemplateKey = ((int)args.DataContext % 2 == 0) ? "even" : "odd";
846+
};
847+
848+
var repeater = new ItemsRepeater()
849+
{
850+
ItemsSource = data,
851+
ItemTemplate = elementFactory,
852+
};
853+
repeaterWeakRef = new WeakReference(repeater);
854+
Content = repeater;
855+
});
856+
857+
IdleSynchronizer.Wait();
858+
859+
RunOnUIThread.Execute(() =>
860+
{
861+
// Clear items to force elements into the RecyclePool, then remove.
862+
data.Clear();
863+
});
864+
865+
IdleSynchronizer.Wait();
866+
867+
RunOnUIThread.Execute(() =>
868+
{
869+
Content = null;
870+
});
871+
872+
IdleSynchronizer.Wait();
873+
874+
for (int i = 0; i < 5 && repeaterWeakRef.IsAlive; i++)
875+
{
876+
GC.Collect();
877+
GC.WaitForPendingFinalizers();
878+
IdleSynchronizer.Wait();
879+
}
880+
881+
Verify.IsFalse(repeaterWeakRef.IsAlive,
882+
"ItemsRepeater with RecyclingElementFactory should be collected after recycling and removal.");
883+
}
884+
758885
}
759886
}

src/controls/dev/Repeater/ItemTemplateWrapper.cpp

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
#include "ItemTemplateWrapper.h"
66
#include "RecyclePool.h"
77
#include "ItemsRepeater.common.h"
8+
#include "FrameworkUdk/Containment.h"
9+
10+
// Bug 61574370: [1.8 Servicing][WASDK] Fix ItemsRepeater RecyclePool reference cycle memory leak
11+
#define WINAPPSDK_CHANGEID_61574370 61574370, WinAppSDK_1_8_9
812

913
ItemTemplateWrapper::ItemTemplateWrapper(winrt::DataTemplate const& dataTemplate)
1014
{
@@ -16,14 +20,47 @@ ItemTemplateWrapper::ItemTemplateWrapper(winrt::DataTemplateSelector const& data
1620
m_dataTemplateSelector = dataTemplateSelector;
1721
}
1822

23+
void ItemTemplateWrapper::EnableTracking(const ITrackerHandleManager* owner)
24+
{
25+
if (owner && m_dataTemplate)
26+
{
27+
m_trackedDataTemplate.emplace(owner, m_dataTemplate);
28+
m_dataTemplate = nullptr; // Release the raw ref; tracker_ref now owns it
29+
m_isTracking = true;
30+
}
31+
}
32+
33+
winrt::DataTemplate ItemTemplateWrapper::GetDataTemplate() const
34+
{
35+
return m_isTracking ? m_trackedDataTemplate->get() : m_dataTemplate;
36+
}
37+
1938
winrt::DataTemplate ItemTemplateWrapper::Template()
2039
{
40+
if (WinAppSdk::Containment::IsChangeEnabled<WINAPPSDK_CHANGEID_61574370>())
41+
{
42+
return GetDataTemplate();
43+
}
2144
return m_dataTemplate;
2245
}
2346

2447
void ItemTemplateWrapper::Template(winrt::DataTemplate const& value)
2548
{
26-
m_dataTemplate = value;
49+
if (WinAppSdk::Containment::IsChangeEnabled<WINAPPSDK_CHANGEID_61574370>())
50+
{
51+
if (m_isTracking)
52+
{
53+
m_trackedDataTemplate->set(value);
54+
}
55+
else
56+
{
57+
m_dataTemplate = value;
58+
}
59+
}
60+
else
61+
{
62+
m_dataTemplate = value;
63+
}
2764
}
2865

2966
winrt::DataTemplateSelector ItemTemplateWrapper::TemplateSelector()
@@ -40,7 +77,10 @@ void ItemTemplateWrapper::TemplateSelector(winrt::DataTemplateSelector const& va
4077

4178
winrt::UIElement ItemTemplateWrapper::GetElement(winrt::ElementFactoryGetArgs const& args)
4279
{
43-
auto selectedTemplate = m_dataTemplate ? m_dataTemplate : m_dataTemplateSelector.SelectTemplate(args.Data());
80+
auto selectedTemplate = WinAppSdk::Containment::IsChangeEnabled<WINAPPSDK_CHANGEID_61574370>()
81+
? GetDataTemplate()
82+
: m_dataTemplate;
83+
selectedTemplate = selectedTemplate ? selectedTemplate : m_dataTemplateSelector.SelectTemplate(args.Data());
4484
// Check if selected template we got is valid
4585
if (selectedTemplate == nullptr)
4686
{
@@ -97,8 +137,11 @@ winrt::UIElement ItemTemplateWrapper::GetElement(winrt::ElementFactoryGetArgs co
97137
void ItemTemplateWrapper::RecycleElement(winrt::ElementFactoryRecycleArgs const& args)
98138
{
99139
auto element = args.Element();
100-
winrt::DataTemplate selectedTemplate = m_dataTemplate?
101-
m_dataTemplate:
140+
auto dataTemplate = WinAppSdk::Containment::IsChangeEnabled<WINAPPSDK_CHANGEID_61574370>()
141+
? GetDataTemplate()
142+
: m_dataTemplate;
143+
winrt::DataTemplate selectedTemplate = dataTemplate ?
144+
dataTemplate :
102145
RecyclePool::GetOriginTemplate(element);
103146
auto recyclePool = RecyclePool::GetPoolInstance(selectedTemplate);
104147
if (!recyclePool)

src/controls/dev/Repeater/ItemTemplateWrapper.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,29 @@ class ItemTemplateWrapper :
1818
winrt::DataTemplateSelector TemplateSelector();
1919
void TemplateSelector(winrt::DataTemplateSelector const& value);
2020

21+
// Enables reference tracking for m_dataTemplate using the provided owner's
22+
// ITrackerHandleManager. This makes the DataTemplate visible to the XAML
23+
// reference tracker, allowing cycle detection and breaking for:
24+
// ItemsRepeater → ItemTemplateWrapper → DataTemplate → RecyclePool → m_owner → ItemsRepeater
25+
// Same pattern as RecyclePool::ElementInfo and ViewManager::PinnedElementInfo.
26+
// Must be called after construction (cannot be done in constructor because
27+
// winrt::make doesn't support ITrackerHandleManager* as first argument).
28+
void EnableTracking(const ITrackerHandleManager* owner);
29+
2130
#pragma region IElementFactory
2231
winrt::UIElement GetElement(winrt::ElementFactoryGetArgs const& args);
2332
void RecycleElement(winrt::ElementFactoryRecycleArgs const& args);
2433
#pragma endregion
2534

2635
private:
2736
winrt::DataTemplate m_dataTemplate{ nullptr };
37+
// When EnableTracking is called, this tracker_ref takes over from m_dataTemplate.
38+
// The tracker_ref uses the caller's ITrackerHandleManager so the XAML reference
39+
// tracker can walk the reference and break cycles.
40+
std::optional<tracker_ref<winrt::DataTemplate>> m_trackedDataTemplate;
41+
bool m_isTracking{ false };
42+
43+
winrt::DataTemplate GetDataTemplate() const;
44+
2845
winrt::DataTemplateSelector m_dataTemplateSelector{ nullptr };
2946
};

src/controls/dev/Repeater/ItemsRepeater.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
#include "ViewportManagerWithPlatformFeatures.h"
1313
#include "RuntimeProfiler.h"
1414
#include "ItemTemplateWrapper.h"
15+
#include "FrameworkUdk/Containment.h"
16+
17+
// Bug 61574370: [1.8 Servicing][WASDK] Fix ItemsRepeater RecyclePool reference cycle memory leak
18+
#define WINAPPSDK_CHANGEID_61574370 61574370, WinAppSDK_1_8_9
1519

1620
// Change to 'true' to turn on debugging outputs in Output window
1721
bool ItemsRepeaterTrace::s_IsDebugOutputEnabled{ false };
@@ -644,7 +648,18 @@ void ItemsRepeater::OnItemTemplateChanged(const winrt::IElementFactory& oldValue
644648
// recycling as opposed to the DataTemplate.
645649
if (auto dataTemplate = newValue.try_as<winrt::DataTemplate>())
646650
{
647-
m_itemTemplateWrapper = winrt::make<ItemTemplateWrapper>(dataTemplate);
651+
if (WinAppSdk::Containment::IsChangeEnabled<WINAPPSDK_CHANGEID_61574370>())
652+
{
653+
auto wrapper = winrt::make_self<ItemTemplateWrapper>(dataTemplate);
654+
// Enable reference tracking so the XAML tracker can detect and break the
655+
// RecyclePool cycle. See ItemTemplateWrapper::EnableTracking for details.
656+
wrapper->EnableTracking(this);
657+
m_itemTemplateWrapper = wrapper.as<winrt::IElementFactory>();
658+
}
659+
else
660+
{
661+
m_itemTemplateWrapper = winrt::make<ItemTemplateWrapper>(dataTemplate);
662+
}
648663
if (auto content = dataTemplate.LoadContent().as<winrt::FrameworkElement>())
649664
{
650665
// Due to bug https://github.com/microsoft/microsoft-ui-xaml/issues/3057, we need to get the framework
@@ -662,6 +677,9 @@ void ItemsRepeater::OnItemTemplateChanged(const winrt::IElementFactory& oldValue
662677
}
663678
else if (auto selector = newValue.try_as<winrt::DataTemplateSelector>())
664679
{
680+
// No EnableTracking needed: the wrapper holds the selector, not individual
681+
// DataTemplates. The RecyclePool cycle doesn't form because there's no
682+
// persistent reference from the wrapper to any DataTemplate with a pool.
665683
m_itemTemplateWrapper = winrt::make<ItemTemplateWrapper>(selector);
666684
}
667685
else if (auto customElementFactory = newValue.try_as<winrt::IElementFactory>())

src/controls/dev/dll/Microsoft.UI.Xaml.Controls.vcxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@
4040
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(MUXProjectRoot)dxaml\xcp\inc;$(TempDir)</AdditionalIncludeDirectories>
4141
</ResourceCompile>
4242
</ItemDefinitionGroup>
43+
<ItemDefinitionGroup>
44+
<Link>
45+
<AdditionalDependencies>
46+
%(AdditionalDependencies);
47+
$(FrameworkUdkLibPath)\Microsoft.Internal.FrameworkUdk.lib;
48+
</AdditionalDependencies>
49+
</Link>
50+
</ItemDefinitionGroup>
4351
<ItemGroup>
4452
<None Include="Microsoft.UI.Xaml.Controls.def" />
4553
</ItemGroup>

src/eng/Version.Details.xml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
<!-- Copyright (c) Microsoft Corporation. Licensed under the MIT License. See LICENSE in the project root for license information. -->
33
<Dependencies>
44
<ProductDependencies>
5-
<Dependency Name="Microsoft.WindowsAppSDK.Foundation.TransportPackage" Version="1.8.0-20260505.1.release">
5+
<Dependency Name="Microsoft.WindowsAppSDK.Foundation.TransportPackage" Version="1.8.0-20260527.0.release">
66
<Uri>https://dev.azure.com/microsoft/ProjectReunion/_git/WindowsAppSDK</Uri>
7-
<Sha>5f010fbc8ce1fbef2a5558615fe064832e0004f8</Sha>
7+
<Sha>bcf23be002bd7a58888a429ef46b781ecb5dc80b</Sha>
88
</Dependency>
9-
<Dependency Name="Microsoft.ProjectReunion.InteractiveExperiences.TransportPackage" Version="1.8.0-stable-27108.1046.260429-1533.0">
9+
<Dependency Name="Microsoft.ProjectReunion.InteractiveExperiences.TransportPackage" Version="1.8.0-stable-27108.1050.260525-0800.1">
1010
<Uri>https://dev.azure.com/microsoft/LiftedIXP/_git/DCPP</Uri>
11-
<Sha>eafe8316011216da9a238ae870b27d1e4fcbe65b</Sha>
11+
<Sha>bcefb53ef31cde2d4d7e97760db1970335047aed</Sha>
1212
</Dependency>
13-
<Dependency Name="Microsoft.Internal.InteractiveExperiences" Version="1.8.0-stable-27108.1046.260429-1533.0">
13+
<Dependency Name="Microsoft.Internal.InteractiveExperiences" Version="1.8.0-stable-27108.1050.260525-0800.1">
1414
<Uri>https://dev.azure.com/microsoft/LiftedIXP/_git/DCPP</Uri>
15-
<Sha>eafe8316011216da9a238ae870b27d1e4fcbe65b</Sha>
15+
<Sha>bcefb53ef31cde2d4d7e97760db1970335047aed</Sha>
1616
</Dependency>
1717
<!-- Microsoft-WinUI-SDK subdirectory in WindowsAppSDKClosed (MSBuild and Visual Studio extensions for building, deploying, and debugging packaged applications.) -->
1818
<Dependency Name="Microsoft.Build.Msix" Version="1.7.0-release.20241114.0">
@@ -28,13 +28,13 @@
2828
<Uri>https://dev.azure.com/microsoft/ProjectReunion/_git/WindowsAppSDKAggregator</Uri>
2929
<Sha>66cd2dc7827ad19d84cd68743af8570d9aca1c93</Sha>
3030
</Dependency>
31-
<Dependency Name="Microsoft.WindowsAppSDK.Foundation" Version="1.8.260505001">
31+
<Dependency Name="Microsoft.WindowsAppSDK.Foundation" Version="1.8.260527000">
3232
<Uri>https://dev.azure.com/microsoft/ProjectReunion/_git/WindowsAppSDK</Uri>
33-
<Sha>5f010fbc8ce1fbef2a5558615fe064832e0004f8</Sha>
33+
<Sha>bcf23be002bd7a58888a429ef46b781ecb5dc80b</Sha>
3434
</Dependency>
35-
<Dependency Name="Microsoft.WindowsAppSDK.InteractiveExperiences" Version="1.8.260430001">
35+
<Dependency Name="Microsoft.WindowsAppSDK.InteractiveExperiences" Version="1.8.260525001">
3636
<Uri>https://dev.azure.com/microsoft/LiftedIXP/_git/DCPP</Uri>
37-
<Sha>eafe8316011216da9a238ae870b27d1e4fcbe65b</Sha>
37+
<Sha>bcefb53ef31cde2d4d7e97760db1970335047aed</Sha>
3838
</Dependency>
3939
</ProductDependencies>
4040
<ToolsetDependencies>

0 commit comments

Comments
 (0)