File: Layout\Grid.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections.Generic;
using Microsoft.Maui.Layouts;
 
namespace Microsoft.Maui.Controls
{
	/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="Type[@FullName='Microsoft.Maui.Controls.Grid']/Docs/*" />
	[ContentProperty(nameof(Children))]
	public class Grid : Layout, IGridLayout
	{
		readonly Dictionary<IView, GridInfo> _viewInfo = new();
 
		/// <summary>Bindable property for <see cref="ColumnDefinitions"/>.</summary>
		public static readonly BindableProperty ColumnDefinitionsProperty = BindableProperty.Create(nameof(ColumnDefinitions),
			typeof(ColumnDefinitionCollection), typeof(Grid), null, validateValue: (bindable, value) => value != null,
			propertyChanged: UpdateSizeChangedHandlers, defaultValueCreator: bindable =>
			{
				var colDef = new ColumnDefinitionCollection();
				colDef.ItemSizeChanged += ((Grid)bindable).DefinitionsChanged;
				return colDef;
			});
 
		/// <summary>Bindable property for <see cref="RowDefinitions"/>.</summary>
		public static readonly BindableProperty RowDefinitionsProperty = BindableProperty.Create(nameof(RowDefinitions),
			typeof(RowDefinitionCollection), typeof(Grid), null, validateValue: (bindable, value) => value != null,
			propertyChanged: UpdateSizeChangedHandlers, defaultValueCreator: bindable =>
			{
				var rowDef = new RowDefinitionCollection();
				rowDef.ItemSizeChanged += ((Grid)bindable).DefinitionsChanged;
				return rowDef;
			});
 
		/// <summary>Bindable property for <see cref="RowSpacing"/>.</summary>
		public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create(nameof(RowSpacing), typeof(double),
			typeof(Grid), 0d, propertyChanged: Invalidate);
 
		/// <summary>Bindable property for <see cref="ColumnSpacing"/>.</summary>
		public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create(nameof(ColumnSpacing), typeof(double),
			typeof(Grid), 0d, propertyChanged: Invalidate);
 
		#region Row/Column/Span Attached Properties
 
		/// <summary>Bindable property for attached property <c>Row</c>.</summary>
		public static readonly BindableProperty RowProperty = BindableProperty.CreateAttached("Row",
			typeof(int), typeof(Grid), default(int), validateValue: (bindable, value) => (int)value >= 0,
			propertyChanged: Invalidate);
 
		/// <summary>Bindable property for attached property <c>RowSpan</c>.</summary>
		public static readonly BindableProperty RowSpanProperty = BindableProperty.CreateAttached("RowSpan",
			typeof(int), typeof(Grid), 1, validateValue: (bindable, value) => (int)value >= 1,
			propertyChanged: Invalidate);
 
		/// <summary>Bindable property for attached property <c>Column</c>.</summary>
		public static readonly BindableProperty ColumnProperty = BindableProperty.CreateAttached("Column",
			typeof(int), typeof(Grid), default(int), validateValue: (bindable, value) => (int)value >= 0,
			propertyChanged: Invalidate);
 
