File: PlatformBindingHelpers.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.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls.Internals;
 
using static System.String;
 
namespace Microsoft.Maui.Controls.Internals
{
	/// <include file="../../docs/Microsoft.Maui.Controls.Internals/PlatformBindingHelpers.xml" path="Type[@FullName='Microsoft.Maui.Controls.Internals.PlatformBindingHelpers']/Docs/*" />	
	internal static class PlatformBindingHelpers
	{
		[RequiresUnreferencedCode(TrimmerConstants.StringPathBindingWarning, Url = TrimmerConstants.ExpressionBasedBindingsDocsUrl)]
		public static void SetBinding<TPlatformView>(TPlatformView target, string targetProperty, BindingBase bindingBase, string updateSourceEventName = null) where TPlatformView : class
		{
			var binding = bindingBase as Binding;
			//This will allow setting bindings from Xaml by reusing the MarkupExtension
			if (IsNullOrEmpty(updateSourceEventName) && binding != null && !IsNullOrEmpty(binding.UpdateSourceEventName))
				updateSourceEventName = binding.UpdateSourceEventName;
			INotifyPropertyChanged eventWrapper = null;
			if (!IsNullOrEmpty(updateSourceEventName))
				eventWrapper = new EventWrapper<TPlatformView>(target, targetProperty, updateSourceEventName);
 
			SetBinding(target, targetProperty, bindingBase, eventWrapper);
		}
 
		[RequiresUnreferencedCode(TrimmerConstants.StringPathBindingWarning, Url = TrimmerConstants.ExpressionBasedBindingsDocsUrl)]
		public static void SetBinding<TPlatformView>(TPlatformView target, string targetProperty, BindingBase bindingBase, INotifyPropertyChanged propertyChanged) where TPlatformView : class
		{
			if (target == null)
				throw new ArgumentNullException(nameof(target));
			if (IsNullOrEmpty(targetProperty))
				throw new ArgumentNullException(nameof(targetProperty));
 
			var binding = bindingBase as Binding;
			var proxy = BindableObjectProxy<TPlatformView>.BindableObjectProxies.GetValue(target, (TPlatformView key) => new BindableObjectProxy<TPlatformView>(key));
			BindableProperty bindableProperty = null;
			propertyChanged = propertyChanged ?? target as INotifyPropertyChanged;
			var targetPropertyInfo = target.GetType().GetProperty(targetProperty);
			var propertyType = targetPropertyInfo?.PropertyType;
			var defaultValue = targetPropertyInfo?.GetMethod.Invoke(target, Array.Empty<object>());
			bindableProperty = CreateBindableProperty(targetProperty, propertyType, defaultValue);
			if (binding != null && binding.Mode != BindingMode.OneWay && propertyChanged != null)
				propertyChanged.PropertyChanged += (sender, e) =>
				{
					if (e.PropertyName != targetProperty)
						return;
					SetValueFromNative(sender as TPlatformView, targetProperty, bindableProperty);
					//we need to keep the listener around he same time we have the proxy
					proxy.NativeINPCListener = propertyChanged;
				};
 
			if (binding != null && binding.Mode != BindingMode.OneWay)
				SetValueFromNative(target, targetProperty, bindableProperty);
 
			proxy.SetBinding(bindableProperty, bindingBase);
 
			static BindableProperty CreateBindableProperty(string targetProperty, Type propertyType = null, object defaultValue = null)
			{
				propertyType = propertyType ?? typeof(object);
				defaultValue = defaultValue ?? (propertyType.IsValueType ? Activator.CreateInstance(propertyType) : null);
				return BindableProperty.Create(
					targetProperty,
					propertyType,
					typeof(BindableObjectProxy<TPlatformView>),
					defaultValue: defaultValue,
					defaultBindingMode: BindingMode.Default,
					propertyChanged: (bindable, oldValue, newValue) =>
					{
						TPlatformView platformView;
						if ((bindable as BindableObjectProxy<TPlatformView>).TargetReference.TryGetTarget(out platformView))
							SetPlatformValue(platformView, targetProperty, newValue);
					}
				);
			}
 
			static void SetPlatformValue(TPlatformView target, string targetProperty, object newValue)
			{
				var mi = target.GetType().GetProperty(targetProperty)?.SetMethod;
				if (mi == null)
					throw new InvalidOperationException(Format("Native Binding on {0}.{1} failed due to missing or inaccessible property", target.GetType(), targetProperty));
				mi.Invoke(target, new[] { newValue });
			}
 
			static void SetValueFromNative(TPlatformView target, string targetProperty, BindableProperty bindableProperty)
			{
				BindableObjectProxy<TPlatformView> proxy;
				if (!BindableObjectProxy<TPlatformView>.BindableObjectProxies.TryGetValue(target, out proxy))
					return;
				SetValueFromRenderer(proxy, bindableProperty, target.GetType().GetProperty(targetProperty)?.GetMethod.Invoke(target, Array.Empty<object>()));
			}
 
			static void SetValueFromRenderer(BindableObject bindable, BindableProperty property, object value)
			{
				bindable.SetValue(property, value);
			}
		}
 
