diff --git a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs index 7771629e0d..250274c39b 100644 --- a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Data.Converters; using Avalonia.LogicalTree; using Avalonia.Reactive; @@ -58,31 +59,39 @@ namespace Avalonia.Controls return false; } - public static IObservable GetResourceObservable(this IStyledElement control, object key) + public static IObservable GetResourceObservable( + this IStyledElement control, + object key, + Func? converter = null) { control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - return new ResourceObservable(control, key); + return new ResourceObservable(control, key, converter); } - public static IObservable GetResourceObservable(this IResourceProvider resourceProvider, object key) + public static IObservable GetResourceObservable( + this IResourceProvider resourceProvider, + object key, + Func? converter = null) { resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); key = key ?? throw new ArgumentNullException(nameof(key)); - return new FloatingResourceObservable(resourceProvider, key); + return new FloatingResourceObservable(resourceProvider, key, converter); } private class ResourceObservable : LightweightObservableBase { private readonly IStyledElement _target; private readonly object _key; + private readonly Func? _converter; - public ResourceObservable(IStyledElement target, object key) + public ResourceObservable(IStyledElement target, object key, Func? converter) { _target = target; _key = key; + _converter = converter; } protected override void Initialize() @@ -97,25 +106,29 @@ namespace Avalonia.Controls protected override void Subscribed(IObserver observer, bool first) { - observer.OnNext(_target.FindResource(_key)); + observer.OnNext(Convert(_target.FindResource(_key))); } private void ResourcesChanged(object sender, ResourcesChangedEventArgs e) { - PublishNext(_target.FindResource(_key)); + PublishNext(Convert(_target.FindResource(_key))); } + + private object? Convert(object? value) => _converter?.Invoke(value) ?? value; } private class FloatingResourceObservable : LightweightObservableBase { private readonly IResourceProvider _target; private readonly object _key; + private readonly Func? _converter; private IResourceHost? _owner; - public FloatingResourceObservable(IResourceProvider target, object key) + public FloatingResourceObservable(IResourceProvider target, object key, Func? converter) { _target = target; _key = key; + _converter = converter; } protected override void Initialize() @@ -134,13 +147,13 @@ namespace Avalonia.Controls { if (_target.Owner is object) { - observer.OnNext(_target.Owner?.FindResource(_key)); + observer.OnNext(Convert(_target.Owner?.FindResource(_key))); } } private void PublishNext() { - PublishNext(_target.Owner?.FindResource(_key)); + PublishNext(Convert(_target.Owner?.FindResource(_key))); } private void OwnerChanged(object sender, EventArgs e) @@ -164,6 +177,8 @@ namespace Avalonia.Controls { PublishNext(); } + + private object? Convert(object? value) => _converter?.Invoke(value) ?? value; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 06c5375520..985a6b9f5a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/ColorToBrushConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/ColorToBrushConverter.cs new file mode 100644 index 0000000000..5b44e9eb50 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/ColorToBrushConverter.cs @@ -0,0 +1,88 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace Avalonia.Markup.Xaml.Converters +{ + /// + /// Converts a to an . + /// + public class ColorToBrushConverter : IValueConverter + { + /// + /// Converts a to an if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// Not used. + /// Not used. + /// + /// If is a and + /// is then converts the color to a solid color brush. + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return Convert(value, targetType); + } + + /// + /// Converts an to a if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// Not used. + /// Not used. + /// + /// If is an and + /// is then converts the solid color brush to a color. + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return ConvertBack(value, targetType); + } + + /// + /// Converts a to an if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// + /// If is a and + /// is then converts the color to a solid color brush. + /// + public static object Convert(object value, Type targetType) + { + if (targetType == typeof(IBrush) && value is Color c) + { + return new ImmutableSolidColorBrush(c); + } + + return value; + } + + /// + /// Converts an to a if the arguments are of the + /// correct type. + /// + /// The value. + /// The target type. + /// + /// If is an and + /// is then converts the solid color brush to a color. + /// + public static object ConvertBack(object value, Type targetType) + { + if (targetType == typeof(Color) && value is ISolidColorBrush brush) + { + return brush.Color; + } + + return value; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index d0d4aae9b8..03fd2e60dd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -1,6 +1,8 @@ using System; using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Markup.Xaml.Converters; +using Avalonia.Media; #nullable enable @@ -54,11 +56,23 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (control != null) { - return InstancedBinding.OneWay(control.GetResourceObservable(ResourceKey)); + var source = control.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); + return InstancedBinding.OneWay(source); } else if (_resourceProvider is object) { - return InstancedBinding.OneWay(_resourceProvider.GetResourceObservable(ResourceKey)); + var source = _resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); + return InstancedBinding.OneWay(source); + } + + return null; + } + + private Func? GetConverter(AvaloniaProperty targetProperty) + { + if (targetProperty?.PropertyType == typeof(IBrush)) + { + return x => ColorToBrushConverter.Convert(x, typeof(IBrush)); } return null; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 6d9fac638c..4ad68fc63e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Reflection; using Avalonia.Controls; using Avalonia.Markup.Data; +using Avalonia.Markup.Xaml.Converters; using Avalonia.Markup.Xaml.XamlIl.Runtime; namespace Avalonia.Markup.Xaml.MarkupExtensions @@ -24,6 +24,14 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public object ProvideValue(IServiceProvider serviceProvider) { var stack = serviceProvider.GetService(); + var provideTarget = serviceProvider.GetService(); + + var targetType = provideTarget.TargetProperty switch + { + AvaloniaProperty ap => ap.PropertyType, + PropertyInfo pi => pi.PropertyType, + _ => null, + }; // Look upwards though the ambient context for IResourceHosts and IResourceProviders // which might be able to give us the resource. @@ -33,30 +41,27 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value)) { - return value; + return ColorToBrushConverter.Convert(value, targetType); } else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value)) { - return value; + return ColorToBrushConverter.Convert(value, targetType); } } - // The resource still hasn't been found, so add a delayed one-time binding. - var provideTarget = serviceProvider.GetService(); - if (provideTarget.TargetObject is IControl target && provideTarget.TargetProperty is PropertyInfo property) { - DelayedBinding.Add(target, property, GetValue); + DelayedBinding.Add(target, property, x => GetValue(x, targetType)); return AvaloniaProperty.UnsetValue; } throw new KeyNotFoundException($"Static resource '{ResourceKey}' not found."); } - private object GetValue(IStyledElement control) + private object GetValue(IStyledElement control, Type targetType) { - return control.FindResource(ResourceKey); + return ColorToBrushConverter.Convert(control.FindResource(ResourceKey), targetType); } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index ed3e4c986b..e651c5f29c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -797,6 +797,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void Automatically_Converts_Color_To_SolidColorBrush() + { + var xaml = @" + + + #ff506070 + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + var brush = (ISolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs index ddfa96c094..efcda47f0f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs @@ -511,6 +511,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(0xff506070, brush.Color.ToUint32()); } + [Fact] + public void Automatically_Converts_Color_To_SolidColorBrush() + { + var xaml = @" + + + #ff506070 + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + var brush = (ISolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With(