File: BindableLayout\BindableLayout.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections;
using System.Collections.Specialized;
using Microsoft.Maui.Controls.Internals;
 
namespace Microsoft.Maui.Controls
{
	// TODO ezhart 2021-07-16 This interface is just here to give Layout and Compatibility.Layout common ground for BindableLayout
	// once we have the IContainer changes in, we may be able to drop this in favor of simply Core.ILayout
	// See also IndicatorView.cs 
	public interface IBindableLayout
	{
		public IList Children { get; }
	}
 
	/// <include file="../../docs/Microsoft.Maui.Controls/BindableLayout.xml" path="Type[@FullName='Microsoft.Maui.Controls.BindableLayout']/Docs/*" />
	public static class BindableLayout
	{
		/// <summary>Bindable property for attached property <c>ItemsSource</c>.</summary>
		public static readonly BindableProperty ItemsSourceProperty =
			BindableProperty.CreateAttached("ItemsSource", typeof(IEnumerable), typeof(IBindableLayout), default(IEnumerable),
				propertyChanged: (b, o, n) => { GetBindableLayoutController(b).ItemsSource = (IEnumerable)n; });
 
		/// <summary>Bindable property for attached property <c>ItemTemplate</c>.</summary>
		public static readonly BindableProperty ItemTemplateProperty =
			BindableProperty.CreateAttached("ItemTemplate", typeof(DataTemplate), typeof(IBindableLayout), default(DataTemplate),
				propertyChanged: (b, o, n) => { GetBindableLayoutController(b).ItemTemplate = (DataTemplate)n; });
 
		/// <summary>Bindable property for attached property <c>ItemTemplateSelector</c>.</summary>
		public static readonly BindableProperty ItemTemplateSelectorProperty =
			BindableProperty.CreateAttached("ItemTemplateSelector", typeof(DataTemplateSelector), typeof(IBindableLayout), default(DataTemplateSelector),
				propertyChanged: (b, o, n) => { GetBindableLayoutController(b).ItemTemplateSelector = (DataTemplateSelector)n; });
 
		static readonly BindableProperty BindableLayoutControllerProperty =
			 BindableProperty.CreateAttached("BindableLayoutController", typeof(BindableLayoutController), typeof(IBindableLayout), default(BindableLayoutController),
				 defaultValueCreator: (b) => new BindableLayoutController((IBindableLayout)b),
				 propertyChanged: (b, o, n) => OnControllerChanged(b, (BindableLayoutController)o, (BindableLayoutController)n));
 
		/// <summary>Bindable property for attached property <c>EmptyView</c>.</summary>
		public static readonly BindableProperty EmptyViewProperty =
			BindableProperty.Create("EmptyView", typeof(object), typeof(IBindableLayout), null, propertyChanged: (b, o, n) => { GetBindableLayoutController(b).EmptyView = n; });
 
		/// <summary>Bindable property for attached property <c>EmptyViewTemplate</c>.</summary>
		public static readonly BindableProperty EmptyViewTemplateProperty =
			BindableProperty.Create("EmptyViewTemplate", typeof(DataTemplate), typeof(IBindableLayout), null, propertyChanged: (b, o, n) => { GetBindableLayoutController(b).EmptyViewTemplate = (DataTemplate)n; });
 
