Skip to content

Commit

Permalink
Improve performance and stability of BindableLayout (dotnet#23136)
Browse files Browse the repository at this point in the history
* Improve performance and stability of `BindableLayout`
Fixes dotnet#23135
Fixes dotnet#10918

* Remove `IDispatcher` from `BindableLayout` enhancements

* Fix empty view not removed when adding an item on an empty observable collection

* Adjust MemoryTests to account for the improved `BindableLayout` behavior
  • Loading branch information
albyrock87 authored Sep 6, 2024
1 parent 566b859 commit bcba26f
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 50 deletions.
231 changes: 186 additions & 45 deletions src/Controls/src/Core/BindableLayout/BindableLayout.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Microsoft.Maui.Controls.Internals;

Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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));

/// <summary>
/// Gets the template reference used to generate a view in the <see cref="BindableLayout"/>.
/// </summary>
static DataTemplate GetBindableLayoutTemplate(BindableObject b)
{
return (DataTemplate)b.GetValue(BindableLayoutTemplateProperty);
}

/// <summary>
/// Sets the template reference used to generate a view in the <see cref="BindableLayout"/>.
/// </summary>
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<IBindableLayout> _layoutWeakReference;
readonly WeakNotifyCollectionChangedProxy _collectionChangedProxy = new();
readonly NotifyCollectionChangedEventHandler _collectionChangedEventHandler;
Expand Down Expand Up @@ -290,7 +326,7 @@ void SetEmptyView(object emptyView)

if (!_isBatchUpdate)
{
CreateChildren();
UpdateEmptyView();
}
}

Expand All @@ -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)
Expand Down Expand Up @@ -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]!;
Expand All @@ -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;
}
}
}
Loading

0 comments on commit bcba26f

Please sign in to comment.