File: Cells\Cell.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.Maui.Controls.Internals;
 
namespace Microsoft.Maui.Controls
{
	// Don't add IElementConfiguration<Cell> because it kills performance on UWP structures that use Cells
	/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="Type[@FullName='Microsoft.Maui.Controls.Cell']/Docs/*" />
	public abstract class Cell : Element, ICellController, IFlowDirectionController, IPropertyPropagationController, IVisualController, IWindowController, IVisualTreeElement
	{
		/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="//Member[@MemberName='DefaultCellHeight']/Docs/*" />
		public const int DefaultCellHeight = 40;
		/// <summary>Bindable property for <see cref="IsEnabled"/>.</summary>
		public static readonly BindableProperty IsEnabledProperty = BindableProperty.Create(nameof(IsEnabled), typeof(bool), typeof(Cell), true, propertyChanged: OnIsEnabledPropertyChanged);
 
		ObservableCollection<MenuItem> _contextActions;
		List<MenuItem> _currentContextActions;
		readonly Lazy<ElementConfiguration> _elementConfiguration;
 
		double _height = -1;
 
		bool _nextCallToForceUpdateSizeQueued;
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
		public Cell()
		{
			_elementConfiguration = new Lazy<ElementConfiguration>(() => new ElementConfiguration(this));
		}
 
		EffectiveFlowDirection _effectiveFlowDirection = default(EffectiveFlowDirection);
		EffectiveFlowDirection IFlowDirectionController.EffectiveFlowDirection
		{
			get { return _effectiveFlowDirection; }
			set
			{
				if (value == _effectiveFlowDirection)
					return;
 
				_effectiveFlowDirection = value;
 
				var ve = (Parent as VisualElement);
				ve?.InvalidateMeasureInternal(InvalidationTrigger.Undefined);
				OnPropertyChanged(VisualElement.FlowDirectionProperty.PropertyName);
			}
		}
 
		IVisual _effectiveVisual = Microsoft.Maui.Controls.VisualMarker.Default;
		IVisual IVisualController.EffectiveVisual
		{
			get { return _effectiveVisual; }
			set
			{
				if (value == _effectiveVisual)
					return;
 
				_effectiveVisual = value;
				OnPropertyChanged(VisualElement.VisualProperty.PropertyName);
			}
		}
		IVisual IVisualController.Visual => Microsoft.Maui.Controls.VisualMarker.MatchParent;
 
		bool IFlowDirectionController.ApplyEffectiveFlowDirectionToChildContainer => true;
 
		IFlowDirectionController FlowController => this;
		IPropertyPropagationController PropertyPropagationController => this;
 
