File: Platform\Windows\CollectionView\ItemContentControl.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Graphics;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using WSize = Windows.Foundation.Size;
using WThickness = Microsoft.UI.Xaml.Thickness;
 
namespace Microsoft.Maui.Controls.Platform
{
	public partial class ItemContentControl : ContentControl
	{
		VisualElement _visualElement;
		IViewHandler _handler;
		DataTemplate _currentTemplate;
 
		public ItemContentControl()
		{
			DefaultStyleKey = typeof(ItemContentControl);
			IsTabStop = false;
		}
 
		public static readonly DependencyProperty MauiContextProperty = DependencyProperty.Register(
			nameof(MauiContext), typeof(IMauiContext), typeof(ItemContentControl),
			new PropertyMetadata(default(IMauiContext), MauiContextChanged));
 
		static void MauiContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			if (e.NewValue == null)
			{
				return;
			}
 
			var itemContentControl = (ItemContentControl)d;
			itemContentControl.Realize();
		}
 
		public IMauiContext MauiContext
		{
			get => (IMauiContext)GetValue(MauiContextProperty);
			set => SetValue(MauiContextProperty, value);
		}
 
		public static readonly DependencyProperty FormsDataTemplateProperty = DependencyProperty.Register(
			nameof(FormsDataTemplate), typeof(DataTemplate), typeof(ItemContentControl),
			new PropertyMetadata(default(DataTemplate), FormsDataTemplateChanged));
 
		static void FormsDataTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			if (e.NewValue == null)
			{
				return;
			}
 