		/// <include file="../../docs/Microsoft.Maui.Controls/BindableLayout.xml" path="//Member[@MemberName='SetItemsSource']/Docs/*" />
		public static void SetItemsSource(BindableObject b, IEnumerable value)
		{
			b.SetValue(ItemsSourceProperty, value);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/BindableLayout.xml" path="//Member[@MemberName='GetItemsSource']/Docs/*" />
		public static IEnumerable GetItemsSource(BindableObject b)
		{
			return (IEnumerable)b.GetValue(ItemsSourceProperty);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/BindableLayout.xml" path="//Member[@MemberName='SetItemTemplate']/Docs/*" />
		public static void SetItemTemplate(BindableObject b, DataTemplate value)
		{
			b.SetValue(ItemTemplateProperty, value);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/BindableLayout.xml" path="//Member[@MemberName='GetItemTemplate']/Docs/*" />
		public static DataTemplate GetItemTemplate(BindableObject b)
		{
			return (DataTemplate)b.GetValue(ItemTemplateProperty);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/BindableLayout.xml" path="//Member[@MemberName='SetItemTemplateSelector']/Docs/*" />
		public static void SetItemTemplateSelector(BindableObject b, DataTemplateSelector value)
		{
			b.SetValue(ItemTemplateSelectorProperty, value);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/BindableLayout.xml" path="//Member[@MemberName='GetItemTemplateSelector']/Docs/*" />
		public static DataTemplateSelector GetItemTemplateSelector(BindableObject b)
		{
			return (DataTemplateSelector)b.GetValue(ItemTemplateSelectorProperty);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/BindableLayout.xml" path="//Member[@MemberName='GetEmptyView']/Docs/*" />
		public static object GetEmptyView(BindableObject b)
		{
			return b.GetValue(EmptyViewProperty);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/BindableLayout.xml" path="//Member[@MemberName='SetEmptyView']/Docs/*" />
		public static void SetEmptyView(BindableObject b, object value)
		{
			b.SetValue(EmptyViewProperty, value);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/BindableLayout.xml" path="//Member[@MemberName='GetEmptyViewTemplate']/Docs/*" />
		public static DataTemplate GetEmptyViewTemplate(BindableObject b)
		{
			return (DataTemplate)b.GetValue(EmptyViewTemplateProperty);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/BindableLayout.xml" path="//Member[@MemberName='SetEmptyViewTemplate']/Docs/*" />
		public static void SetEmptyViewTemplate(BindableObject b, DataTemplate value)
		{
			b.SetValue(EmptyViewTemplateProperty, value);
		}
 
		internal static BindableLayoutController GetBindableLayoutController(BindableObject b)
		{
			return (BindableLayoutController)b.GetValue(BindableLayoutControllerProperty);
		}
 
		static void SetBindableLayoutController(BindableObject b, BindableLayoutController value)
		{
			b.SetValue(BindableLayoutControllerProperty, value);
		}
 
		static void OnControllerChanged(BindableObject b, BindableLayoutController oldC, BindableLayoutController newC)
		{
			if (oldC != null)
			{
				oldC.ItemsSource = null;
			}
 
			if (newC == null)
			{
				return;
			}
 
			newC.StartBatchUpdate();
			newC.ItemsSource = GetItemsSource(b);
			newC.ItemTemplate = GetItemTemplate(b);
			newC.ItemTemplateSelector = GetItemTemplateSelector(b);
			newC.EmptyView = GetEmptyView(b);
			newC.EmptyViewTemplate = GetEmptyViewTemplate(b);
			newC.EndBatchUpdate();
		}
 
		internal static void Add(this IBindableLayout layout, object item)
		{
			if (layout is Maui.ILayout mauiLayout && item is IView view)
			{
				mauiLayout.Add(view);
			}
			else
			{
				_ = 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)
		{
			if (layout is Maui.ILayout mauiLayout && item is IView view)
			{
				mauiLayout.Insert(index, view);
			}
			else
			{
				layout.Children.Insert(index, item);
			}
		}
 
		internal static void Remove(this IBindableLayout layout, object item)
		{
			if (layout is Maui.ILayout mauiLayout && item is IView view)
			{
				_ = mauiLayout.Remove(view);
			}
			else
			{
				layout.Children.Remove(item);
			}
		}
 
		internal static void RemoveAt(this IBindableLayout layout, int index)
		{
			if (layout is Maui.ILayout mauiLayout)
			{
				mauiLayout.RemoveAt(index);
			}
			else
			{
				layout.Children.RemoveAt(index);
			}
		}
 
		internal static void Clear(this IBindableLayout layout)
		{
			if (layout is Maui.ILayout mauiLayout)
			{
				mauiLayout.Clear();
			}
			else
			{
				layout.Children.Clear();
			}
		}
	}
 
	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, static (object o) => o);
			return label;
		});
 
		readonly WeakReference<IBindableLayout> _layoutWeakReference;
		readonly WeakNotifyCollectionChangedProxy _collectionChangedProxy = new();
		readonly NotifyCollectionChangedEventHandler _collectionChangedEventHandler;
		IEnumerable _itemsSource;
		DataTemplate _itemTemplate;
		DataTemplateSelector _itemTemplateSelector;
		bool _isBatchUpdate;
		object _emptyView;
		DataTemplate _emptyViewTemplate;
		View _currentEmptyView;
 
		public IEnumerable ItemsSource { get => _itemsSource; set => SetItemsSource(value); }
		public DataTemplate ItemTemplate { get => _itemTemplate; set => SetItemTemplate(value); }
		public DataTemplateSelector ItemTemplateSelector { get => _itemTemplateSelector; set => SetItemTemplateSelector(value); }
 
		public object EmptyView { get => _emptyView; set => SetEmptyView(value); }
		public DataTemplate EmptyViewTemplate { get => _emptyViewTemplate; set => SetEmptyViewTemplate(value); }
 
		public BindableLayoutController(IBindableLayout layout)
		{
			_layoutWeakReference = new WeakReference<IBindableLayout>(layout);
			_collectionChangedEventHandler = ItemsSourceCollectionChanged;
		}
 
		~BindableLayoutController() => _collectionChangedProxy.Unsubscribe();
 
		internal void StartBatchUpdate()
		{
			_isBatchUpdate = true;
		}
 
		internal void EndBatchUpdate()
		{
			_isBatchUpdate = false;
			CreateChildren();
		}
 
		void SetItemsSource(IEnumerable itemsSource)
		{
			if (_itemsSource is INotifyCollectionChanged)
			{
				_collectionChangedProxy.Unsubscribe();
			}
 
			_itemsSource = itemsSource;
 
			if (_itemsSource is INotifyCollectionChanged c)
			{
				_collectionChangedProxy.Subscribe(c, _collectionChangedEventHandler);
			}
 
			if (!_isBatchUpdate)
			{
				CreateChildren();
			}
		}
 
		void SetItemTemplate(DataTemplate itemTemplate)
		{
			if (itemTemplate is DataTemplateSelector)
			{
				throw new NotSupportedException($"You are using an instance of {nameof(DataTemplateSelector)} to set the {nameof(BindableLayout)}.{BindableLayout.ItemTemplateProperty.PropertyName} property. Use {nameof(BindableLayout)}.{BindableLayout.ItemTemplateSelectorProperty.PropertyName} property instead to set an item template selector");
			}
 
			_itemTemplate = itemTemplate;
 
			if (!_isBatchUpdate)
			{
				CreateChildren();
			}
		}
 
		void SetItemTemplateSelector(DataTemplateSelector itemTemplateSelector)
		{
			_itemTemplateSelector = itemTemplateSelector;
 
			if (!_isBatchUpdate)
			{
				CreateChildren();
			}
		}
 
		void SetEmptyView(object emptyView)
		{
			_emptyView = emptyView;
 
			_currentEmptyView = CreateEmptyView(_emptyView, _emptyViewTemplate);
 
			if (!_isBatchUpdate)
			{
				UpdateEmptyView();
			}
		}
 
		void SetEmptyViewTemplate(DataTemplate emptyViewTemplate)
		{
			_emptyViewTemplate = emptyViewTemplate;
 
			_currentEmptyView = CreateEmptyView(_emptyView, _emptyViewTemplate);
 
			if (!_isBatchUpdate)
			{
				UpdateEmptyView();
			}
		}
 
		void UpdateEmptyView()
		{
			if (!_layoutWeakReference.TryGetTarget(out IBindableLayout layout))
			{
				return;
			}
 
			TryAddEmptyView(layout, out _);
		}
 
		void CreateChildren()
		{
			if (!_layoutWeakReference.TryGetTarget(out IBindableLayout layout))
			{
				return;
			}
 
			if (TryAddEmptyView(layout, out IEnumerator enumerator))
			{
				return;
			}
 
			var layoutChildren = layout.Children;
			var childrenCount = layoutChildren.Count;
 
			// if we have the empty view, remove it before generating children
			if (childrenCount == 1 && layoutChildren[0] == _currentEmptyView)
			{
				layout.RemoveAt(0);
				childrenCount = 0;
			}
 
			// Add or replace items
			var index = 0;
			do
			{
				var item = enumerator.Current;
				if (index < childrenCount)
				{
					ReplaceChild(item, layout, layoutChildren, index);
				}
				else
				{
					layout.Add(CreateItemView(item, SelectTemplate(item, layout)));
				}
 
				++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;
			}
		}
 
		bool TryAddEmptyView(IBindableLayout layout, out IEnumerator enumerator)
		{
			enumerator = _itemsSource?.GetEnumerator();
 
			if (enumerator == null || !enumerator.MoveNext())
			{
				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;
			}
 
			return false;
		}
 
		void ClearChildren(IBindableLayout layout)
		{
			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);
			}
		}
 
		DataTemplate SelectTemplate(object item, IBindableLayout layout)
		{
			return _itemTemplate ?? _itemTemplateSelector?.SelectTemplate(item, layout as BindableObject) ?? DefaultItemTemplate;
		}
 
		View CreateEmptyView(object emptyView, DataTemplate dataTemplate)
		{
			if (!_layoutWeakReference.TryGetTarget(out IBindableLayout layout))
			{
				return null;
			}
 
			if (dataTemplate != null)
			{
				var view = (View)dataTemplate.CreateContent();
				return view;
			}
 
			if (emptyView is View emptyLayout)
			{
				return emptyLayout;
			}
 
			return new Label { Text = emptyView?.ToString(), HorizontalTextAlignment = TextAlignment.Center };
		}
 
		void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			if (!_layoutWeakReference.TryGetTarget(out IBindableLayout layout))
			{
				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, _) =>
				{
					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]!;
					layout.RemoveAt(index);
 
					// 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);
		}
 
		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;
		}
	}
}