Browse Source

Added support for implicit themes.

If no `Theme` property is provided, try to look up a resource keyed with the control's `StyleKey`.
Steven Kirk 3 years ago
parent
commit
d21e634ab3

+ 28 - 0
src/Avalonia.Base/StyledElement.cs

@@ -60,6 +60,7 @@ namespace Avalonia
         public static readonly StyledProperty<ControlTheme?> ThemeProperty =
             AvaloniaProperty.Register<StyledElement, ControlTheme?>(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;
 
         /// <summary>
         /// Initializes static members of the <see cref="StyledElement"/> 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);

+ 2 - 2
src/Avalonia.Base/Styling/IStyleable.cs

@@ -27,9 +27,9 @@ namespace Avalonia.Styling
         ITemplatedControl? TemplatedParent { get; }
 
         /// <summary>
-        /// Gets the theme to be applied to the control.
+        /// Gets the effective theme for the control as used by the syling system.
         /// </summary>
-        public ControlTheme? Theme { get; }
+        ControlTheme? GetEffectiveTheme();
 
         /// <summary>
         /// Notifies the element that a style has been applied.

+ 2 - 2
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)

+ 43 - 0
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<Border>(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]

+ 2 - 2
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()

+ 4 - 2
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)