@@ -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}
0 commit comments