diff --git a/src/Controls/src/Core/BindableLayout/BindableLayout.cs b/src/Controls/src/Core/BindableLayout/BindableLayout.cs
index e99a7a2e40a8..3ee825380163 100644
--- a/src/Controls/src/Core/BindableLayout/BindableLayout.cs
+++ b/src/Controls/src/Core/BindableLayout/BindableLayout.cs
@@ -1,7 +1,6 @@
#nullable disable
using System;
using System.Collections;
-using System.Collections.Generic;
using System.Collections.Specialized;
using Microsoft.Maui.Controls.Internals;
@@ -148,6 +147,18 @@ internal static void Add(this IBindableLayout layout, object item)
_ = layout.Children.Add(item);
}
}
+
+ internal static void Replace(this IBindableLayout layout, object item, int index)
+ {
+ if (layout is Maui.ILayout mauiLayout && item is IView view)
+ {
+ mauiLayout[index] = view;
+ }
+ else
+ {
+ layout.Children[index] = item;
+ }
+ }
internal static void Insert(this IBindableLayout layout, object item, int index)
{
@@ -200,6 +211,31 @@ internal static void Clear(this IBindableLayout layout)
class BindableLayoutController
{
+ static readonly BindableProperty BindableLayoutTemplateProperty = BindableProperty.CreateAttached("BindableLayoutTemplate", typeof(DataTemplate), typeof(BindableLayoutController), default(DataTemplate));
+
+ ///
+ /// Gets the template reference used to generate a view in the .
+ ///
+ static DataTemplate GetBindableLayoutTemplate(BindableObject b)
+ {
+ return (DataTemplate)b.GetValue(BindableLayoutTemplateProperty);
+ }
+
+ ///
+ /// Sets the template reference used to generate a view in the .
+ ///
+ static void SetBindableLayoutTemplate(BindableObject b, DataTemplate value)
+ {
+ b.SetValue(BindableLayoutTemplateProperty, value);
+ }
+
+ static readonly DataTemplate DefaultItemTemplate = new DataTemplate(() =>
+ {
+ var label = new Label { HorizontalTextAlignment = TextAlignment.Center };
+ label.SetBinding(Label.TextProperty, ".");
+ return label;
+ });
+
readonly WeakReference _layoutWeakReference;
readonly WeakNotifyCollectionChangedProxy _collectionChangedProxy = new();
readonly NotifyCollectionChangedEventHandler _collectionChangedEventHandler;
@@ -290,7 +326,7 @@ void SetEmptyView(object emptyView)
if (!_isBatchUpdate)
{
- CreateChildren();
+ UpdateEmptyView();
}
}
@@ -302,78 +338,134 @@ void SetEmptyViewTemplate(DataTemplate emptyViewTemplate)
if (!_isBatchUpdate)
{
- CreateChildren();
+ UpdateEmptyView();
}
}
- void CreateChildren()
+ void UpdateEmptyView()
{
if (!_layoutWeakReference.TryGetTarget(out IBindableLayout layout))
{
return;
}
- ClearChildren(layout);
+ TryAddEmptyView(layout, out _);
+ }
- UpdateEmptyView(layout);
+ void CreateChildren()
+ {
+ if (!_layoutWeakReference.TryGetTarget(out IBindableLayout layout))
+ {
+ return;
+ }
- if (_itemsSource == null)
+ if (TryAddEmptyView(layout, out IEnumerator enumerator))
+ {
return;
+ }
+
+ var layoutChildren = layout.Children;
+ var childrenCount = layoutChildren.Count;
- foreach (object item in _itemsSource)
+ // if we have the empty view, remove it before generating children
+ if (childrenCount == 1 && layoutChildren[0] == _currentEmptyView)
{
- layout.Add(CreateItemView(item, layout));
+ layout.RemoveAt(0);
+ childrenCount = 0;
}
- }
- void ClearChildren(IBindableLayout layout)
- {
- var index = layout.Children.Count;
- while (--index >= 0)
+ // Add or replace items
+ var index = 0;
+ do
{
- var child = (View)layout.Children[index]!;
- layout.RemoveAt(index);
+ var item = enumerator.Current;
+ if (index < childrenCount)
+ {
+ ReplaceChild(item, layout, layoutChildren, index);
+ }
+ else
+ {
+ layout.Add(CreateItemView(item, SelectTemplate(item, layout)));
+ }
- // Empty view inherits the BindingContext automatically,
- // we don't want to mess up with automatic inheritance.
- if (child == _currentEmptyView) continue;
-
- // Given that we've set BindingContext manually on children we have to clear it on removal.
+ ++index;
+ } while (enumerator.MoveNext());
+
+ // Remove exceeding items
+ while (index <= --childrenCount)
+ {
+ var child = (BindableObject) layoutChildren[childrenCount]!;
+ layout.RemoveAt(childrenCount);
+ // It's our responsibility to clear the BindingContext for the children
+ // Given that we've set them manually in CreateItemView
child.BindingContext = null;
}
}
- void UpdateEmptyView(IBindableLayout layout)
+ bool TryAddEmptyView(IBindableLayout layout, out IEnumerator enumerator)
{
- if (_currentEmptyView == null)
- return;
+ enumerator = _itemsSource?.GetEnumerator();
- if (!_itemsSource?.GetEnumerator().MoveNext() ?? true)
+ if (enumerator == null || !enumerator.MoveNext())
{
- layout.Add(_currentEmptyView);
- return;
+ var layoutChildren = layout.Children;
+
+ // We may have a single child that is either the old empty view or a generated item
+ if (layoutChildren.Count == 1)
+ {
+ var maybeEmptyView = (View)layoutChildren[0]!;
+
+ // If the current empty view is already in place we have nothing to do
+ if (maybeEmptyView == _currentEmptyView)
+ {
+ return true;
+ }
+
+ // We may have a single child that is either the old empty view or a generated item
+ // So remove it to make room for the new empty view
+ layout.RemoveAt(0);
+
+ // If this is a generated item, we need to clear the BindingContext
+ if (maybeEmptyView.IsSet(BindableLayoutTemplateProperty))
+ {
+ maybeEmptyView.ClearValue(BindableObject.BindingContextProperty);
+ }
+ }
+ else if (layoutChildren.Count > 1)
+ {
+ // If we have more than one child it means we have generated items only
+ // So clear them all to make room for the new empty view
+ ClearChildren(layout);
+ }
+
+ // If an empty view is set, add it
+ if (_currentEmptyView != null)
+ {
+ layout.Add(_currentEmptyView);
+ }
+
+ return true;
}
- layout.Remove(_currentEmptyView);
+ return false;
}
- View CreateItemView(object item, IBindableLayout layout)
+ void ClearChildren(IBindableLayout layout)
{
- return CreateItemView(item, _itemTemplate ?? _itemTemplateSelector?.SelectTemplate(item, layout as BindableObject));
+ var index = layout.Children.Count;
+ while (--index >= 0)
+ {
+ var child = (View)layout.Children[index]!;
+ layout.RemoveAt(index);
+
+ // It's our responsibility to clear the manually-set BindingContext for the generated children
+ child.ClearValue(BindableObject.BindingContextProperty);
+ }
}
- View CreateItemView(object item, DataTemplate dataTemplate)
+ DataTemplate SelectTemplate(object item, IBindableLayout layout)
{
- if (dataTemplate != null)
- {
- var view = (View)dataTemplate.CreateContent();
- view.BindingContext = item;
- return view;
- }
- else
- {
- return new Label { Text = item?.ToString(), HorizontalTextAlignment = TextAlignment.Center };
- }
+ return _itemTemplate ?? _itemTemplateSelector?.SelectTemplate(item, layout as BindableObject) ?? DefaultItemTemplate;
}
View CreateEmptyView(object emptyView, DataTemplate dataTemplate)
@@ -404,8 +496,29 @@ void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArg
return;
}
+ if (e.Action == NotifyCollectionChangedAction.Replace)
+ {
+ var index = e.OldStartingIndex;
+ var layoutChildren = layout.Children;
+ foreach (var item in e.NewItems!)
+ {
+ ReplaceChild(item, layout, layoutChildren, index);
+ ++index;
+ }
+ return;
+ }
+
e.Apply(
- insert: (item, index, _) => layout.Insert(CreateItemView(item, layout), index),
+ insert: (item, index, _) =>
+ {
+ var layoutChildren = layout.Children;
+ if (layoutChildren.Count == 1 && layoutChildren[0] == _currentEmptyView)
+ {
+ layout.RemoveAt(0);
+ }
+
+ layout.Insert(CreateItemView(item, SelectTemplate(item, layout)), index);
+ },
removeAt: (item, index) =>
{
var child = (View)layout.Children[index]!;
@@ -414,12 +527,40 @@ void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArg
// It's our responsibility to clear the BindingContext for the children
// Given that we've set them manually in CreateItemView
child.BindingContext = null;
+
+ // If we removed the last item, we need to insert the empty view
+ if (layout.Children.Count == 0 && _currentEmptyView != null)
+ {
+ layout.Add(_currentEmptyView);
+ }
},
reset: CreateChildren);
+ }
- // UpdateEmptyView is called from within CreateChildren, therefor skip it for Reset
- if (e.Action != NotifyCollectionChangedAction.Reset)
- UpdateEmptyView(layout);
+ void ReplaceChild(object item, IBindableLayout layout, IList layoutChildren, int index)
+ {
+ var template = SelectTemplate(item, layout);
+ var child = (BindableObject) layoutChildren[index]!;
+ var currentTemplate = GetBindableLayoutTemplate(child);
+ if (currentTemplate == template)
+ {
+ child.BindingContext = item;
+ }
+ else
+ {
+ // It's our responsibility to clear the BindingContext for the children
+ // Given that we've set them manually in CreateItemView
+ child.BindingContext = null;
+ layout.Replace(CreateItemView(item, template), index);
+ }
+ }
+
+ static View CreateItemView(object item, DataTemplate dataTemplate)
+ {
+ var view = (View)dataTemplate.CreateContent();
+ SetBindableLayoutTemplate(view, dataTemplate);
+ view.BindingContext = item;
+ return view;
}
}
}
\ No newline at end of file
diff --git a/src/Controls/tests/Core.UnitTests/BindableLayoutTests.cs b/src/Controls/tests/Core.UnitTests/BindableLayoutTests.cs
index 8426d7cfbaac..a4af7ac2f12f 100644
--- a/src/Controls/tests/Core.UnitTests/BindableLayoutTests.cs
+++ b/src/Controls/tests/Core.UnitTests/BindableLayoutTests.cs
@@ -13,7 +13,6 @@ namespace Microsoft.Maui.Controls.Core.UnitTests
{
using StackLayout = Microsoft.Maui.Controls.Compatibility.StackLayout;
-
public class BindableLayoutTests : BaseTestFixture
{
[Fact]
@@ -193,6 +192,109 @@ public void ItemTemplateSelectorIsSet()
Assert.Equal(itemsSource.Count, layout.Children.Cast().Count());
}
+ [Fact]
+ public void ChangingTemplateRecreatesChildren()
+ {
+ var layout = new StackLayout
+ {
+ IsPlatformEnabled = true,
+ };
+
+ var itemsSource = new ObservableCollection(Enumerable.Range(0, 10));
+ BindableLayout.SetItemsSource(layout, itemsSource);
+
+ BindableLayout.SetItemTemplate(layout, new DataTemplate(() => new Label()));
+ Assert.All(layout.Children, c => Assert.IsType