		public static void SetBinding<TPlatformView>(TPlatformView target, BindableProperty targetProperty, BindingBase binding) where TPlatformView : class
		{
			if (target == null)
				throw new ArgumentNullException(nameof(target));
			if (targetProperty == null)
				throw new ArgumentNullException(nameof(targetProperty));
			if (binding == null)
				throw new ArgumentNullException(nameof(binding));
 
			var proxy = BindableObjectProxy<TPlatformView>.BindableObjectProxies.GetValue(target, (TPlatformView key) => new BindableObjectProxy<TPlatformView>(key));
			proxy.BindingsBackpack.Add(new KeyValuePair<BindableProperty, BindingBase>(targetProperty, binding));
		}
 
		public static void SetValue<TPlatformView>(TPlatformView target, BindableProperty targetProperty, object value) where TPlatformView : class
		{
			if (target == null)
				throw new ArgumentNullException(nameof(target));
			if (targetProperty == null)
				throw new ArgumentNullException(nameof(targetProperty));
 
			var proxy = BindableObjectProxy<TPlatformView>.BindableObjectProxies.GetValue(target, (TPlatformView key) => new BindableObjectProxy<TPlatformView>(key));
			proxy.ValuesBackpack.Add(new KeyValuePair<BindableProperty, object>(targetProperty, value));
		}
 
		public static void SetBindingContext<TPlatformView>(TPlatformView target, object bindingContext, Func<TPlatformView, IEnumerable<TPlatformView>> getChild = null) where TPlatformView : class
		{
			if (target == null)
				throw new ArgumentNullException(nameof(target));
 
			var proxy = BindableObjectProxy<TPlatformView>.BindableObjectProxies.GetValue(target, (TPlatformView key) => new BindableObjectProxy<TPlatformView>(key));
			proxy.BindingContext = bindingContext;
			if (getChild == null)
				return;
			var children = getChild(target);
			if (children == null)
				return;
			foreach (var child in children)
				if (child != null)
					SetBindingContext(child, bindingContext, getChild);
		}
 
		public static void TransferBindablePropertiesToWrapper<TPlatformView, TPlatformWrapper>(TPlatformView platformView, TPlatformWrapper wrapper)
			where TPlatformView : class
			where TPlatformWrapper : View
		{
			BindableObjectProxy<TPlatformView> proxy;
			if (!BindableObjectProxy<TPlatformView>.BindableObjectProxies.TryGetValue(platformView, out proxy))
				return;
			proxy.TransferAttachedPropertiesTo(wrapper);
		}
 
		class EventWrapper<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents)] TTarget> : INotifyPropertyChanged
		{
			string TargetProperty { get; set; }
 
			public EventWrapper(TTarget target, string targetProperty, string updateSourceEventName)
			{
				TargetProperty = targetProperty;
				Delegate handlerDelegate = null;
				EventInfo updateSourceEvent = null;
				try
				{
					updateSourceEvent = typeof(TTarget).GetRuntimeEvent(updateSourceEventName);
					handlerDelegate = ((EventHandler)OnPropertyChanged).Method.CreateDelegate(updateSourceEvent.EventHandlerType, this);
				}
				catch (Exception)
				{
					throw new ArgumentException(Format("No declared or accessible event {0} on {1}", updateSourceEventName, target.GetType()), nameof(updateSourceEventName));
				}
				if (updateSourceEvent != null && handlerDelegate != null)
					updateSourceEvent.AddEventHandler(target, handlerDelegate);
			}
 
			[Preserve]
			void OnPropertyChanged(object sender, EventArgs e)
			{
				PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(TargetProperty));
			}
 
			public event PropertyChangedEventHandler PropertyChanged;
		}
 
		//This needs to be internal for testing purposes
		internal class BindableObjectProxy<TPlatformView> : BindableObject where TPlatformView : class
		{
			public static ConditionalWeakTable<TPlatformView, BindableObjectProxy<TPlatformView>> BindableObjectProxies { get; } = new ConditionalWeakTable<TPlatformView, BindableObjectProxy<TPlatformView>>();
			public WeakReference<TPlatformView> TargetReference { get; set; }
			public IList<KeyValuePair<BindableProperty, BindingBase>> BindingsBackpack { get; } = new List<KeyValuePair<BindableProperty, BindingBase>>();
			public IList<KeyValuePair<BindableProperty, object>> ValuesBackpack { get; } = new List<KeyValuePair<BindableProperty, object>>();
			public INotifyPropertyChanged NativeINPCListener;
 
			public BindableObjectProxy(TPlatformView target)
			{
				TargetReference = new WeakReference<TPlatformView>(target);
			}
 
			public void TransferAttachedPropertiesTo(View wrapper)
			{
				foreach (var kvp in BindingsBackpack)
					wrapper.SetBinding(kvp.Key, kvp.Value);
				foreach (var kvp in ValuesBackpack)
					wrapper.SetValue(kvp.Key, kvp.Value);
			}
		}
	}
}