		Window _window;
		Window IWindowController.Window
		{
			get => _window;
			set
			{
				if (value == _window)
					return;
 
				_window = value;
				OnPropertyChanged(VisualElement.WindowProperty.PropertyName);
			}
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="//Member[@MemberName='ContextActions']/Docs/*" />
		public IList<MenuItem> ContextActions
		{
			get
			{
				if (_contextActions == null)
				{
					_contextActions = new ObservableCollection<MenuItem>();
					_contextActions.CollectionChanged += OnContextActionsChanged;
				}
 
				return _contextActions;
			}
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="//Member[@MemberName='HasContextActions']/Docs/*" />
		public bool HasContextActions
		{
			get { return _contextActions != null && _contextActions.Count > 0 && IsEnabled; }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="//Member[@MemberName='IsContextActionsLegacyModeEnabled']/Docs/*" />
		public bool IsContextActionsLegacyModeEnabled { get; set; } = false;
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="//Member[@MemberName='Height']/Docs/*" />
		public double Height
		{
			get { return _height; }
			set
			{
				if (_height == value)
					return;
 
				OnPropertyChanging(nameof(Height));
				OnPropertyChanging(nameof(RenderHeight));
				_height = value;
				OnPropertyChanged(nameof(Height));
				OnPropertyChanged(nameof(RenderHeight));
			}
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="//Member[@MemberName='IsEnabled']/Docs/*" />
		public bool IsEnabled
		{
			get { return (bool)GetValue(IsEnabledProperty); }
			set { SetValue(IsEnabledProperty, value); }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="//Member[@MemberName='RenderHeight']/Docs/*" />
		public double RenderHeight
		{
			get
			{
				var table = RealParent as TableView;
				if (table != null)
					return table.HasUnevenRows && Height > 0 ? Height : table.RowHeight;
 
				var list = RealParent as ListView;
				if (list != null)
					return list.HasUnevenRows && Height > 0 ? Height : list.RowHeight;
 
				return DefaultCellHeight;
			}
		}
 
		double IFlowDirectionController.Width => (Parent as VisualElement)?.Width ?? 0;
 
		public event EventHandler Appearing;
 
		public event EventHandler Disappearing;
 
		[EditorBrowsable(EditorBrowsableState.Never)]
		public event EventHandler ForceUpdateSizeRequested;
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="//Member[@MemberName='ForceUpdateSize']/Docs/*" />
		public void ForceUpdateSize()
		{
			if (_nextCallToForceUpdateSizeQueued)
				return;
 
			if ((Parent as ListView)?.HasUnevenRows == true || (Parent as TableView)?.HasUnevenRows == true)
			{
				_nextCallToForceUpdateSizeQueued = true;
				OnForceUpdateSizeRequested();
			}
		}
 
		public event EventHandler Tapped;
 
		protected internal virtual void OnTapped()
			=> Tapped?.Invoke(this, EventArgs.Empty);
 
		protected virtual void OnAppearing()
			=> Appearing?.Invoke(this, EventArgs.Empty);
 
		protected override void OnBindingContextChanged()
		{
			base.OnBindingContextChanged();
 
			if (HasContextActions)
			{
				for (var i = 0; i < _contextActions.Count; i++)
					SetInheritedBindingContext(_contextActions[i], BindingContext);
			}
		}
 
		protected virtual void OnDisappearing()
			=> Disappearing?.Invoke(this, EventArgs.Empty);
 
		protected override void OnParentSet()
		{
			if (RealParent != null)
			{
				RealParent.PropertyChanged += OnParentPropertyChanged;
				RealParent.PropertyChanging += OnParentPropertyChanging;
			}
 
			base.OnParentSet();
 
			PropertyPropagationController.PropagatePropertyChanged(null);
		}
 
		protected override void OnPropertyChanging(string propertyName = null)
		{
			if (propertyName == "Parent")
			{
				if (RealParent != null)
				{
					RealParent.PropertyChanged -= OnParentPropertyChanged;
					RealParent.PropertyChanging -= OnParentPropertyChanging;
				}
 
				PropertyPropagationController.PropagatePropertyChanged(null);
			}
 
			base.OnPropertyChanging(propertyName);
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="//Member[@MemberName='SendAppearing']/Docs/*" />
		[EditorBrowsable(EditorBrowsableState.Never)]
		public void SendAppearing()
		{
			OnAppearing();
 
			var container = RealParent as ListView;
			container?.SendCellAppearing(this);
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Cell.xml" path="//Member[@MemberName='SendDisappearing']/Docs/*" />
		[EditorBrowsable(EditorBrowsableState.Never)]
		public void SendDisappearing()
		{
			OnDisappearing();
 
			var container = RealParent as ListView;
			container?.SendCellDisappearing(this);
		}
 
		void IPropertyPropagationController.PropagatePropertyChanged(string propertyName)
		{
			PropertyPropagationExtensions.PropagatePropertyChanged(propertyName, this, ((IVisualTreeElement)this).GetVisualChildren());
		}
 
		void OnContextActionsChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			for (var i = 0; i < _contextActions.Count; i++)
			{
				SetInheritedBindingContext(_contextActions[i], BindingContext);
				_contextActions[i].Parent = this;
				_currentContextActions?.Remove(_contextActions[i]);
			}
 
			if (_currentContextActions != null)
			{
				foreach (MenuItem item in _currentContextActions)
				{
					item.Parent = null;
				}
			}
 
			_currentContextActions = new List<MenuItem>(_contextActions);
 
			OnPropertyChanged(nameof(HasContextActions));
		}
 
		async void OnForceUpdateSizeRequested()
		{
			// don't run more than once per 16 milliseconds
			await Task.Delay(TimeSpan.FromMilliseconds(16));
			ForceUpdateSizeRequested?.Invoke(this, null);
			Handler?.Invoke("ForceUpdateSizeRequested", null);
 
			_nextCallToForceUpdateSizeQueued = false;
		}
 
		static void OnIsEnabledPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
		{
			(bindable as Cell).OnPropertyChanged(nameof(HasContextActions));
		}
 
		void OnParentPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			// Technically we might be raising this even if it didn't change, but I'm taking the bet that
			// its uncommon enough that we don't want to take the penalty of N GetValue calls to verify.
			if (e.PropertyName == "RowHeight")
				OnPropertyChanged(nameof(RenderHeight));
			else if (e.PropertyName == VisualElement.FlowDirectionProperty.PropertyName ||
					 e.PropertyName == VisualElement.VisualProperty.PropertyName)
				PropertyPropagationController.PropagatePropertyChanged(e.PropertyName);
		}
 
		void OnParentPropertyChanging(object sender, PropertyChangingEventArgs e)
		{
			if (e.PropertyName == "RowHeight")
				OnPropertyChanging(nameof(RenderHeight));
		}
 
#if ANDROID
		// This is used by ListView to pass data to the GetCell call
		// Ideally we can pass these as arguments to ToHandler
		// But we'll do that in a different smaller more targeted PR
		internal Android.Views.View ConvertView { get; set; }
#elif IOS
		internal UIKit.UITableViewCell ReusableCell { get; set; }
 
		WeakReference<UIKit.UITableView> _tableView;
 
		internal UIKit.UITableView TableView
		{
			get => _tableView?.GetTargetOrDefault();
			set => _tableView = value is null ? null : new(value);
		}
#endif
 
 
		IReadOnlyList<Maui.IVisualTreeElement> IVisualTreeElement.GetVisualChildren()
		{
			var children = new List<Maui.IVisualTreeElement>(LogicalChildrenInternal);
 
			if (_contextActions != null)
				children.AddRange(_contextActions);
 
			return children;
		}
 
		#region Nested IElementConfiguration<Cell> Implementation
		// This creates a nested class to keep track of IElementConfiguration<Cell> because adding 
		// IElementConfiguration<Cell> to the Cell itself tanks performance on UWP ListViews
		// Issue has been logged with UWP
		public IPlatformElementConfiguration<T, Cell> On<T>() where T : IConfigPlatform
		{
			return GetElementConfiguration().On<T>();
		}
 
		IElementConfiguration<Cell> GetElementConfiguration()
		{
			return _elementConfiguration.Value;
		}
 
		class ElementConfiguration : IElementConfiguration<Cell>
		{
			readonly Lazy<PlatformConfigurationRegistry<Cell>> _platformConfigurationRegistry;
			public ElementConfiguration(Cell cell)
			{
				_platformConfigurationRegistry =
					new Lazy<PlatformConfigurationRegistry<Cell>>(() => new PlatformConfigurationRegistry<Cell>(cell));
			}
 
			/// <inheritdoc/>
			public IPlatformElementConfiguration<T, Cell> On<T>() where T : IConfigPlatform
			{
				return _platformConfigurationRegistry.Value.On<T>();
			}
 
			internal PlatformConfigurationRegistry<Cell> Registry => _platformConfigurationRegistry.Value;
		}
		#endregion
 
	}
}