File: AppThemeBinding.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Xaml.Diagnostics;
 
namespace Microsoft.Maui.Controls
{
	class AppThemeBinding : BindingBase
	{
		public const string AppThemeResource = "__MAUI_ApplicationTheme__";
		class AppThemeProxy : Element
		{
			public AppThemeProxy(Element parent, AppThemeBinding binding)
			{
				_parent = parent;
				Binding = binding;
				this.SetDynamicResource(AppThemeProperty, AppThemeResource);
				((IElementDefinition)parent)?.AddResourcesChangedListener(OnParentResourcesChanged);
			}
 
			public static BindableProperty AppThemeProperty = BindableProperty.Create("AppTheme", typeof(AppTheme), typeof(AppThemeBinding), AppTheme.Unspecified,
				propertyChanged: (bindable, oldValue, newValue) => ((AppThemeProxy)bindable).OnAppThemeChanged());
			readonly Element _parent;
 
			public void OnAppThemeChanged()
			{
				Binding.Apply(false);
			}
 
			public AppThemeBinding Binding { get; }
 
			public void Unsubscribe()
			{
				((IElementDefinition)_parent)?.RemoveResourcesChangedListener(OnParentResourcesChanged);
			}
		}
 
		WeakReference<BindableObject> _weakTarget;
		BindableProperty _targetProperty;
		SetterSpecificity specificity;
		AppThemeProxy _appThemeProxy;
 
		internal override BindingBase Clone()
		{
			var clone = new AppThemeBinding
			{
				Light = Light,
				_isLightSet = _isLightSet,
				Dark = Dark,
				_isDarkSet = _isDarkSet,
				Default = Default
			};
 
			if (VisualDiagnostics.IsEnabled && VisualDiagnostics.GetSourceInfo(this) is SourceInfo info)
				VisualDiagnostics.RegisterSourceInfo(clone, info.SourceUri, info.LineNumber, info.LinePosition);
 
			return clone;
		}
 
		internal override void Apply(bool fromTarget)
		{
			base.Apply(fromTarget);
			ApplyCore();
		}
 
		internal override void Apply(object context, BindableObject bindObj, BindableProperty targetProperty, bool fromBindingContextChanged, SetterSpecificity specificity)
		{
			_weakTarget = new WeakReference<BindableObject>(bindObj);
			_targetProperty = targetProperty;
			base.Apply(context, bindObj, targetProperty, fromBindingContextChanged, specificity);
			this.specificity = specificity;
			ApplyCore(false);
			_appThemeProxy = new AppThemeProxy(bindObj as Element, this);
 
		}
 
		internal override void Unapply(bool fromBindingContextChanged = false)
		{
			_appThemeProxy?.Unsubscribe();
			_appThemeProxy = null;
 
			base.Unapply(fromBindingContextChanged);
			_weakTarget = null;
			_targetProperty = null;
		}
 
		void OnRequestedThemeChanged(object sender, AppThemeChangedEventArgs e)
			=> ApplyCore(true);
 
		void ApplyCore(bool dispatch = false)
		{
			if (_weakTarget == null || !_weakTarget.TryGetTarget(out var target))
			{
				return;
			}
 
			if (dispatch)
				target.Dispatcher.DispatchIfRequired(Set);
			else
				Set();
 
			void Set()
			{
				var value = GetValue();
				if (value is DynamicResource dynamicResource)
					target.SetDynamicResource(_targetProperty, dynamicResource.Key, specificity);
				else
				{
					if (!BindingExpressionHelper.TryConvert(ref value, _targetProperty, _targetProperty.ReturnType, true))
					{
						BindingDiagnostics.SendBindingFailure(this, null, target, _targetProperty, "AppThemeBinding", BindingExpression.CannotConvertTypeErrorMessage, value, _targetProperty.ReturnType);
						return;
					}
					target.SetValueCore(_targetProperty, value, Internals.SetValueFlags.ClearDynamicResource, BindableObject.SetValuePrivateFlags.Default | BindableObject.SetValuePrivateFlags.Converted, specificity);
				}
			};
		}
 
		object _light;
		object _dark;
		bool _isLightSet;
		bool _isDarkSet;
 
		public object Light
		{
			get => _light;
			set
			{
				_light = value;
				_isLightSet = true;
			}
		}
 
		public object Dark
		{
			get => _dark;
			set
			{
				_dark = value;
				_isDarkSet = true;
			}
		}
 
		public object Default { get; set; }
 
		// Ideally this will get reworked to not use `Application.Current` at all
		// https://github.com/dotnet/maui/issues/8713
		// But I'm going with a simple nudge for now so that we can get our 
		// device tests back to a working state and address issues
		// of the more crashing variety
		object GetValue()
		{
			Application app;
 
			if (_weakTarget?.TryGetTarget(out var target) == true &&
				target is VisualElement ve &&
				ve?.Window?.Parent is Application a)
			{
				app = a;
			}
			else
			{
				app = Application.Current;
			}
 
			AppTheme appTheme;
			if (app == null)
				appTheme = AppInfo.RequestedTheme;
			else
				appTheme = app.RequestedTheme;
 
			return appTheme switch
			{
				AppTheme.Dark => _isDarkSet ? Dark : Default,
				_ => _isLightSet ? Light : Default,
			};
		}
	}
}