Skip to content

Commit 2628799

Browse files
albyrock87jsuarezruiz
authored andcommitted
BindableLayout should disconnect handlers
1 parent 15284b6 commit 2628799

File tree

3 files changed

+129
-19
lines changed

3 files changed

+129
-19
lines changed

src/Controls/src/Core/BindableLayout/BindableLayout.cs

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System;
33
using System.Collections;
44
using System.Collections.Specialized;
5+
using System.Linq;
56
using Microsoft.Maui.Controls.Internals;
67

78
namespace Microsoft.Maui.Controls
@@ -371,6 +372,7 @@ void CreateChildren()
371372
if (childrenCount == 1 && layoutChildren[0] == _currentEmptyView)
372373
{
373374
layout.RemoveAt(0);
375+
_currentEmptyView.DisconnectHandlers();
374376
childrenCount = 0;
375377
}
376378

@@ -394,11 +396,15 @@ void CreateChildren()
394396
// Remove exceeding items
395397
while (index <= --childrenCount)
396398
{
397-
var child = (BindableObject)layoutChildren[childrenCount]!;
399+
IView child = (IView)layoutChildren[childrenCount]!;
398400
layout.RemoveAt(childrenCount);
401+
402+
// Disconnect platform view so when we clear binding context it doesn't run mappers
403+
child.DisconnectHandlers();
404+
399405
// It's our responsibility to clear the BindingContext for the children
400406
// Given that we've set them manually in CreateItemView
401-
child.BindingContext = null;
407+
ClearBindingContext(child);
402408
}
403409
}
404410

@@ -413,7 +419,7 @@ bool TryAddEmptyView(IBindableLayout layout, out IEnumerator enumerator)
413419
// We may have a single child that is either the old empty view or a generated item
414420
if (layoutChildren.Count == 1)
415421
{
416-
var maybeEmptyView = (View)layoutChildren[0]!;
422+
var maybeEmptyView = (IView)layoutChildren[0]!;
417423

418424
// If the current empty view is already in place we have nothing to do
419425
if (maybeEmptyView == _currentEmptyView)
@@ -425,11 +431,10 @@ bool TryAddEmptyView(IBindableLayout layout, out IEnumerator enumerator)
425431
// So remove it to make room for the new empty view
426432
layout.RemoveAt(0);
427433

434+
// Disconnect platform view so when we clear binding context it doesn't run mappers
435+
maybeEmptyView.DisconnectHandlers();
428436
// If this is a generated item, we need to clear the BindingContext
429-
if (maybeEmptyView.IsSet(BindableLayoutTemplateProperty))
430-
{
431-
maybeEmptyView.ClearValue(BindableObject.BindingContextProperty);
432-
}
437+
ClearBindingContext(maybeEmptyView);
433438
}
434439
else if (layoutChildren.Count > 1)
435440
{
@@ -452,14 +457,19 @@ bool TryAddEmptyView(IBindableLayout layout, out IEnumerator enumerator)
452457

453458
void ClearChildren(IBindableLayout layout)
454459
{
455-
var index = layout.Children.Count;
456-
while (--index >= 0)
460+
var layoutChildren = layout.Children.OfType<IView>().ToArray();
461+
layout.Clear();
462+
463+
foreach (var child in layoutChildren)
457464
{
458-
var child = (View)layout.Children[index]!;
459-
layout.RemoveAt(index);
465+
// Disconnect platform view so when we clear binding context it doesn't run mappers
466+
child.DisconnectHandlers();
460467

461468
// It's our responsibility to clear the manually-set BindingContext for the generated children
462-
child.ClearValue(BindableObject.BindingContextProperty);
469+
if (child is BindableObject bindable)
470+
{
471+
bindable.ClearValue(BindableObject.BindingContextProperty);
472+
}
463473
}
464474
}
465475

@@ -515,18 +525,21 @@ void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArg
515525
if (layoutChildren.Count == 1 && layoutChildren[0] == _currentEmptyView)
516526
{
517527
layout.RemoveAt(0);
528+
_currentEmptyView.DisconnectHandlers();
518529
}
519530

520531
layout.Insert(CreateItemView(item, SelectTemplate(item, layout)), index);
521532
},
522533
removeAt: (item, index) =>
523534
{
524-
var child = (View)layout.Children[index]!;
535+
var child = (IView)layout.Children[index]!;
525536
layout.RemoveAt(index);
526537

538+
// Disconnect platform view so when we clear binding context it doesn't run mappers
539+
child.DisconnectHandlers();
527540
// It's our responsibility to clear the BindingContext for the children
528541
// Given that we've set them manually in CreateItemView
529-
child.BindingContext = null;
542+
ClearBindingContext(child);
530543

531544
// If we removed the last item, we need to insert the empty view
532545
if (layout.Children.Count == 0 && _currentEmptyView != null)
@@ -540,18 +553,18 @@ void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArg
540553
void ReplaceChild(object item, IBindableLayout layout, IList layoutChildren, int index)
541554
{
542555
var template = SelectTemplate(item, layout);
543-
var child = (BindableObject)layoutChildren[index]!;
544-
var currentTemplate = GetBindableLayoutTemplate(child);
545-
if (currentTemplate == template)
556+
var child = (IView)layoutChildren[index]!;
557+
if (child is BindableObject bindable && GetBindableLayoutTemplate(bindable) == template)
546558
{
547-
child.BindingContext = item;
559+
bindable.BindingContext = item;
548560
}
549561
else
550562
{
551563
// It's our responsibility to clear the BindingContext for the children
552564
// Given that we've set them manually in CreateItemView
553-
child.BindingContext = null;
554565
layout.Replace(CreateItemView(item, template), index);
566+
child.DisconnectHandlers();
567+
ClearBindingContext(child);
555568
}
556569
}
557570

@@ -562,5 +575,14 @@ static View CreateItemView(object item, DataTemplate dataTemplate)
562575
view.BindingContext = item;
563576
return view;
564577
}
578+
579+
580+
static void ClearBindingContext(IView child)
581+
{
582+
if (child is BindableObject bindable && bindable.IsSet(BindableLayoutTemplateProperty))
583+
{
584+
bindable.ClearValue(BindableObject.BindingContextProperty);
585+
}
586+
}
565587
}
566588
}

