File: RadioButton\RadioButton.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Graphics;
 
namespace Microsoft.Maui.Controls
{
	/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="Type[@FullName='Microsoft.Maui.Controls.RadioButton']/Docs/*" />
	public partial class RadioButton : TemplatedView, IElementConfiguration<RadioButton>, ITextElement, IFontElement, IBorderElement, IRadioButton
	{
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='CheckedVisualState']/Docs/*" />
		public const string CheckedVisualState = "Checked";
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='UncheckedVisualState']/Docs/*" />
		public const string UncheckedVisualState = "Unchecked";
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='TemplateRootName']/Docs/*" />
		public const string TemplateRootName = "Root";
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='CheckedIndicator']/Docs/*" />
		public const string CheckedIndicator = "CheckedIndicator";
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='UncheckedButton']/Docs/*" />
		public const string UncheckedButton = "Button";
 
		internal const string GroupNameChangedMessage = "RadioButtonGroupNameChanged";
		internal const string ValueChangedMessage = "RadioButtonValueChanged";
 
		// App Theme string constants for Light/Dark modes
		internal const string RadioButtonOuterEllipseStrokeLight = "RadioButtonOuterEllipseStrokeLight";
		internal const string RadioButtonOuterEllipseStrokeDark = "RadioButtonOuterEllipseStrokeDark";
		internal const string RadioButtonCheckGlyphStrokeLight = "RadioButtonCheckGlyphStrokeLight";
		internal const string RadioButtonCheckGlyphStrokeDark = "RadioButtonCheckGlyphStrokeDark";
		internal const string RadioButtonCheckGlyphFillLight = "RadioButtonCheckGlyphFillLight";
		internal const string RadioButtonCheckGlyphFillDark = "RadioButtonCheckGlyphFillDark";
 
		// Older App Theme constants
		internal const string RadioButtonThemeColor = "RadioButtonThemeColor";
		internal const string RadioButtonCheckMarkThemeColor = "RadioButtonCheckMarkThemeColor";
 
 
		// Template Parts
		TapGestureRecognizer _tapGestureRecognizer;
		View _templateRoot;
 
		static ControlTemplate s_defaultTemplate;
 
		readonly Lazy<PlatformConfigurationRegistry<RadioButton>> _platformConfigurationRegistry;
 
		public event EventHandler<CheckedChangedEventArgs> CheckedChanged;
 
		/// <summary>Bindable property for <see cref="Content"/>.</summary>
		public static readonly BindableProperty ContentProperty =
			BindableProperty.Create(nameof(Content), typeof(object), typeof(RadioButton), null);
 
		/// <summary>Bindable property for <see cref="Value"/>.</summary>
		public static readonly BindableProperty ValueProperty =
			BindableProperty.Create(nameof(Value), typeof(object), typeof(RadioButton), null,
			propertyChanged: (b, o, n) => ((RadioButton)b).OnValuePropertyChanged());
 
		/// <summary>Bindable property for <see cref="IsChecked"/>.</summary>
		public static readonly BindableProperty IsCheckedProperty = BindableProperty.Create(
			nameof(IsChecked), typeof(bool), typeof(RadioButton), false,
			propertyChanged: (b, o, n) => ((RadioButton)b).OnIsCheckedPropertyChanged((bool)n),
			defaultBindingMode: BindingMode.TwoWay);
 
		/// <summary>Bindable property for <see cref="GroupName"/>.</summary>
		public static readonly BindableProperty GroupNameProperty = BindableProperty.Create(
			nameof(GroupName), typeof(string), typeof(RadioButton), null,
			propertyChanged: (b, o, n) => ((RadioButton)b).OnGroupNamePropertyChanged((string)o, (string)n));
 
		/// <summary>Bindable property for <see cref="TextColor"/>.</summary>
		public static readonly BindableProperty TextColorProperty = TextElement.TextColorProperty;
 
		/// <summary>Bindable property for <see cref="CharacterSpacing"/>.</summary>
		public static readonly BindableProperty CharacterSpacingProperty = TextElement.CharacterSpacingProperty;
 
		/// <summary>Bindable property for <see cref="TextTransform"/>.</summary>
		public static readonly BindableProperty TextTransformProperty = TextElement.TextTransformProperty;
 
		/// <summary>Bindable property for <see cref="FontAttributes"/>.</summary>
		public static readonly BindableProperty FontAttributesProperty = FontElement.FontAttributesProperty;
 
		/// <summary>Bindable property for <see cref="FontFamily"/>.</summary>
		public static readonly BindableProperty FontFamilyProperty = FontElement.FontFamilyProperty;
 
		/// <summary>Bindable property for <see cref="FontSize"/>.</summary>
		public static readonly BindableProperty FontSizeProperty = FontElement.FontSizeProperty;
 
		/// <summary>Bindable property for <see cref="FontAutoScalingEnabled"/>.</summary>
		public static readonly BindableProperty FontAutoScalingEnabledProperty = FontElement.FontAutoScalingEnabledProperty;
 
		/// <summary>Bindable property for <see cref="BorderColor"/>.</summary>
		public static readonly BindableProperty BorderColorProperty = BorderElement.BorderColorProperty;
 
		/// <summary>Bindable property for <see cref="CornerRadius"/>.</summary>
		public static readonly BindableProperty CornerRadiusProperty = BorderElement.CornerRadiusProperty;
 
		/// <summary>Bindable property for <see cref="BorderWidth"/>.</summary>
		public static readonly BindableProperty BorderWidthProperty = BorderElement.BorderWidthProperty;
 
		// If Content is set to a string, the string will be displayed using the native Text property
		// on platforms which support that; in a ControlTemplate it will be automatically converted
		// to a Label. If Content is set to a View, the View will be displayed on platforms which 
		// support Content natively or in the ContentPresenter of the ControlTemplate, if a ControlTemplate
		// is set. If a ControlTemplate is not set and the platform does not natively support arbitrary
		// Content, the ToString() representation of Content will be displayed.
		// For all types other than View and string, the ToString() representation of Content will be displayed.
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='Content']/Docs/*" />
		public object Content
		{
			get => GetValue(ContentProperty);
			set => SetValue(ContentProperty, value);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='Value']/Docs/*" />
		public object Value
		{
			get => GetValue(ValueProperty);
			set => SetValue(ValueProperty, value);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='IsChecked']/Docs/*" />
		public bool IsChecked
		{
			get { return (bool)GetValue(IsCheckedProperty); }
			set { SetValue(IsCheckedProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='GroupName']/Docs/*" />
		public string GroupName
		{
			get { return (string)GetValue(GroupNameProperty); }
			set { SetValue(GroupNameProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='TextColor']/Docs/*" />
		public Color TextColor
		{
			get { return (Color)GetValue(TextColorProperty); }
			set { SetValue(TextColorProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='CharacterSpacing']/Docs/*" />
		public double CharacterSpacing
		{
			get { return (double)GetValue(CharacterSpacingProperty); }
			set { SetValue(CharacterSpacingProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='TextTransform']/Docs/*" />
		public TextTransform TextTransform
		{
			get { return (TextTransform)GetValue(TextTransformProperty); }
			set { SetValue(TextTransformProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='FontAttributes']/Docs/*" />
		public FontAttributes FontAttributes
		{
			get { return (FontAttributes)GetValue(FontAttributesProperty); }
			set { SetValue(FontAttributesProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='FontFamily']/Docs/*" />
		public string FontFamily
		{
			get { return (string)GetValue(FontFamilyProperty); }
			set { SetValue(FontFamilyProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='FontSize']/Docs/*" />
		[System.ComponentModel.TypeConverter(typeof(FontSizeConverter))]
		public double FontSize
		{
			get { return (double)GetValue(FontSizeProperty); }
			set { SetValue(FontSizeProperty, value); }
		}
 
		public bool FontAutoScalingEnabled
		{
			get => (bool)GetValue(FontAutoScalingEnabledProperty);
			set => SetValue(FontAutoScalingEnabledProperty, value);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='BorderWidth']/Docs/*" />
		public double BorderWidth
		{
			get { return (double)GetValue(BorderWidthProperty); }
			set { SetValue(BorderWidthProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='BorderColor']/Docs/*" />
		public Color BorderColor
		{
			get { return (Color)GetValue(BorderColorProperty); }
			set { SetValue(BorderColorProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='CornerRadius']/Docs/*" />
		public int CornerRadius
		{
			get { return (int)GetValue(CornerRadiusProperty); }
			set { SetValue(CornerRadiusProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
		public RadioButton()
		{
			_platformConfigurationRegistry = new Lazy<PlatformConfigurationRegistry<RadioButton>>(() =>
				new PlatformConfigurationRegistry<RadioButton>(this));
		}
 
		/// <inheritdoc/>
		public IPlatformElementConfiguration<T, RadioButton> On<T>() where T : IConfigPlatform
		{
			return _platformConfigurationRegistry.Value.On<T>();
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='DefaultTemplate']/Docs/*" />
		public static ControlTemplate DefaultTemplate
		{
			get
			{
				if (s_defaultTemplate == null)
				{
					s_defaultTemplate = new ControlTemplate(() => BuildDefaultTemplate());
				}
 
				return s_defaultTemplate;
			}
		}
 
		void ITextElement.OnTextTransformChanged(TextTransform oldValue, TextTransform newValue)
			=> InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
 
		void ITextElement.OnTextColorPropertyChanged(Color oldValue, Color newValue)
		{
		}
 
		void ITextElement.OnCharacterSpacingPropertyChanged(double oldValue, double newValue)
			=> InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
 
		void IFontElement.OnFontFamilyChanged(string oldValue, string newValue) =>
			HandleFontChanged();
 
		void IFontElement.OnFontSizeChanged(double oldValue, double newValue) =>
			HandleFontChanged();
 
		void IFontElement.OnFontAttributesChanged(FontAttributes oldValue, FontAttributes newValue) =>
			HandleFontChanged();
 
		void IFontElement.OnFontAutoScalingEnabledChanged(bool oldValue, bool newValue) =>
			HandleFontChanged();
 
		void HandleFontChanged()
		{
			Handler?.UpdateValue(nameof(ITextStyle.Font));
			InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
		}
 
		double IFontElement.FontSizeDefaultValueCreator() =>
			this.GetDefaultFontSize();
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='UpdateFormsText']/Docs/*" />
		public virtual string UpdateFormsText(string source, TextTransform textTransform)
			=> TextTransformUtilites.GetTransformedText(source, textTransform);
 
		int IBorderElement.CornerRadiusDefaultValue => (int)BorderElement.CornerRadiusProperty.DefaultValue;
 
		Color IBorderElement.BorderColorDefaultValue => (Color)BorderElement.BorderColorProperty.DefaultValue;
 
		double IBorderElement.BorderWidthDefaultValue => (double)BorderElement.BorderWidthProperty.DefaultValue;
 
		void IBorderElement.OnBorderColorPropertyChanged(Color oldValue, Color newValue)
		{
		}
 
		bool IBorderElement.IsCornerRadiusSet() => IsSet(BorderElement.CornerRadiusProperty);
		bool IBorderElement.IsBackgroundColorSet() => IsSet(BackgroundColorProperty);
		bool IBorderElement.IsBackgroundSet() => IsSet(BackgroundProperty);
		bool IBorderElement.IsBorderColorSet() => IsSet(BorderElement.BorderColorProperty);
		bool IBorderElement.IsBorderWidthSet() => IsSet(BorderElement.BorderWidthProperty);
 
		protected internal override void ChangeVisualState()
		{
			ApplyIsCheckedState();
 
			base.ChangeVisualState();
		}
 
		[Obsolete("Use MeasureOverride instead")]
		protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
		{
			if (ControlTemplate == null)
			{
				return Handler?.GetDesiredSize(widthConstraint, heightConstraint) ?? new();
			}
 
			return base.OnMeasure(widthConstraint, heightConstraint);
		}
 
		protected override void OnApplyTemplate()
		{
			base.OnApplyTemplate();
 
			_templateRoot = (this as IControlTemplated)?.TemplateRoot as View;
 
			ApplyIsCheckedState();
			UpdateIsEnabled();
		}
 
		internal override void OnControlTemplateChanged(ControlTemplate oldValue, ControlTemplate newValue)
		{
			base.OnControlTemplateChanged(oldValue, newValue);
		}
 
		void UpdateIsEnabled()
		{
			if (ControlTemplate == null)
			{
				return;
			}
 
			if (_tapGestureRecognizer == null)
			{
				_tapGestureRecognizer = new TapGestureRecognizer();
			}
 
			if (IsEnabled)
			{
				_tapGestureRecognizer.Tapped += SelectRadioButton;
				GestureRecognizers.Add(_tapGestureRecognizer);
			}
			else
			{
				_tapGestureRecognizer.Tapped -= SelectRadioButton;
				GestureRecognizers.Remove(_tapGestureRecognizer);
			}
		}
 
		void ApplyIsCheckedState()
		{
			if (IsChecked)
			{
				VisualStateManager.GoToState(this, CheckedVisualState);
				if (_templateRoot != null)
				{
					VisualStateManager.GoToState(_templateRoot, CheckedVisualState);
				}
			}
			else
			{
				VisualStateManager.GoToState(this, UncheckedVisualState);
				if (_templateRoot != null)
				{
					VisualStateManager.GoToState(_templateRoot, UncheckedVisualState);
				}
			}
		}
 
		void SelectRadioButton(object sender, EventArgs e)
		{
			if (IsEnabled)
				SetValue(IsCheckedProperty, true, specificity: SetterSpecificity.FromHandler);
		}
 
		void OnIsCheckedPropertyChanged(bool isChecked)
		{
			if (isChecked)
				RadioButtonGroup.UpdateRadioButtonGroup(this);
 
			ChangeVisualState();
			CheckedChanged?.Invoke(this, new CheckedChangedEventArgs(isChecked));
		}
 
		void OnValuePropertyChanged()
		{
			if (!IsChecked || string.IsNullOrEmpty(GroupName))
			{
				return;
			}
#pragma warning disable CS0618 // TODO: Remove when we internalize/replace MessagingCenter
			MessagingCenter.Send(this, ValueChangedMessage,
						new RadioButtonValueChanged(RadioButtonGroup.GetVisualRoot(this)));
#pragma warning restore CS0618 // Type or member is obsolete
		}
 
		void OnGroupNamePropertyChanged(string oldGroupName, string newGroupName)
		{
#pragma warning disable CS0618 // TODO: Remove when we internalize/replace MessagingCenter
			if (!string.IsNullOrEmpty(newGroupName))
			{
				if (string.IsNullOrEmpty(oldGroupName))
				{
					MessagingCenter.Subscribe<RadioButton, RadioButtonGroupSelectionChanged>(this,
						RadioButtonGroup.GroupSelectionChangedMessage, HandleRadioButtonGroupSelectionChanged);
					MessagingCenter.Subscribe<Element, RadioButtonGroupValueChanged>(this,
						RadioButtonGroup.GroupValueChangedMessage, HandleRadioButtonGroupValueChanged);
				}
 
				MessagingCenter.Send(this, GroupNameChangedMessage,
					new RadioButtonGroupNameChanged(RadioButtonGroup.GetVisualRoot(this), oldGroupName));
			}
			else
			{
				if (!string.IsNullOrEmpty(oldGroupName))
				{
					MessagingCenter.Unsubscribe<RadioButton, RadioButtonGroupSelectionChanged>(this, RadioButtonGroup.GroupSelectionChangedMessage);
					MessagingCenter.Unsubscribe<Element, RadioButtonGroupValueChanged>(this, RadioButtonGroup.GroupValueChangedMessage);
				}
			}
#pragma warning restore CS0618 // Type or member is obsolete
		}
 
		bool MatchesScope(RadioButtonScopeMessage message)
		{
			return RadioButtonGroup.GetVisualRoot(this) == message.Scope;
		}
 
		void HandleRadioButtonGroupSelectionChanged(RadioButton selected, RadioButtonGroupSelectionChanged args)
		{
			if (!IsChecked || selected == this || string.IsNullOrEmpty(GroupName) || GroupName != selected.GroupName || (Value is not null && object.Equals(Value, args.Value)) || !MatchesScope(args))
			{
				return;
			}
 
			SetValue(IsCheckedProperty, false, specificity: SetterSpecificity.FromHandler);
		}
 
		void HandleRadioButtonGroupValueChanged(Element layout, RadioButtonGroupValueChanged args)
		{
			if (IsChecked || string.IsNullOrEmpty(GroupName) || GroupName != args.GroupName || !object.Equals(Value, args.Value) || !MatchesScope(args))
			{
				return;
			}
 
			SetValue(IsCheckedProperty, true, specificity: SetterSpecificity.FromHandler);
		}
 
		static View BuildDefaultTemplate()
		{
			Border border = new Border()
			{
				Padding = 6
			};
 
			border.SetBinding(BackgroundColorProperty, static (RadioButton rb) => rb.BackgroundColor, source: RelativeBindingSource.TemplatedParent);
			border.SetBinding(HorizontalOptionsProperty, static (RadioButton rb) => rb.HorizontalOptions, source: RelativeBindingSource.TemplatedParent);
			border.SetBinding(MarginProperty, static (RadioButton rb) => rb.Margin, source: RelativeBindingSource.TemplatedParent);
			border.SetBinding(OpacityProperty, static (RadioButton rb) => rb.Opacity, source: RelativeBindingSource.TemplatedParent);
			border.SetBinding(RotationProperty, static (RadioButton rb) => rb.Rotation, source: RelativeBindingSource.TemplatedParent);
			border.SetBinding(ScaleProperty, static (RadioButton rb) => rb.Scale, source: RelativeBindingSource.TemplatedParent);
			border.SetBinding(ScaleXProperty, static (RadioButton rb) => rb.ScaleX, source: RelativeBindingSource.TemplatedParent);
			border.SetBinding(ScaleYProperty, static (RadioButton rb) => rb.ScaleY, source: RelativeBindingSource.TemplatedParent);
			border.SetBinding(TranslationYProperty, static (RadioButton rb) => rb.TranslationY, source: RelativeBindingSource.TemplatedParent);
			border.SetBinding(TranslationXProperty, static (RadioButton rb) => rb.TranslationX, source: RelativeBindingSource.TemplatedParent);
			border.SetBinding(VerticalOptionsProperty, static (RadioButton rb) => rb.VerticalOptions, source: RelativeBindingSource.TemplatedParent);
 
			border.SetBinding(Border.StrokeProperty, static (RadioButton rb) => rb.BorderColor, source: RelativeBindingSource.TemplatedParent);
			border.SetBinding(Border.StrokeShapeProperty, static (RadioButton rb) => rb.CornerRadius, source: RelativeBindingSource.TemplatedParent, converter: new CornerRadiusToShape());
			border.SetBinding(Border.StrokeThicknessProperty, static (RadioButton rb) => rb.BorderWidth, source: RelativeBindingSource.TemplatedParent);
 
			var grid = new Grid
			{
				Padding = 2,
				RowSpacing = 0,
				ColumnSpacing = 6,
				ColumnDefinitions = new ColumnDefinitionCollection {
					new ColumnDefinition { Width = GridLength.Auto },
					new ColumnDefinition { Width = GridLength.Star }
				},
				RowDefinitions = new RowDefinitionCollection {
					new RowDefinition { Height = GridLength.Auto }
				}
			};
 
			var normalEllipse = new Ellipse
			{
				Fill = Brush.Transparent,
				Aspect = Stretch.Uniform,
				HorizontalOptions = LayoutOptions.Center,
				VerticalOptions = LayoutOptions.Center,
				HeightRequest = 21,
				WidthRequest = 21,
				StrokeThickness = 2,
				InputTransparent = false
			};
 
			var checkMark = new Ellipse
			{
				Aspect = Stretch.Uniform,
				HorizontalOptions = LayoutOptions.Center,
				VerticalOptions = LayoutOptions.Center,
				HeightRequest = 11,
				WidthRequest = 11,
				Opacity = 0,
				InputTransparent = false
			};
 
			var contentPresenter = new ContentPresenter
			{
				HorizontalOptions = LayoutOptions.Fill,
				VerticalOptions = LayoutOptions.Fill
			};
 
			object dynamicOuterEllipseThemeColor = null;
			object dynamicCheckMarkThemeColor = null;
			object outerEllipseVisualStateLight = null;
			object outerEllipseVisualStateDark = null;
			object checkMarkVisualStateLight = null;
			object checkMarkVisualStateDark = null;
			object checkMarkFillVisualStateLight = null;
			object checkMarkFillVisualStateDark = null;
 
			if (!normalEllipse.TrySetDynamicThemeColor(
				RadioButtonThemeColor,
				Ellipse.StrokeProperty,
				out dynamicOuterEllipseThemeColor))
			{
				normalEllipse.TrySetAppTheme(
					RadioButtonOuterEllipseStrokeLight,
					RadioButtonOuterEllipseStrokeDark,
					Ellipse.StrokeProperty,
					SolidColorBrush.White,
					SolidColorBrush.Black,
					out outerEllipseVisualStateLight,
					out outerEllipseVisualStateDark);
			}
 
			if (!checkMark.TrySetDynamicThemeColor(
				RadioButtonCheckMarkThemeColor,
				Ellipse.StrokeProperty,
				out dynamicCheckMarkThemeColor))
			{
				checkMark.TrySetAppTheme(
					RadioButtonCheckGlyphStrokeLight,
					RadioButtonCheckGlyphStrokeDark,
					Ellipse.StrokeProperty,
					SolidColorBrush.White,
					SolidColorBrush.Black,
					out checkMarkVisualStateLight,
					out checkMarkVisualStateDark);
			}
 
			if (!checkMark.TrySetDynamicThemeColor(
				RadioButtonCheckMarkThemeColor,
				Ellipse.FillProperty,
				out dynamicCheckMarkThemeColor))
			{
				checkMark.TrySetAppTheme(
					RadioButtonCheckGlyphFillLight,
					RadioButtonCheckGlyphFillDark,
					Ellipse.FillProperty,
					SolidColorBrush.White,
					SolidColorBrush.Black,
					out checkMarkFillVisualStateLight,
					out checkMarkFillVisualStateDark);
			}
 
			contentPresenter.SetBinding(MarginProperty, static (RadioButton radio) => radio.Padding, BindingMode.OneWay, source: RelativeBindingSource.TemplatedParent);
			contentPresenter.SetBinding(BackgroundColorProperty, static (RadioButton radio) => radio.BackgroundColor, BindingMode.OneWay, source: RelativeBindingSource.TemplatedParent);
 
			grid.Add(normalEllipse);
			grid.Add(checkMark);
			grid.Add(contentPresenter, 1, 0);
 
			border.Content = grid;
 
			INameScope nameScope = new NameScope();
			NameScope.SetNameScope(border, nameScope);
			nameScope.RegisterName(TemplateRootName, border);
			nameScope.RegisterName(UncheckedButton, normalEllipse);
			nameScope.RegisterName(CheckedIndicator, checkMark);
			nameScope.RegisterName("ContentPresenter", contentPresenter);
 
			VisualStateGroupList visualStateGroups = new VisualStateGroupList();
 
			var common = new VisualStateGroup() { Name = "Common" };
			common.States.Add(new VisualState() { Name = VisualStateManager.CommonStates.Normal });
			common.States.Add(new VisualState() { Name = VisualStateManager.CommonStates.Disabled });
 
			visualStateGroups.Add(common);
 
			var checkedStates = new VisualStateGroup() { Name = "CheckedStates" };
 
			VisualState checkedVisualState = new VisualState() { Name = CheckedVisualState };
			checkedVisualState.Setters.Add(new Setter() { Property = OpacityProperty, TargetName = CheckedIndicator, Value = 1 });
			checkedVisualState.Setters.Add(
				new Setter()
				{
					Property = Shape.StrokeProperty,
					TargetName = UncheckedButton,
					Value = dynamicOuterEllipseThemeColor is not null ? dynamicOuterEllipseThemeColor : new AppThemeBinding() { Light = outerEllipseVisualStateLight, Dark = outerEllipseVisualStateDark }
				});
			checkedVisualState.Setters.Add(
				new Setter()
				{
					Property = Shape.StrokeProperty,
					TargetName = CheckedIndicator,
					Value = dynamicCheckMarkThemeColor is not null ? dynamicCheckMarkThemeColor : new AppThemeBinding() { Light = checkMarkVisualStateLight, Dark = checkMarkVisualStateDark }
				});
			checkedVisualState.Setters.Add(
				new Setter()
				{
					Property = Shape.FillProperty,
					TargetName = CheckedIndicator,
					Value = dynamicCheckMarkThemeColor is not null ? dynamicCheckMarkThemeColor : new AppThemeBinding() { Light = checkMarkFillVisualStateLight, Dark = checkMarkFillVisualStateDark }
				});
			checkedStates.States.Add(checkedVisualState);
 
			VisualState uncheckedVisualState = new VisualState() { Name = UncheckedVisualState };
			uncheckedVisualState.Setters.Add(new Setter() { Property = OpacityProperty, TargetName = CheckedIndicator, Value = 0 });
 
			uncheckedVisualState.Setters.Add(
				new Setter()
				{
					Property = Shape.StrokeProperty,
					TargetName = UncheckedButton,
					Value = dynamicOuterEllipseThemeColor is not null ? dynamicOuterEllipseThemeColor : new AppThemeBinding() { Light = outerEllipseVisualStateLight, Dark = outerEllipseVisualStateDark }
				});
 
			checkedStates.States.Add(uncheckedVisualState);
 
			visualStateGroups.Add(checkedStates);
 
			VisualStateManager.SetVisualStateGroups(border, visualStateGroups);
 
			return border;
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/RadioButton.xml" path="//Member[@MemberName='ContentAsString']/Docs/*" />
		public string ContentAsString()
		{
			var content = Content;
			if (content is View)
			{
				Application.Current?.FindMauiContext()?.CreateLogger<RadioButton>()?.LogWarning("Warning - {RuntimePlatform} does not support View as the {PropertyName} property of RadioButton; the return value of the ToString() method will be displayed instead.", DeviceInfo.Platform, ContentProperty.PropertyName);
			}
 
			return content?.ToString();
		}
 
		Font ITextStyle.Font => this.ToFont();
 
#if ANDROID	
		object IContentView.Content 
		{
			get
			{
				if (ResolveControlTemplate() == null)
					return ContentAsString();
 
				return Content;
			}
		}
#endif
 
		IView IContentView.PresentedContent => ((this as IControlTemplated).TemplateRoot as IView) ?? (Content as IView);
 
		double IButtonStroke.StrokeThickness => (double)GetValue(BorderWidthProperty);
 
		Color IButtonStroke.StrokeColor => (Color)GetValue(BorderColorProperty);
 
		int IButtonStroke.CornerRadius => (int)GetValue(CornerRadiusProperty);
 
		bool IRadioButton.IsChecked
		{
			get => IsChecked;
			set => SetValue(IsCheckedProperty, value, SetterSpecificity.FromHandler);
		}
 
		private protected override Semantics UpdateSemantics()
		{
			var semantics = base.UpdateSemantics();
 
			if (ControlTemplate != null)
			{
				string contentAsString = ContentAsString();
 
				if (!string.IsNullOrWhiteSpace(contentAsString) && string.IsNullOrWhiteSpace(semantics?.Description))
				{
					semantics ??= new Semantics();
					semantics.Description = contentAsString;
				}
			}
 
			return semantics;
		}
 
		class CornerRadiusToShape : IValueConverter
		{
			public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
			{
				return new RoundRectangle
				{
					CornerRadius = (int)value,
				};
			}
 
			public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
			{
				throw new NotImplementedException();
			}
		}
	}
}