			var itemContentControl = (ItemContentControl)d;
			itemContentControl.Realize();
		}
 
		public DataTemplate FormsDataTemplate
		{
			get => (DataTemplate)GetValue(FormsDataTemplateProperty);
			set => SetValue(FormsDataTemplateProperty, value);
		}
 
		public static readonly DependencyProperty FormsDataContextProperty = DependencyProperty.Register(
			nameof(FormsDataContext), typeof(object), typeof(ItemContentControl),
			new PropertyMetadata(default(object), FormsDataContextChanged));
 
		static void FormsDataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var formsContentControl = (ItemContentControl)d;
			formsContentControl.Realize();
		}
 
		public object FormsDataContext
		{
			get => GetValue(FormsDataContextProperty);
			set => SetValue(FormsDataContextProperty, value);
		}
 
		public static readonly DependencyProperty FormsContainerProperty = DependencyProperty.Register(
			nameof(FormsContainer), typeof(BindableObject), typeof(ItemContentControl),
			new PropertyMetadata(default(BindableObject), FormsContainerChanged));
 
		static void FormsContainerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var formsContentControl = (ItemContentControl)d;
			formsContentControl.Realize();
		}
 
		public BindableObject FormsContainer
		{
			get => (BindableObject)GetValue(FormsContainerProperty);
			set => SetValue(FormsContainerProperty, value);
		}
 
		public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register(
			nameof(ItemHeight), typeof(double), typeof(ItemContentControl),
			new PropertyMetadata(default(double)));
 
		public double ItemHeight
		{
			get => (double)GetValue(ItemHeightProperty);
			set => SetValue(ItemHeightProperty, value);
		}
 
		public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register(
			nameof(ItemWidth), typeof(double), typeof(ItemContentControl),
			new PropertyMetadata(default(double)));
 
		public double ItemWidth
		{
			get => (double)GetValue(ItemWidthProperty);
			set => SetValue(ItemWidthProperty, value);
		}
 
		public static readonly DependencyProperty ItemSpacingProperty = DependencyProperty.Register(
			nameof(ItemSpacing), typeof(Thickness), typeof(ItemContentControl),
			new PropertyMetadata(default(Thickness)));
 
		public Thickness ItemSpacing
		{
			get => (Thickness)GetValue(ItemSpacingProperty);
			set => SetValue(ItemSpacingProperty, value);
		}
 
		protected override void OnContentChanged(object oldContent, object newContent)
		{
			base.OnContentChanged(oldContent, newContent);
 
			if (oldContent != null && _visualElement != null)
			{
				_visualElement.MeasureInvalidated -= OnViewMeasureInvalidated;
				_visualElement.PropertyChanged -= OnViewPropertyChanged;
			}
 
			if (newContent != null && _visualElement != null)
			{
				_visualElement.MeasureInvalidated += OnViewMeasureInvalidated;
				_visualElement.PropertyChanged += OnViewPropertyChanged;
				UpdateSemanticProperties(_visualElement);
			}
		}
 
		internal void Realize()
		{
			var dataContext = FormsDataContext;
			var dataTemplate = FormsDataTemplate;
			var container = FormsContainer;
			var mauiContext = MauiContext;
 
			var itemsView = container as ItemsView;
 
			if (itemsView != null && _handler?.VirtualView is Element e)
			{
				itemsView.RemoveLogicalChild(e);
			}
 
			if (dataContext is null || dataTemplate is null || container is null || mauiContext is null)
			{
				return;
			}
 
			// If the template is a DataTemplateSelector, we need to get the actual DataTemplate to use
			// for our recycle check below
			dataTemplate = dataTemplate.SelectDataTemplate(dataContext, container);
 
			if (Content is null || _currentTemplate != dataTemplate)
			{
				// If the content has never been realized (i.e., this is a new instance), 
				// or if we need to switch DataTemplates (because this instance is being recycled)
				// then we'll need to create the content from the template 
				var content = dataTemplate.CreateContent();
				_visualElement = content as VisualElement;
 
				if(_visualElement is null)
				{
					throw new InvalidOperationException($"{dataTemplate} could not be created from {content}");
				}
 
				_visualElement.BindingContext = dataContext;
				_handler = _visualElement.ToHandler(mauiContext);
 
				// We need to set IsPlatformStateConsistent explicitly; otherwise, it won't be set until the renderer's Loaded 
				// event. If the CollectionView is in a Layout, the Layout won't measure or layout the CollectionView until
				// every visible descendant has `IsPlatformStateConsistent == true`. And the problem that Layout is trying
				// to avoid by skipping layout for controls with not-yet-loaded children does not apply to CollectionView
				// items. If we don't set this, the CollectionView just won't get layout at all, and will be invisible until
				// the window is resized. 
				SetNativeStateConsistent(_visualElement);
 
				// Keep track of the template in case this instance gets reused later
				_currentTemplate = dataTemplate;
			}
			else
			{
				// We are reusing this ItemContentControl and we can reuse the Element
				_visualElement.BindingContext = dataContext;
			}
 
			if (_handler.VirtualView is ICrossPlatformLayout)
			{
				Content = _handler.ToPlatform();
			}
			else
			{
				Content = new ContentLayoutPanel(_handler.VirtualView);
			}
 
			itemsView?.AddLogicalChild(_visualElement);
 
			if (itemsView is SelectableItemsView selectableItemsView && selectableItemsView.SelectionMode is not SelectionMode.None)
			{
				bool isSelected = false;
				if (selectableItemsView.SelectionMode == SelectionMode.Single)
					isSelected = selectableItemsView.SelectedItem == FormsDataContext;
				else
					isSelected = selectableItemsView.SelectedItems.Contains(FormsDataContext);
 
				if (isSelected)
					UpdateIsSelected(isSelected);
			}
		}
 
		void SetNativeStateConsistent(VisualElement visualElement)
		{
			visualElement.IsPlatformStateConsistent = true;
 
			foreach (var child in ((IElementController)visualElement).LogicalChildren)
			{
				if (!(child is VisualElement ve))
				{
					continue;
				}
 
				SetNativeStateConsistent(ve);
			}
		}
 
		internal void UpdateIsSelected(bool isSelected)
		{
			var formsElement = _handler?.VirtualView as VisualElement;
 
			if (formsElement == null)
				return;
 
			VisualStateManager.GoToState(formsElement, isSelected
				? VisualStateManager.CommonStates.Selected
				: VisualStateManager.CommonStates.Normal);
		}
 
		void OnViewMeasureInvalidated(object sender, EventArgs e)
		{
			InvalidateMeasure();
		}
 
		void OnViewPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
		{
			if (e.IsOneOf(
				SemanticProperties.HeadingLevelProperty,
				SemanticProperties.HintProperty,
				SemanticProperties.DescriptionProperty,
				AutomationProperties.IsInAccessibleTreeProperty) &&
				sender is IView view)
			{
				UpdateSemanticProperties(view);
			}
		}
 
		void UpdateSemanticProperties(IView view)
		{
			// If you don't set the automation properties on the root element
			// of a list item it just reads out the class type to narrator
			// https://docs.microsoft.com/en-us/accessibility-tools-docs/items/uwpxaml/listitem_name
			// Because this is the root element of the ListViewItem we need to propagate
			// the semantic properties from the root xplat element to this platform element
			if (view == null)
				return;
 
			this.UpdateSemantics(view);
 
			var semantics = view.Semantics;
 
			UI.Xaml.Automation.Peers.AccessibilityView defaultAccessibilityView =
				UI.Xaml.Automation.Peers.AccessibilityView.Content;
 
			if (!String.IsNullOrWhiteSpace(semantics?.Description) || !String.IsNullOrWhiteSpace(semantics?.Hint))
			{
				defaultAccessibilityView = UI.Xaml.Automation.Peers.AccessibilityView.Raw;
			}
 
			this.SetAutomationPropertiesAccessibilityView(_visualElement, defaultAccessibilityView);
		}
 
		/// <inheritdoc/>
		protected override WSize ArrangeOverride(WSize finalSize)
		{
			return base.ArrangeOverride(finalSize);
		}
 
		/// <inheritdoc/>
		protected override WSize MeasureOverride(WSize availableSize)
		{
			if (_handler is null)
			{
				// Make sure we supply a real number for sizes otherwise virtualization won't function
				if (double.IsFinite(availableSize.Width) && !double.IsFinite(availableSize.Height))
					return new WSize(availableSize.Width, 32);
				else if (!double.IsFinite(availableSize.Width) && double.IsFinite(availableSize.Height))
					return new WSize(88, availableSize.Height);
 
				return base.MeasureOverride(availableSize);
			}
 
			var width = ItemWidth == default ? availableSize.Width : ItemWidth;
			var height = ItemHeight == default ? availableSize.Height : ItemHeight;
 
			// I realize if ItemWidth and ItemHeight are set that this call seems pointless, but it's not.
			// From what I can tell, calling `base.MeasureOverride` causes the `ContentControl` to realize its content
			// and build its visual tree. So, in order to just play nice with the WinUI `ContentControl` we always
			// call base.MeasureOverride, so that we don't short circuit whatever bookkeeping it needs to do.
 
			var measureSize = base.MeasureOverride(new WSize(width, height));
 
			return new WSize(Max(measureSize.Width, width), Max(measureSize.Height, height));
		}
 
		double Max(double requested, double available)
		{
			return Math.Max(requested, ClampInfinity(available));
		}
 
		double ClampInfinity(double value)
		{
			return double.IsInfinity(value) ? 0 : value;
		}
 
		internal VisualElement GetVisualElement() => _visualElement;
 
		partial class ContentLayoutPanel : Panel
		{
			IView _view;
			public ContentLayoutPanel(IView view)
			{
				_view = view;
 
				var platformView = view.ToPlatform();
 
#pragma warning disable RS0030 // Do not use banned APIs; Panel.Children is banned for performance reasons. Here we cannot avoid accessing it.
				// Just in case this view is already parented to a wrapper that's been cycled out
				if (platformView.Parent is ContentLayoutPanel clp)
					clp.Children.Remove(platformView);
 
				Children.Add(platformView);
#pragma warning restore RS0030 // Do not use banned APIs
			}
 
			protected override WSize ArrangeOverride(WSize finalSize) => _view.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height)).ToPlatform();
 
			protected override WSize MeasureOverride(WSize availableSize) => _view.Measure(availableSize.Width, availableSize.Height).ToPlatform();
		}
	}
}