File: Compatibility\Handlers\ListView\Windows\CellControl.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.Specialized;
using System.ComponentModel;
using Microsoft.Maui.Controls.Handlers.Compatibility;
using Microsoft.Maui.Controls.Internals;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Windows.Foundation;
using Windows.UI.Input;
using WBrush = Microsoft.UI.Xaml.Media.Brush;
using WFlyoutBase = Microsoft.UI.Xaml.Controls.Primitives.FlyoutBase;
using WMenuFlyout = Microsoft.UI.Xaml.Controls.MenuFlyout;
using WSolidColorBrush = Microsoft.UI.Xaml.Media.SolidColorBrush;
 
namespace Microsoft.Maui.Controls.Platform.Compatibility
{
	public partial class CellControl : ContentControl
	{
		public static readonly DependencyProperty CellProperty = DependencyProperty.Register("Cell", typeof(object), typeof(CellControl),
			new PropertyMetadata(null, (o, e) => ((CellControl)o).SetSource((Cell)e.OldValue, (Cell)e.NewValue)));
 
		public static readonly DependencyProperty IsGroupHeaderProperty = DependencyProperty.Register("IsGroupHeader", typeof(bool), typeof(CellControl), null);
 
		internal static readonly BindableProperty MeasuredEstimateProperty = BindableProperty.Create("MeasuredEstimate", typeof(double), typeof(ListView), -1d);
		readonly Lazy<ListView> _listView;
		readonly PropertyChangedEventHandler _propertyChangedHandler;
		WBrush _defaultOnColor;
 
		IList<MenuItem> _contextActions;
		Microsoft.UI.Xaml.DataTemplate _currentTemplate;
		bool _isListViewRealized;
		object _newValue;
 
		public CellControl()
		{
			_listView = new Lazy<ListView>(GetListView);
 
			DataContextChanged += OnDataContextChanged;
 
			Loaded += OnLoaded;
			Unloaded += OnUnloaded;
 
			_propertyChangedHandler = OnCellPropertyChanged;
		}
 
		void OnLoaded(object sender, RoutedEventArgs e)
		{
			if (Cell == null)
				return;
 
			// 🚀 subscribe topropertychanged
			// make sure we do not subscribe twice (because this could happen in SetSource(Cell oldCell, Cell newCell))
			Cell.PropertyChanged -= _propertyChangedHandler;
			Cell.PropertyChanged += _propertyChangedHandler;
		}
 
		void OnUnloaded(object sender, RoutedEventArgs e)
		{
			if (Cell == null)
				return;
 
			Cell.SendDisappearing();
			// 🚀 unsubscribe from propertychanged
			Cell.PropertyChanged -= _propertyChangedHandler;
			// Allows the Cell to unsubscribe from Parent.PropertyChanged
			if (Cell.Parent is ListView)
				Cell.Parent = null;
		}
 
 
		public Cell Cell
		{
			get { return (Cell)GetValue(CellProperty); }
			set { SetValue(CellProperty, value); }
		}
 
		public bool IsGroupHeader
		{
			get { return (bool)GetValue(IsGroupHeaderProperty); }
			set { SetValue(IsGroupHeaderProperty, value); }
		}
 
		protected FrameworkElement CellContent
		{
			get { return (FrameworkElement)Content; }
		}
 
		protected override global::Windows.Foundation.Size MeasureOverride(global::Windows.Foundation.Size availableSize)
		{
			ListView lv = _listView.Value;
 
			// set the Cell now that we have a reference to the ListView, since it will have been skipped
			// on DataContextChanged.
			if (_newValue != null)
			{
				SetCell(_newValue);
				_newValue = null;
			}
 
			if (Content == null)
			{
				if (lv != null)
				{
					if (lv.HasUnevenRows)
					{
						var estimate = (double)lv.GetValue(MeasuredEstimateProperty);
						if (estimate > -1)
							return new global::Windows.Foundation.Size(availableSize.Width, estimate);
					}
					else
					{
						double rowHeight = lv.RowHeight;
						if (rowHeight > -1)
							return new global::Windows.Foundation.Size(availableSize.Width, rowHeight);
					}
				}
 
				// This needs to return a size with a non-zero height; 
				// otherwise, it kills virtualization.
				return new global::Windows.Foundation.Size(0, Cell.DefaultCellHeight);
			}
 
			// Children still need measure called on them
			global::Windows.Foundation.Size result = base.MeasureOverride(availableSize);
 
			if (lv != null)
			{
				lv.SetValue(MeasuredEstimateProperty, result.Height);
			}
 
			SetDefaultSwitchColor();
 
			return result;
		}
 
