Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

[Bug] AppThemeBindingExtension does not work in a MultiBinding #15037

Open
tonyhallett opened this issue Jan 9, 2022 · 1 comment
Open

[Bug] AppThemeBindingExtension does not work in a MultiBinding #15037

tonyhallett opened this issue Jan 9, 2022 · 1 comment
Labels
a/Xaml </> s/unverified New report that has yet to be verified t/bug 🐛

Comments

@tonyhallett
Copy link
Contributor

Description

AppThemeBindingExtension does not work in a MultiBinding as conversion code needs the property type ( and targetObject is the MultiBinding, targetProperty isnull ) -

?? throw new InvalidOperationException("Cannot determine property to provide the value for.");

Steps to Reproduce

<Label.TextColor> <MultiBinding Converter="{StaticResource MyConverter}"> <Binding Path="ServerReceiver"/> <AppThemeBinding Light="Black" Dark="White" /> </MultiBinding> </Label.TextColor>

Expected Behavior

Given the extension returns a BindingBase it should be usable in a MultiBinding Bindings property - IList<BindingBase>

Actual Behavior

"Cannot determine property to provide the value for."

Basic Information

  • Version with issue: Current / probably all versions
  • Last known good version:

Workaround

Create a new extension - AppThemeBindingInMultiBindingExtension

Reflection due to internals.
Markup extension service IValueConverterProvider is internal.
TypeConversionExtensions is internal.
AppThemeBinding is internal.

Provide the property type for conversion or don't convert.

<Label.TextColor>
                        <MultiBinding Converter="{StaticResource MyConverter}">
                            <Binding Path="ServerReceiver"/>
                            <!-- if using conversion-->
                            <!--<markupExtensions:AppThemeBindingInMultiBindingExtension Light="Black" Dark="White" BindableProperty="{x:Static Label.TextColorProperty}"/>-->
                            <!-- no conversion -->
                            <markupExtensions:AppThemeBindingInMultiBindingExtension Light="{StaticResource LightLabelTextColor}" Dark="{StaticResource DarkLabelTextColor}" />
                        </MultiBinding>
</Label.TextColor>
 public static class XamlParseExceptionLineInfoHelper
    {
        public static XamlParseException CreateException(string message, IServiceProvider serviceProvider, Exception innerException = null)
        {
            return new XamlParseException(message, GetLineInfo(serviceProvider), innerException);
        }

        private static IXmlLineInfo GetLineInfo(IServiceProvider serviceProvider)
        {
            IXmlLineInfoProvider xmlLineInfoProvider = serviceProvider.GetService(typeof(IXmlLineInfoProvider)) as IXmlLineInfoProvider;
            if (xmlLineInfoProvider == null)
            {
                return new XmlLineInfo();
            }

            return xmlLineInfoProvider.XmlLineInfo;
        }

    }

    public class AppThemeBindingReflection
    {
        private static readonly Type appThemeBindingType;
        private static readonly PropertyInfo lightPropertyInfo;
        private static readonly PropertyInfo darkPropertyInfo;
        private static readonly PropertyInfo defaultPropertyInfo;
        static AppThemeBindingReflection()
        {
            var types = typeof(OnPlatform<>).Assembly.GetTypes();
            appThemeBindingType = types.First(t => t.Name == "AppThemeBinding");
            lightPropertyInfo = appThemeBindingType.GetProperty("Light");
            darkPropertyInfo = appThemeBindingType.GetProperty("Dark");
            defaultPropertyInfo = appThemeBindingType.GetProperty("Default");
        }
        public BindingBase AppThemeBinding { get; }

        public AppThemeBindingReflection()
        {
            AppThemeBinding = (BindingBase)Activator.CreateInstance(appThemeBindingType);
        }

        public void SetLight(object value)
        {
            lightPropertyInfo.SetValue(AppThemeBinding, value);
        }

        public void SetDark(object value)
        {
            darkPropertyInfo.SetValue(AppThemeBinding, value);
        }

        public void SetDefault(object value)
        {
            defaultPropertyInfo.SetValue(AppThemeBinding, value);
        }

    }

    [ContentProperty("Default")]
    public class AppThemeBindingInMultiBindingExtension : IMarkupExtension<BindingBase>
    {
        #region reflection 
        private static readonly Type iValueConverterProviderType;
        private static readonly MethodInfo convertWithValueConverterMethodInfo;
        private static readonly MethodInfo convertToMethodInfo;
        #endregion
        static AppThemeBindingInMultiBindingExtension()
        {
            var types = typeof(OnPlatform<>).Assembly.GetTypes();
            var typeConverterExtensionsType = types.First(t => t.Name == "TypeConversionExtensions");
            convertToMethodInfo = typeConverterExtensionsType.GetMethods(BindingFlags.Static | BindingFlags.NonPublic).First(m => m.GetParameters()[2].ParameterType == typeof(Func<MemberInfo>));
            iValueConverterProviderType = types.First(t => t.Name == "IValueConverterProvider");
            convertWithValueConverterMethodInfo = iValueConverterProviderType.GetMethod("Convert");
        }

        #region light, dark, default
        private object _default;

        private bool _hasdefault;

        private object _light;

        private bool _haslight;

        private object _dark;

        private bool _hasdark;

        public object Default
        {
            get
            {
                return _default;
            }
            set
            {
                _default = value;
                _hasdefault = true;
            }
        }

        public object Light
        {
            get
            {
                return _light;
            }
            set
            {
                _light = value;
                _haslight = true;
            }
        }

        public object Dark
        {
            get
            {
                return _dark;
            }
            set
            {
                _dark = value;
                _hasdark = true;
            }
        }
        #endregion
        
        public BindableProperty BindableProperty { get; set; }
        
        private BindingBase GetConfiguredAppThemeBinding(Func<object,object> converter)
        {
            var appThemeBindingReflection = new AppThemeBindingReflection();
            if (_haslight)
            {
                appThemeBindingReflection.SetLight(converter(Light));
            }

            if (_hasdark)
            {
                appThemeBindingReflection.SetDark(converter(Dark));
            }

            if (_hasdefault)
            {
                appThemeBindingReflection.SetDefault(converter(Default));
            }

            return appThemeBindingReflection.AppThemeBinding;
        }

        private BindingBase WithoutConversion()
        {
            return GetConfiguredAppThemeBinding(themeValue => themeValue);
        }

        private BindingBase WithConversion(IServiceProvider serviceProvider)
        {
            var toType = BindableProperty.ReturnType;
            object valueConverterProvider = serviceProvider.GetService(iValueConverterProviderType);
            Func<object, object> convertWithValueConverterProvider = themeValue => convertWithValueConverterMethodInfo.Invoke(valueConverterProvider, new object[] { themeValue, toType, (Func<MemberInfo>)minforetriever, serviceProvider });
            Func<object, object> converter = valueConverterProvider == null ? convertWithValueConverterProvider :
                themeValue =>
                {
                    var args = new object[] { themeValue, toType, (Func<MemberInfo>)minforetriever, serviceProvider, null };
                    var converted = convertToMethodInfo.Invoke(null, args);
                    var exception = (Exception)args[4];
                    if (exception != null)
                    {
                        throw exception;
                    }
                    return converted;
                };

            return GetConfiguredAppThemeBinding(converter);
            
            MemberInfo minforetriever()
            {
                MemberInfo memberInfo = null;
                try
                {
                    memberInfo = BindableProperty.DeclaringType.GetRuntimeProperty(BindableProperty.PropertyName);
                }
                catch (AmbiguousMatchException innerException)
                {
                    throw XamlParseExceptionLineInfoHelper.CreateException($"Multiple properties with name '{BindableProperty.DeclaringType}.{BindableProperty.PropertyName}' found.", serviceProvider, innerException);
                }

                if (memberInfo != null)
                {
                    return memberInfo;
                }

                try
                {
                    return BindableProperty.DeclaringType.GetRuntimeMethod("Get" + BindableProperty.PropertyName, new Type[1]
                    {
                        typeof(BindableObject)
                    });
                }
                catch (AmbiguousMatchException innerException2)
                {
                    throw XamlParseExceptionLineInfoHelper.CreateException($"Multiple methods with name '{BindableProperty.DeclaringType}.Get{BindableProperty.PropertyName}' found.", serviceProvider, innerException2);
                }
            }
        }

        public object ProvideValue(IServiceProvider serviceProvider)
        {
            return ((IMarkupExtension<BindingBase>)this).ProvideValue(serviceProvider);
        }

        BindingBase IMarkupExtension<BindingBase>.ProvideValue(IServiceProvider serviceProvider)
        {
            if (Default == null && Light == null && Dark == null)
            {
                throw XamlParseExceptionLineInfoHelper.CreateException("AppThemeBindingExtension requires a non-null value to be specified for at least one theme or Default.", serviceProvider);
            }

            if (BindableProperty == null)
            {
                return WithoutConversion();
            }

            return WithConversion(serviceProvider);
        }
    }