src/Controls/tests/Core.UnitTests/BindableLayoutTests.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
using System.Linq;
88
using System.Reflection;
99
using System.Threading.Tasks;
10+
using Microsoft.Maui.Controls.Hosting;
11+
using Microsoft.Maui.Handlers;
12+
using Microsoft.Maui.Hosting;
13+
using Microsoft.Maui.Platform;
1014
using Xunit;
1115

1216
namespace Microsoft.Maui.Controls.Core.UnitTests
@@ -652,6 +656,86 @@ public async Task UpdatesAfterGC()
652656
Assert.Equal(3, layout.Children.Count);
653657
}
654658

659+
[Fact("BindableLayout disconnects handlers when removing views")]
660+
public void DisconnectsHandlersWhenRemovingViews()
661+
{
662+
var mauiApp = MauiApp.CreateBuilder()
663+
.UseMauiApp<ApplicationStub>()
664+
.ConfigureMauiHandlers(handlers => handlers.AddHandler<ContentPage, HandlerStub>())
665+
.ConfigureMauiHandlers(handlers => handlers.AddHandler<Button, HandlerStub>())
666+
.ConfigureMauiHandlers(handlers => handlers.AddHandler<VerticalStackLayout, BindableLayoutHandlerStub>())
667+
.Build();
668+
669+
MauiContext mauiContext = new MauiContext(mauiApp.Services);
670+
671+
var bindableLayout = new VerticalStackLayout();
672+
var items = new ObservableCollection<int>(Enumerable.Range(0, 10));
673+
BindableLayout.SetItemsSource(bindableLayout, items);
674+
BindableLayout.SetItemTemplate(bindableLayout, new DataTemplate(() => new Button()));
675+
BindableLayout.SetEmptyView(bindableLayout, new Button());
676+
677+
bindableLayout.ToHandler(mauiContext);
678+
679+
// Ensure we have the handlers on all elements
680+
Assert.All(bindableLayout.Children, c => Assert.NotNull(c.Handler));
681+
682+
// Test removal of an item
683+
var lastChildIndex = items.Count - 1;
684+
var lastChild = bindableLayout[lastChildIndex];
685+
items.RemoveAt(lastChildIndex);
686+
Assert.Null(lastChild.Handler);
687+
688+
// Test removal of all items
689+
var children = bindableLayout.Children.ToList();
690+
items.Clear();
691+
Assert.All(children, c => Assert.Null(c.Handler));
692+
693+
// Test removal of empty view
694+
var emptyView = bindableLayout.FirstOrDefault() as Button;
695+
Assert.NotNull(emptyView);
696+
Assert.NotNull(emptyView.Handler);
697+
items.Add(1000);
698+
Assert.Null(emptyView.Handler);
699+
700+
// Test replacing the items source with an empty enumerable
701+
lastChildIndex = 0;
702+
lastChild = bindableLayout[lastChildIndex];
703+
BindableLayout.SetItemsSource(bindableLayout, Enumerable.Empty<int>());
704+
Assert.Null(lastChild.Handler);
705+
Assert.NotNull(emptyView.Handler);
706+
}
707+
708+
class BindableLayoutHandlerStub : HandlerStub
709+
{
710+
public static CommandMapper<IView, IViewHandler> CommandMapper = new()
711+
{
712+
[nameof(ILayoutHandler.Add)] = MapCreatePlatformHandler,
713+
[nameof(ILayoutHandler.Insert)] = MapCreatePlatformHandler,
714+
};
715+
716+
static void MapCreatePlatformHandler(IViewHandler handler, IView view, object arg)
717+
{
718+
if (arg is LayoutHandlerUpdate args)
719+
{
720+
args.View.ToHandler(handler.MauiContext!);
721+
}
722+
}
723+
724+
public BindableLayoutHandlerStub() : base(new PropertyMapper<IView>(), CommandMapper)
725+
{
726+
}
727+
728+
public override void SetVirtualView(IView view)
729+
{
730+
base.SetVirtualView(view);
731+
var bindableLayout = (IBindableLayout)view;
732+
foreach (var child in bindableLayout.Children.OfType<IView>())
733+
{
734+
child.ToHandler(MauiContext!);
735+
}
736+
}
737+
}
738+
655739
// Checks if for every item in the items source there's a corresponding view
656740
static bool IsLayoutWithItemsSource(IEnumerable itemsSource, Compatibility.Layout layout)
657741
{

src/Controls/tests/Core.UnitTests/TestClasses/HandlerStub.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ public HandlerStub(PropertyMapper mapper) : base(mapper)
1515
{
1616
}
1717

18+
public HandlerStub(PropertyMapper mapper, CommandMapper commandMapper) : base(mapper, commandMapper)
19+
{
20+
}
21+
1822
protected override object CreatePlatformView()
1923
{
2024
return new object();

0 commit comments

Comments
 (0)