		ListView GetListView()
		{
			DependencyObject parent = VisualTreeHelper.GetParent(this);
			while (parent != null)
			{
				var lv = parent as ListViewRenderer.ListViewTransparent;
				if (lv != null)
				{
					_isListViewRealized = true;
					return lv.ListViewRenderer.Element;
				}
 
				parent = VisualTreeHelper.GetParent(parent);
			}
 
			return null;
		}
 
		Microsoft.UI.Xaml.DataTemplate GetTemplate(Cell cell)
		{
			return (UI.Xaml.DataTemplate)cell.ToHandler(cell.FindMauiContext()).PlatformView;
		}
 
		void OnCellPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if (e.PropertyName == "HasContextActions")
			{
				SetupContextMenu();
			}
			else if (e.PropertyName == VisualElement.FlowDirectionProperty.PropertyName)
				UpdateFlowDirection(Cell);
			else if (e.PropertyName == SwitchCell.OnProperty.PropertyName ||
				e.PropertyName == SwitchCell.OnColorProperty.PropertyName)
			{
				UpdateOnColor();
			}
		}
 
		void UpdateOnColor()
		{
			if (!(Cell is SwitchCell switchCell))
				return;
 
			var color = switchCell.OnColor.IsDefault()
				? _defaultOnColor
				: new WSolidColorBrush(switchCell.OnColor.ToWindowsColor());
 
			var nativeSwitch = this.GetFirstDescendant<ToggleSwitch>();
 
			// change fill color in switch rectangle
			var rects = nativeSwitch.GetDescendantsByName<Microsoft.UI.Xaml.Shapes.Rectangle>("SwitchKnobBounds");
			foreach (var rect in rects)
				rect.Fill = color;
 
			// change color in animation on PointerOver
			var grid = nativeSwitch.GetFirstDescendant<Microsoft.UI.Xaml.Controls.Grid>();
			var gridVisualStateGroups = Microsoft.UI.Xaml.VisualStateManager.GetVisualStateGroups(grid);
			Microsoft.UI.Xaml.VisualStateGroup vsGroup = null;
			foreach (var visualGroup in gridVisualStateGroups)
			{
				if (visualGroup.Name == "CommonStates")
				{
					vsGroup = visualGroup;
					break;
				}
			}
			if (vsGroup == null)
				return;
 
			Microsoft.UI.Xaml.VisualState vState = null;
			foreach (var visualState in vsGroup.States)
			{
				if (visualState.Name == "PointerOver")
				{
					vState = visualState;
					break;
				}
			}
			if (vState == null)
				return;
 
			var visualStates = vState.Storyboard.Children;
			foreach (var state in visualStates)
			{
				// in XF we were setting the MinWidth of the ToggleSwitch to zero which looks to 
				// setup the visual states of ToggleSwitch to all be ObjectAnimationUsingKeyFrames.
				// This MinWidth was removed which is why this check was added
				//
				// If you find yourself here trying to figure out a SwitchCell issue
				// Try setting the MinWidth on ToggleSwitch to zero	
				if (state is ObjectAnimationUsingKeyFrames item)
				{
					if ((string)item.GetValue(Storyboard.TargetNameProperty) == "SwitchKnobBounds")
					{
						item.KeyFrames[0].Value = color;
						break;
					}
				}
			}
		}
 