		/// <summary>Bindable property for attached property <c>ColumnSpan</c>.</summary>
		public static readonly BindableProperty ColumnSpanProperty = BindableProperty.CreateAttached("ColumnSpan",
			typeof(int), typeof(Grid), 1, validateValue: (bindable, value) => (int)value >= 1,
			propertyChanged: Invalidate);
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='GetColumn'][1]/Docs/*" />
		public static int GetColumn(BindableObject bindable)
		{
			return (int)bindable.GetValue(ColumnProperty);
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='GetColumnSpan'][1]/Docs/*" />
		public static int GetColumnSpan(BindableObject bindable)
		{
			return (int)bindable.GetValue(ColumnSpanProperty);
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='GetRow'][1]/Docs/*" />
		public static int GetRow(BindableObject bindable)
		{
			return (int)bindable.GetValue(RowProperty);
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='GetRowSpan'][1]/Docs/*" />
		public static int GetRowSpan(BindableObject bindable)
		{
			return (int)bindable.GetValue(RowSpanProperty);
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='SetColumn'][1]/Docs/*" />
		public static void SetColumn(BindableObject bindable, int value)
		{
			bindable.SetValue(ColumnProperty, value);
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='SetColumnSpan'][1]/Docs/*" />
		public static void SetColumnSpan(BindableObject bindable, int value)
		{
			bindable.SetValue(ColumnSpanProperty, value);
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='SetRow'][1]/Docs/*" />
		public static void SetRow(BindableObject bindable, int value)
		{
			bindable.SetValue(RowProperty, value);
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='SetRowSpan'][1]/Docs/*" />
		public static void SetRowSpan(BindableObject bindable, int value)
		{
			bindable.SetValue(RowSpanProperty, value);
		}
 
		#endregion
 
		ReadOnlyCastingList<IGridRowDefinition, RowDefinition> _rowDefs;
		ReadOnlyCastingList<IGridColumnDefinition, ColumnDefinition> _colDefs;
		IReadOnlyList<IGridRowDefinition> IGridLayout.RowDefinitions => _rowDefs ??= new(RowDefinitions);
		IReadOnlyList<IGridColumnDefinition> IGridLayout.ColumnDefinitions => _colDefs ??= new(ColumnDefinitions);
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='ColumnDefinitions']/Docs/*" />
		[System.ComponentModel.TypeConverter(typeof(ColumnDefinitionCollectionTypeConverter))]
		public ColumnDefinitionCollection ColumnDefinitions
		{
			get { return (ColumnDefinitionCollection)GetValue(ColumnDefinitionsProperty); }
			set { SetValue(ColumnDefinitionsProperty, value); }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='RowDefinitions']/Docs/*" />
		[System.ComponentModel.TypeConverter(typeof(RowDefinitionCollectionTypeConverter))]
		public RowDefinitionCollection RowDefinitions
		{
			get { return (RowDefinitionCollection)GetValue(RowDefinitionsProperty); }
			set { SetValue(RowDefinitionsProperty, value); }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='RowSpacing']/Docs/*" />
		public double RowSpacing
		{
			get { return (double)GetValue(RowSpacingProperty); }
			set { SetValue(RowSpacingProperty, value); }
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls/Grid.xml" path="//Member[@MemberName='ColumnSpacing']/Docs/*" />
		public double ColumnSpacing
		{
			get { return (double)GetValue(ColumnSpacingProperty); }
			set { SetValue(ColumnSpacingProperty, value); }
		}
 
		public int GetColumn(IView view)
		{
			return view switch
			{
				BindableObject bo => (int)bo.GetValue(ColumnProperty),
				_ => _viewInfo[view].Col,
			};
		}
 
		public int GetColumnSpan(IView view)
		{
			return view switch
			{
				BindableObject bo => (int)bo.GetValue(ColumnSpanProperty),
				_ => _viewInfo[view].ColSpan,
			};
		}
 
		public int GetRow(IView view)
		{
			return view switch
			{
				BindableObject bo => (int)bo.GetValue(RowProperty),
				_ => _viewInfo[view].Row,
			};
		}
 
		public int GetRowSpan(IView view)
		{
			return view switch
			{
				BindableObject bo => (int)bo.GetValue(RowSpanProperty),
				_ => _viewInfo[view].RowSpan,
			};
		}
 
		public void AddRowDefinition(RowDefinition gridRowDefinition)
		{
			RowDefinitions.Add(gridRowDefinition);
		}
 
		public void AddColumnDefinition(ColumnDefinition gridColumnDefinition)
		{
			ColumnDefinitions.Add(gridColumnDefinition);
		}
 
		public void SetRow(IView view, int row)
		{
			switch (view)
			{
				case BindableObject bo:
					bo.SetValue(RowProperty, row);
					break;
				default:
					_viewInfo[view].Row = row;
					InvalidateMeasure();
					break;
			}
		}
 
		public void SetRowSpan(IView view, int span)
		{
			switch (view)
			{
				case BindableObject bo:
					bo.SetValue(RowSpanProperty, span);
					break;
				default:
					_viewInfo[view].RowSpan = span;
					InvalidateMeasure();
					break;
			}
		}
 
		public void SetColumn(IView view, int col)
		{
			switch (view)
			{
				case BindableObject bo:
					bo.SetValue(ColumnProperty, col);
					break;
				default:
					_viewInfo[view].Col = col;
					InvalidateMeasure();
					break;
			}
		}
 
		public void SetColumnSpan(IView view, int span)
		{
			switch (view)
			{
				case BindableObject bo:
					bo.SetValue(ColumnSpanProperty, span);
					break;
				default:
					_viewInfo[view].ColSpan = span;
					InvalidateMeasure();
					break;
			}
		}
 
		protected override void OnAdd(int index, IView view)
		{
			if (view is not BindableObject)
			{
				_viewInfo[view] = new GridInfo();
			}
 
			base.OnAdd(index, view);
		}
 
		protected override void OnClear()
		{
			_viewInfo.Clear();
			base.OnClear();
		}
 
		protected override void OnRemove(int index, IView view)
		{
			_viewInfo.Remove(view);
			base.OnRemove(index, view);
		}
 
		protected override void OnInsert(int index, IView view)
		{
			if (view is not BindableObject)
			{
				_viewInfo[view] = new GridInfo();
			}
 
			base.OnInsert(index, view);
		}
 
		protected override void OnUpdate(int index, IView view, IView oldView)
		{
			_viewInfo.Remove(oldView);
 
			if (view is not BindableObject)
			{
				_viewInfo[view] = new GridInfo();
			}
 
			base.OnUpdate(index, view, oldView);
		}
 
		protected override ILayoutManager CreateLayoutManager() => new GridLayoutManager(this);
 
		static void UpdateSizeChangedHandlers(BindableObject bindable, object oldValue, object newValue)
		{
			var gridLayout = (Grid)bindable;
 
			if (oldValue is ColumnDefinitionCollection oldColDefs)
			{
				oldColDefs.ItemSizeChanged -= gridLayout.DefinitionsChanged;
			}
			else if (oldValue is RowDefinitionCollection oldRowDefs)
			{
				oldRowDefs.ItemSizeChanged -= gridLayout.DefinitionsChanged;
			}
 
			if (newValue is ColumnDefinitionCollection newColDefs)
			{
				newColDefs.ItemSizeChanged += gridLayout.DefinitionsChanged;
			}
			else if (newValue is RowDefinitionCollection newRowDefs)
			{
				newRowDefs.ItemSizeChanged += gridLayout.DefinitionsChanged;
			}
 
			gridLayout.DefinitionsChanged(bindable, EventArgs.Empty);
		}
 
		static void Invalidate(BindableObject bindable, object oldValue, object newValue)
		{
			if (bindable is Grid grid)
			{
				grid.InvalidateMeasure();
			}
			else if (bindable is Element element && element.Parent is Grid parentGrid)
			{
				parentGrid.InvalidateMeasure();
			}
		}
 
		void DefinitionsChanged(object sender, EventArgs args)
		{
			// Clear out the IGridLayout row/col defs; they'll be set up again next time they're accessed
			_rowDefs = null;
			_colDefs = null;
 
			UpdateRowColumnBindingContexts();
 
			InvalidateMeasure();
		}
 
		protected override void InvalidateMeasure()
		{
			base.InvalidateMeasure();
			(this as IView)?.InvalidateMeasure();
		}
 
		protected override void OnBindingContextChanged()
		{
			base.OnBindingContextChanged();
			UpdateRowColumnBindingContexts();
		}
 
		void UpdateRowColumnBindingContexts()
		{
			var bindingContext = BindingContext;
 
			RowDefinitionCollection rowDefs = RowDefinitions;
			for (var i = 0; i < rowDefs.Count; i++)
			{
				SetInheritedBindingContext(rowDefs[i], bindingContext);
			}
 
			ColumnDefinitionCollection colDefs = ColumnDefinitions;
			for (var i = 0; i < colDefs.Count; i++)
			{
				SetInheritedBindingContext(colDefs[i], bindingContext);
			}
		}
 
		class GridInfo
		{
			public int Row { get; set; }
			public int Col { get; set; }
			public int RowSpan { get; set; } = 1;
			public int ColSpan { get; set; } = 1;
		}
	}
}