@@ -389,6 +389,77 @@ public void ValidateBindableProperties()
389389 Assert . Equal ( itemTemplateSelector , layout . GetValue ( BindableLayout . ItemTemplateSelectorProperty ) ) ;
390390 }
391391
392+ [ Fact ]
393+ public async Task DoesNotLeakChildListeningToBindingContextOnRemove ( )
394+ {
395+ var myViewModel = new MyViewModel ( ) ;
396+ var list = new ObservableCollection < MyViewModel > { myViewModel } ;
397+
398+ var triggeredCount = 0 ;
399+ var itemTemplate = new DataTemplate ( ( ) => new MyViewModelBoundComponent ( onTextChangedCallback : ( ) => triggeredCount ++ ) ) ;
400+
401+ var layout = new StackLayout { IsPlatformEnabled = true } ;
402+ BindableLayout . SetItemTemplate ( layout , itemTemplate ) ;
403+ BindableLayout . SetItemsSource ( layout , list ) ;
404+
405+ list . RemoveAt ( 0 ) ;
406+
407+ // Run GC
408+ await Task . Yield ( ) ;
409+ GC . Collect ( ) ;
410+ GC . WaitForPendingFinalizers ( ) ;
411+
412+ // The component should be gone
413+ Assert . Equal ( 0 , MyViewModelBoundComponent . InstanceCount ) ;
414+ }
415+
416+ [ Fact ]
417+ public async Task DoesNotLeakChildListeningToBindingContextOnClear ( )
418+ {
419+ var myViewModel = new MyViewModel ( ) ;
420+ var list = new ObservableCollection < MyViewModel > { myViewModel } ;
421+
422+ var triggeredCount = 0 ;
423+ var itemTemplate = new DataTemplate ( ( ) => new MyViewModelBoundComponent ( onTextChangedCallback : ( ) => triggeredCount ++ ) ) ;
424+
425+ var layout = new StackLayout { IsPlatformEnabled = true } ;
426+ BindableLayout . SetItemTemplate ( layout , itemTemplate ) ;
427+ BindableLayout . SetItemsSource ( layout , list ) ;
428+
429+ list . Clear ( ) ;
430+
431+ // Run GC
432+ await Task . Yield ( ) ;
433+ GC . Collect ( ) ;
434+ GC . WaitForPendingFinalizers ( ) ;
435+
436+ // The component should be gone
437+ Assert . Equal ( 0 , MyViewModelBoundComponent . InstanceCount ) ;
438+ }
439+
440+ [ Fact ]
441+ public async Task EmptyViewTemplateContentInheritsLayoutBindingContext ( )
442+ {
443+ var list = new ObservableCollection < string > ( ) ;
444+
445+ var bindingContext = "Foo" ;
446+
447+ var layout = new StackLayout { IsPlatformEnabled = true , BindingContext = bindingContext } ;
448+ BindableLayout . SetEmptyViewTemplate ( layout , new DataTemplate ( ( ) => new Label ( ) ) ) ;
449+ BindableLayout . SetItemsSource ( layout , list ) ;
450+
451+ // Verify that the empty view is bound to layout's binding context
452+ var emptyView = layout . Children . FirstOrDefault ( ) as Label ;
453+ Assert . NotNull ( emptyView ) ;
454+ Assert . Equal ( bindingContext , emptyView . BindingContext ) ;
455+
456+ // Change binding context on layout
457+ layout . BindingContext = bindingContext = "Bar" ;
458+
459+ // Verify empty view inherited the binding context
460+ Assert . Equal ( bindingContext , emptyView . BindingContext ) ;
461+ }
462+
392463 [ Fact ]
393464 public async Task DoesNotLeak ( )
394465 {
@@ -530,5 +601,72 @@ public MyDataTemplateSelectorTest(Func<object, BindableObject, DataTemplate> fun
530601 protected override DataTemplate OnSelectTemplate ( object item , BindableObject container )
531602 => _func ( item , container ) ;
532603 }
604+
605+ class MyViewModel : INotifyPropertyChanged
606+ {
607+ string _text ;
608+
609+ public event PropertyChangedEventHandler PropertyChanged ;
610+
611+ public string Text
612+ {
613+ get => _text ;
614+ set
615+ {
616+ _text = value ;
617+ PropertyChanged ? . Invoke ( this , new PropertyChangedEventArgs ( nameof ( Text ) ) ) ;
618+ }
619+ }
620+ }
621+
622+ /// <summary>
623+ /// When binding context is changed, the component watches for changes in the view model `Text` property.
624+ /// The event handler is removed correctly when the binding context is changed or cleared.
625+ /// </summary>
626+ class MyViewModelBoundComponent : ContentView
627+ {
628+ public static int InstanceCount = 0 ;
629+
630+ readonly Action _onTextChangedCallback ;
631+ MyViewModel _myViewModel ;
632+
633+ public string Text => _myViewModel ? . Text ;
634+
635+ public MyViewModelBoundComponent ( Action onTextChangedCallback )
636+ {
637+ ++ InstanceCount ;
638+ _onTextChangedCallback = onTextChangedCallback ;
639+ }
640+
641+ ~ MyViewModelBoundComponent ( )
642+ {
643+ -- InstanceCount ;
644+ }
645+
646+ protected override void OnBindingContextChanged ( )
647+ {
648+ base . OnBindingContextChanged ( ) ;
649+
650+ if ( _myViewModel != null )
651+ {
652+ _myViewModel . PropertyChanged -= OnBindingContextFixturePropertyChanged ;
653+ }
654+
655+ _myViewModel = BindingContext as MyViewModel ;
656+
657+ if ( _myViewModel != null )
658+ {
659+ _myViewModel . PropertyChanged += OnBindingContextFixturePropertyChanged ;
660+ }
661+ }
662+
663+ void OnBindingContextFixturePropertyChanged ( object sender , PropertyChangedEventArgs e )
664+ {
665+ if ( e . PropertyName == nameof ( MyViewModel . Text ) )
666+ {
667+ _onTextChangedCallback ? . Invoke ( ) ;
668+ }
669+ }
670+ }
533671 }
534672}
0 commit comments