		void SetDefaultSwitchColor()
		{
			if (_defaultOnColor == null && Cell is SwitchCell)
			{
				var nativeSwitch = this.GetFirstDescendant<ToggleSwitch>();
				var rects = nativeSwitch.GetDescendantsByName<Microsoft.UI.Xaml.Shapes.Rectangle>("SwitchKnobBounds");
				foreach (var rect in rects)
					_defaultOnColor = rect.Fill;
				UpdateOnColor();
			}
		}
 
		void OnContextActionsChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			var flyout = GetAttachedFlyout();
			if (flyout != null)
			{
				flyout.Items.Clear();
				SetupMenuItems(flyout);
			}
		}
 
		void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
		{
			if (args.NewValue == null)
				return;
 
			// We don't want to set the Cell until the ListView is realized, just in case the 
			// Cell has an ItemTemplate. Instead, we'll store the new data item, and it will be
			// set on MeasureOverrideDelegate. However, if the parent is a TableView, we'll already 
			// have a complete Cell object to work with, so we can move ahead.
			if (_isListViewRealized || args.NewValue is Cell)
				SetCell(args.NewValue);
			else if (args.NewValue != null)
				_newValue = args.NewValue;
		}
 
		private void OnCellContentRightTapped(object sender, RightTappedRoutedEventArgs e)
		{
			OpenContextMenu(e.GetPosition(relativeTo: CellContent));
			e.Handled = true;
		}
 
		/// <summary>
		/// To check the context, not just the text.
		/// </summary>
		WMenuFlyout GetAttachedFlyout()
		{
			if (WFlyoutBase.GetAttachedFlyout(CellContent) is WMenuFlyout flyout)
			{
				var actions = Cell.ContextActions;
				if (flyout.Items.Count != actions.Count)
					return null;
 
				for (int i = 0; i < flyout.Items.Count; i++)
				{
					if (flyout.Items[i].DataContext != actions[i])
						return null;
				}
				return flyout;
			}
			return null;
		}
 
		void OpenContextMenu(Point point)
		{
			if (GetAttachedFlyout() == null)
			{
				var flyout = new WMenuFlyout();
				SetupMenuItems(flyout);
 
				((INotifyCollectionChanged)Cell.ContextActions).CollectionChanged += OnContextActionsChanged;
 
				_contextActions = Cell.ContextActions;
				WFlyoutBase.SetAttachedFlyout(CellContent, flyout);
			}
 
			WFlyoutBase
				.GetAttachedFlyout(CellContent)
				.ShowAt(
					CellContent,
					new FlyoutShowOptions
					{
						Position = point,
					});
		}
 
		void SetCell(object newContext)
		{
			var cell = newContext as Cell;
 
			if (cell != null)
			{
				Cell = cell;
				return;
			}
 
			if (ReferenceEquals(Cell?.BindingContext, newContext))
				return;
 
			// If there is a ListView, load the Cell content from the ItemTemplate.
			// Otherwise, the given Cell is already a templated Cell from a TableView.
			ListView lv = _listView.Value;
 
			if (lv != null)
			{
				Cell oldCell = Cell;
				bool isGroupHeader = IsGroupHeader;
				DataTemplate template = isGroupHeader ? lv.GroupHeaderTemplate : lv.ItemTemplate;
				object bindingContext = newContext;
 
				bool sameTemplate = false;
				if (template is DataTemplateSelector dataTemplateSelector)
				{
					template = dataTemplateSelector.SelectTemplate(bindingContext, lv);
 
					// 🚀 If there exists an old cell, get its data template and check
					// whether the new- and old template matches. In that case, we can recycle it
					if (oldCell?.BindingContext != null)
					{
						DataTemplate oldTemplate = dataTemplateSelector.SelectTemplate(oldCell?.BindingContext, lv);
						sameTemplate = oldTemplate == template;
					}
				}
 
				// Reuse cell
				var canReuseCell = Cell != null && sameTemplate;
 
				// 🚀 If we can reuse the cell, just reuse it...
				if (canReuseCell)
				{
					cell = Cell;
				}
				else if (template != null)
				{
					cell = template.CreateContent() as Cell;
				}
				else
				{
					if (isGroupHeader)
						bindingContext = lv.GetDisplayTextFromGroup(bindingContext);
 
					cell = lv.CreateDefaultCell(bindingContext);
				}
 
				// A TableView cell should already have its parent,
				// but we need to set the parent for a ListView cell.
				cell.Parent = lv;
 
				// Set inherited BindingContext after setting the Parent so it won't be wiped out
				BindableObject.SetInheritedBindingContext(cell, bindingContext);
 
				// This provides the Group Header styling (e.g., larger font, etc.) when the
				// template is loaded later.
				cell.SetIsGroupHeader<ItemsView<Cell>, Cell>(isGroupHeader);
			}
 
			// 🚀 Only set the cell if it DID change
			// Note: The cleanup (SendDisappearing(), etc.) is done by the Cell propertychanged callback so we do not need to do any cleanup ourselves.
 
			if (Cell != cell)
				Cell = cell;
 
			// 🚀 Even if the cell did not change, we **must** call SendDisappearing() and SendAppearing()
			// because frameworks such as Reactive UI rely on this! (this.WhenActivated())
			else if (Cell != null)
			{
				Cell.SendDisappearing();
				Cell.SendAppearing();
			}
		}
 
		void SetSource(Cell oldCell, Cell newCell)
		{
			if (oldCell != null)
			{
				oldCell.PropertyChanged -= _propertyChangedHandler;
				oldCell.SendDisappearing();
			}
 
			if (newCell != null)
			{
				newCell.SendAppearing();
 
				UpdateContent(newCell);
				UpdateFlowDirection(newCell);
				SetupContextMenu();
 
				// 🚀 make sure we do not subscribe twice (OnLoaded!)
				newCell.PropertyChanged -= _propertyChangedHandler;
				newCell.PropertyChanged += _propertyChangedHandler;
			}
		}
 
 
		void SetupContextMenu()
		{
			if (CellContent == null || Cell == null)
				return;
 
			if (!Cell.HasContextActions)
			{
				CellContent.RightTapped -= OnCellContentRightTapped;
				if (_contextActions != null)
				{
					((INotifyCollectionChanged)_contextActions).CollectionChanged -= OnContextActionsChanged;
					_contextActions = null;
				}
 
				WFlyoutBase.SetAttachedFlyout(CellContent, null);
				return;
			}
 
			CellContent.RightTapped += OnCellContentRightTapped;
		}
 
		void SetupMenuItems(WMenuFlyout flyout)
		{
			foreach (MenuItem item in Cell.ContextActions)
			{
				var flyoutItem = new UI.Xaml.Controls.MenuFlyoutItem();
				flyoutItem.SetBinding(UI.Xaml.Controls.MenuFlyoutItem.TextProperty, "Text");
				flyoutItem.SetBinding(UI.Xaml.Controls.MenuFlyoutItem.IconProperty, "IconImageSource", new IconConverter());
				flyoutItem.Command = new MenuItemCommand(item);
				flyoutItem.DataContext = item;
 
				flyout.Items.Add(flyoutItem);
			}
		}
 
		void UpdateContent(Cell newCell)
		{
			Microsoft.UI.Xaml.DataTemplate dt = GetTemplate(newCell);
			if (dt != _currentTemplate || Content == null)
			{
				_currentTemplate = dt;
				Content = dt.LoadContent();
			}
 
			((FrameworkElement)Content).DataContext = newCell;
		}
 
		protected override AutomationPeer OnCreateAutomationPeer()
		{
			return new FrameworkElementAutomationPeer(this);
		}
 
		void UpdateFlowDirection(Cell newCell)
		{
			if (newCell is ViewCell)
				return;
 
			this.UpdateFlowDirection(newCell.Parent as VisualElement);
		}
	}
}