@tonyhallett tonyhallett added s/unverified New report that has yet to be verified t/bug 🐛 labels Jan 9, 2022
@tonyhallett
Copy link
Contributor Author

Without the reflection but having to provide the BindableProperty for MultiBinding

    public class AppThemeBindingX : IMarkupExtension<BindingBase>
    {
        private AppThemeBindingExtension appThemeBindingExtension = new AppThemeBindingExtension();

        private class ServiceProvider : IServiceProvider, IProvideValueTarget
        {
            private IServiceProvider serviceProviderActual;
            public ServiceProvider(IServiceProvider serviceProviderActual, BindableProperty targetProperty)
            {
                this.serviceProviderActual = serviceProviderActual;
                TargetProperty = targetProperty;
            }

            public object TargetObject => null;

            public object TargetProperty { get; }

            public object GetService(Type serviceType)
            {
                if (serviceType == typeof(IProvideValueTarget))
                {
                    return this;
                }
                return serviceProviderActual.GetService(serviceType);
            }
        }

        public BindableProperty BindableProperty { get; set; }

        public object Dark
        {
            get => appThemeBindingExtension.Dark;
            set => appThemeBindingExtension.Dark = value;
        }

        public object Light
        {
            get => appThemeBindingExtension.Light;
            set => appThemeBindingExtension.Light = value;
        }

        public object Default
        {
            get => appThemeBindingExtension.Default;
            set => appThemeBindingExtension.Default = value;
        }

        public BindingBase ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget provideValueTarget = serviceProvider?.GetService<IProvideValueTarget>() ?? throw new ArgumentException();
            object appThemeBinding = null;
            if (provideValueTarget.TargetObject.GetType() == typeof(MultiBinding))
            {
                appThemeBinding = appThemeBindingExtension.ProvideValue(new ServiceProvider(serviceProvider,BindableProperty));
            }
            else
            {
                appThemeBinding = appThemeBindingExtension.ProvideValue(serviceProvider);
            }
            return (BindingBase)appThemeBinding;
        }

        object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
        {
            return ProvideValue(serviceProvider);
        }
    }

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
a/Xaml </> s/unverified New report that has yet to be verified t/bug 🐛
Projects
None yet
Development

No branches or pull requests

2 participants