diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 75c4b94174..f377eb848c 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -60,6 +60,7 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); + private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; private readonly Classes _classes = new Classes(); @@ -72,6 +73,7 @@ namespace Avalonia private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; private bool _hasPromotedTheme; + private ControlTheme? _implicitTheme; /// /// Initializes static members of the class. @@ -495,6 +497,31 @@ namespace Avalonia }; } + ControlTheme? IStyleable.GetEffectiveTheme() + { + var theme = Theme; + + // Explitly set Theme property takes precedence. + if (theme is not null) + return theme; + + // If the Theme property is not set, try to find a ControlTheme resource with our StyleKey. + if (_implicitTheme is null) + { + var key = ((IStyleable)this).StyleKey; + + if (this.TryFindResource(key, out var value) && value is ControlTheme t) + _implicitTheme = t; + else + _implicitTheme = s_invalidTheme; + } + + if (_implicitTheme != s_invalidTheme) + return _implicitTheme; + + return null; + } + void IStyleable.StyleApplied(IStyleInstance instance) { instance = instance ?? throw new ArgumentNullException(nameof(instance)); @@ -736,6 +763,7 @@ namespace Avalonia if (_logicalRoot != null) { _logicalRoot = null; + _implicitTheme = null; DetachStyles(); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); diff --git a/src/Avalonia.Base/Styling/IStyleable.cs b/src/Avalonia.Base/Styling/IStyleable.cs index 61fcbdf850..254da4d85c 100644 --- a/src/Avalonia.Base/Styling/IStyleable.cs +++ b/src/Avalonia.Base/Styling/IStyleable.cs @@ -27,9 +27,9 @@ namespace Avalonia.Styling ITemplatedControl? TemplatedParent { get; } /// - /// Gets the theme to be applied to the control. + /// Gets the effective theme for the control as used by the syling system. /// - public ControlTheme? Theme { get; } + ControlTheme? GetEffectiveTheme(); /// /// Notifies the element that a style has been applied. diff --git a/src/Avalonia.Base/Styling/Styler.cs b/src/Avalonia.Base/Styling/Styler.cs index c9ea123bdc..6ac2e8d372 100644 --- a/src/Avalonia.Base/Styling/Styler.cs +++ b/src/Avalonia.Base/Styling/Styler.cs @@ -11,10 +11,10 @@ namespace Avalonia.Styling // If the control has a themed templated parent then first apply the styles from // the templated parent theme. if (target.TemplatedParent is IStyleable styleableParent) - styleableParent.Theme?.TryAttach(target, styleableParent); + styleableParent.GetEffectiveTheme()?.TryAttach(target, styleableParent); // Next apply the control theme. - target.Theme?.TryAttach(target, target); + target.GetEffectiveTheme()?.TryAttach(target, target); // Apply styles from the rest of the tree. if (target is IStyleHost styleHost) diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index ab6c239393..522937b669 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -158,6 +158,49 @@ public class StyledElementTests_Theming } } + public class ImplicitTheme + { + [Fact] + public void Implicit_Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); + Assert.NotNull(target.Template); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(Brushes.Red, border.Background); + + target.Classes.Add("foo"); + Assert.Equal(Brushes.Green, border.Background); + } + + [Fact] + public void Implicit_Theme_Is_Cleared_When_Removed_From_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); + + Assert.NotNull(((IStyleable)target).GetEffectiveTheme()); + + root.Child = null; + + Assert.Null(((IStyleable)target).GetEffectiveTheme()); + } + + private static ThemedControl CreateTarget() => new ThemedControl(); + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot(); + result.Resources.Add(typeof(ThemedControl), CreateTheme()); + result.Child = child; + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } + } + public class ThemeFromStyle { [Fact] diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index ca59fe8480..75e21a7138 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -137,9 +137,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters get { throw new NotImplementedException(); } } - public ControlTheme Theme + public ControlTheme GetEffectiveTheme() { - get { throw new NotImplementedException(); } + throw new NotImplementedException(); } public void DetachStyles() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index 592dbfc0d1..987725c314 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -845,7 +845,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal("bar", border.Tag); var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0]; - Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources); + Assert.Contains("bar", resourceProvider.RequestedResources); + Assert.DoesNotContain("foo", resourceProvider.RequestedResources); } [Fact] @@ -883,7 +884,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal("bar", border.Tag); var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0]; - Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources); + Assert.Contains("bar", resourceProvider.RequestedResources); + Assert.DoesNotContain("foo", resourceProvider.RequestedResources); } private IDisposable StyledWindow(params (string, string)[] assets)