diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs
index 3bf72460df..95628fb087 100644
--- a/src/Avalonia.Controls/Application.cs
+++ b/src/Avalonia.Controls/Application.cs
@@ -163,7 +163,7 @@ namespace Avalonia
IStyleHost IStyleHost.StylingParent => null;
///
- bool IStyleHost.IsStylesInitialized => _styles != null;
+ bool IStyleHost.HasStyles => _styles?.Count > 0;
///
/// Application lifetime, use it for things like setting the main window and exiting the app from code
diff --git a/src/Avalonia.Controls/ControlTheme.cs b/src/Avalonia.Controls/ControlTheme.cs
new file mode 100644
index 0000000000..9f003e309a
--- /dev/null
+++ b/src/Avalonia.Controls/ControlTheme.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Styling;
+
+#nullable enable
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines a switchable theme for a control.
+ ///
+ public class ControlTheme : StyleBase
+ {
+ private Styles? _styles;
+
+ ///
+ /// Gets the child styles of the control theme.
+ ///
+ public Styles Styles => _styles ??= new Styles(Owner);
+
+ protected override IReadOnlyList GetChildrenCore()
+ {
+ return (IReadOnlyList?)_styles ?? Array.Empty();
+ }
+
+ protected override bool GetHasResourcesCore()
+ {
+ if (ResourcesCore?.Count > 0)
+ {
+ return true;
+ }
+
+ return ((IResourceNode?)_styles)?.HasResources ?? false;
+ }
+
+ public override SelectorMatchResult TryAttach(IStyleable target, object? host)
+ {
+ if (target == host)
+ {
+ // If target and host are the same control, then we're applying styles to the
+ // control that the theme is applied to.
+ Attach(target);
+ _styles?.TryAttach(target, host);
+ return SelectorMatchResult.AlwaysThisType;
+ }
+ else
+ {
+ // If the target is different to the host then we're applying styles to a templated
+ // child of the host. The setters in the control theme itself don't apply here: only
+ // the child styles.
+ return _styles?.TryAttach(target, host) ?? SelectorMatchResult.NeverThisType;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
index 820d5777f5..69efbcc8c4 100644
--- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs
+++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
@@ -5,14 +5,13 @@ using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Styling;
-using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
///
/// A lookless control whose visual appearance is defined by its .
///
- public class TemplatedControl : Control, ITemplatedControl
+ public class TemplatedControl : Control, ITemplatedControl, IThemed
{
///
/// Defines the property.
@@ -74,6 +73,12 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty TemplateProperty =
AvaloniaProperty.Register(nameof(Template));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ThemeProperty =
+ AvaloniaProperty.Register(nameof(Theme));
+
///
/// Defines the IsTemplateFocusTarget attached property.
///
@@ -88,6 +93,8 @@ namespace Avalonia.Controls.Primitives
"TemplateApplied",
RoutingStrategies.Direct);
+ private static ControlTheme s_unthemed = CreateUnthemed();
+
private IControlTemplate _appliedTemplate;
///
@@ -97,6 +104,7 @@ namespace Avalonia.Controls.Primitives
{
ClipToBoundsProperty.OverrideDefaultValue(true);
TemplateProperty.Changed.AddClassHandler((x, e) => x.OnTemplateChanged(e));
+ ThemeProperty.Changed.AddClassHandler((x, e) => x.OnThemeChanged(e));
}
///
@@ -198,6 +206,17 @@ namespace Avalonia.Controls.Primitives
set { SetValue(TemplateProperty, value); }
}
+ ///
+ /// Gets or sets the theme to be applied to the control.
+ ///
+ public IStyle Theme
+ {
+ get { return GetValue(ThemeProperty); }
+ set { SetValue(ThemeProperty, value); }
+ }
+
+ IStyle IThemed.Theme => Theme ?? GetDefaultControlTheme();
+
///
/// Gets the value of the IsTemplateFocusTargetProperty attached property on a control.
///
@@ -265,7 +284,9 @@ namespace Avalonia.Controls.Primitives
var e = new TemplateAppliedEventArgs(nameScope);
OnApplyTemplate(e);
+#pragma warning disable CS0618 // Type or member is obsolete
OnTemplateApplied(e);
+#pragma warning restore CS0618 // Type or member is obsolete
RaiseEvent(e);
}
@@ -273,6 +294,8 @@ namespace Avalonia.Controls.Primitives
}
}
+ protected virtual IStyle GetDefaultControlTheme() => s_unthemed;
+
///
protected override IControl GetTemplateFocusTarget()
{
@@ -362,5 +385,37 @@ namespace Avalonia.Controls.Primitives
}
}
}
+
+ ///
+ /// Called when the property changes.
+ ///
+ /// The event args.
+ private void OnThemeChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ InvalidateStyles();
+ }
+
+ private static ControlTheme CreateUnthemed()
+ {
+ var template = new FuncControlTemplate((o, n) =>
+ new Border
+ {
+ Background = Brushes.Red,
+ Child = new TextBlock
+ {
+ Foreground = Brushes.White,
+ Text = "No template found.",
+ }
+ });
+ ;
+
+ return new ControlTheme
+ {
+ Setters =
+ {
+ new Setter(TemplateProperty, template),
+ }
+ };
+ }
}
}
diff --git a/src/Avalonia.Styling/Controls/IThemed.cs b/src/Avalonia.Styling/Controls/IThemed.cs
new file mode 100644
index 0000000000..080a160f2e
--- /dev/null
+++ b/src/Avalonia.Styling/Controls/IThemed.cs
@@ -0,0 +1,17 @@
+using Avalonia.Styling;
+
+#nullable enable
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Represents a themed element.
+ ///
+ public interface IThemed
+ {
+ ///
+ /// Gets the theme style for the element.
+ ///
+ public IStyle? Theme { get; }
+ }
+}
diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs
index bdd01924f1..c46be1144e 100644
--- a/src/Avalonia.Styling/StyledElement.cs
+++ b/src/Avalonia.Styling/StyledElement.cs
@@ -298,7 +298,7 @@ namespace Avalonia
Type IStyleable.StyleKey => GetType();
///
- bool IStyleHost.IsStylesInitialized => _styles != null;
+ bool IStyleHost.HasStyles => _styles?.Count > 0;
///
IStyleHost? IStyleHost.StylingParent => (IStyleHost)InheritanceParent;
diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs
index 78fbe0f2b5..002be685f7 100644
--- a/src/Avalonia.Styling/Styling/IStyle.cs
+++ b/src/Avalonia.Styling/Styling/IStyle.cs
@@ -19,10 +19,10 @@ namespace Avalonia.Styling
/// Attaches the style and any child styles to a control if the style's selector matches.
///
/// The control to attach to.
- /// The element that hosts the style.
+ /// The element that is hosting this style.
///
/// A describing how the style matches the control.
///
- SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host);
+ SelectorMatchResult TryAttach(IStyleable target, object? host);
}
}
diff --git a/src/Avalonia.Styling/Styling/IStyleHost.cs b/src/Avalonia.Styling/Styling/IStyleHost.cs
index 360b40d9a1..05d54f9cdc 100644
--- a/src/Avalonia.Styling/Styling/IStyleHost.cs
+++ b/src/Avalonia.Styling/Styling/IStyleHost.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Styling
/// The property may be lazily initialized, if so this property
/// indicates whether it has been initialized.
///
- bool IsStylesInitialized { get; }
+ bool HasStyles { get; }
///
/// Gets the styles for the element.
diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs
index 00819ef7be..b022dcf398 100644
--- a/src/Avalonia.Styling/Styling/Style.cs
+++ b/src/Avalonia.Styling/Styling/Style.cs
@@ -1,8 +1,4 @@
using System;
-using System.Collections.Generic;
-using Avalonia.Animation;
-using Avalonia.Controls;
-using Avalonia.Metadata;
#nullable enable
@@ -11,13 +7,8 @@ namespace Avalonia.Styling
///
/// Defines a style.
///
- public class Style : AvaloniaObject, IStyle, IResourceProvider
+ public class Style : StyleBase
{
- private IResourceHost? _owner;
- private IResourceDictionary? _resources;
- private List? _setters;
- private List? _animations;
-
///
/// Initializes a new instance of the class.
///
@@ -34,89 +25,11 @@ namespace Avalonia.Styling
Selector = selector(null);
}
- public IResourceHost? Owner
- {
- get => _owner;
- private set
- {
- if (_owner != value)
- {
- _owner = value;
- OwnerChanged?.Invoke(this, EventArgs.Empty);
- }
- }
- }
-
- ///
- /// Gets or sets a dictionary of style resources.
- ///
- public IResourceDictionary Resources
- {
- get => _resources ?? (Resources = new ResourceDictionary());
- set
- {
- value = value ?? throw new ArgumentNullException(nameof(value));
-
- var hadResources = _resources?.HasResources ?? false;
-
- _resources = value;
-
- if (Owner is object)
- {
- _resources.AddOwner(Owner);
-
- if (hadResources || _resources.HasResources)
- {
- Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
- }
- }
- }
- }
-
///
/// Gets or sets the style's selector.
///
public Selector? Selector { get; set; }
- ///
- /// Gets the style's setters.
- ///
- [Content]
- public IList Setters => _setters ??= new List();
-
- ///
- /// Gets the style's animations.
- ///
- public IList Animations => _animations ??= new List();
-
- bool IResourceNode.HasResources => _resources?.Count > 0;
- IReadOnlyList IStyle.Children => Array.Empty();
-
- public event EventHandler? OwnerChanged;
-
- public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
- {
- target = target ?? throw new ArgumentNullException(nameof(target));
-
- var match = Selector is object ? Selector.Match(target) :
- target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
-
- if (match.IsMatch && (_setters is object || _animations is object))
- {
- var instance = new StyleInstance(this, target, _setters, _animations, match.Activator);
- target.StyleApplied(instance);
- instance.Start();
- }
-
- return match.Result;
- }
-
- public bool TryGetResource(object key, out object? result)
- {
- result = null;
- return _resources?.TryGetResource(key, out result) ?? false;
- }
-
///
/// Returns a string representation of the style.
///
@@ -133,28 +46,21 @@ namespace Avalonia.Styling
}
}
- void IResourceProvider.AddOwner(IResourceHost owner)
+ public override SelectorMatchResult TryAttach(IStyleable target, object? host)
{
- owner = owner ?? throw new ArgumentNullException(nameof(owner));
-
- if (Owner != null)
- {
- throw new InvalidOperationException("The Style already has a parent.");
- }
-
- Owner = owner;
- _resources?.AddOwner(owner);
- }
+ target = target ?? throw new ArgumentNullException(nameof(target));
- void IResourceProvider.RemoveOwner(IResourceHost owner)
- {
- owner = owner ?? throw new ArgumentNullException(nameof(owner));
+ var match = Selector is object ? Selector.Match(target) :
+ target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
- if (Owner == owner)
+ if (match.IsMatch && (SettersCore is object || AnimationsCore is object))
{
- Owner = null;
- _resources?.RemoveOwner(owner);
+ var instance = new StyleInstance(this, target, SettersCore, AnimationsCore, match.Activator);
+ target.StyleApplied(instance);
+ instance.Start();
}
+
+ return match.Result;
}
}
}
diff --git a/src/Avalonia.Styling/Styling/StyleBase.cs b/src/Avalonia.Styling/Styling/StyleBase.cs
new file mode 100644
index 0000000000..77251d0daf
--- /dev/null
+++ b/src/Avalonia.Styling/Styling/StyleBase.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Metadata;
+
+#nullable enable
+
+namespace Avalonia.Styling
+{
+ ///
+ /// Base class for and ControlTheme.
+ ///
+ public abstract class StyleBase : AvaloniaObject, IStyle, IResourceProvider
+ {
+ private IResourceHost? _owner;
+
+ public IResourceHost? Owner
+ {
+ get => _owner;
+ private set
+ {
+ if (_owner != value)
+ {
+ _owner = value;
+ OwnerChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets a dictionary of style resources.
+ ///
+ public IResourceDictionary Resources
+ {
+ get => ResourcesCore ?? (Resources = new ResourceDictionary());
+ set
+ {
+ value = value ?? throw new ArgumentNullException(nameof(value));
+
+ var hadResources = ResourcesCore?.HasResources ?? false;
+
+ ResourcesCore = value;
+
+ if (Owner is object)
+ {
+ ResourcesCore.AddOwner(Owner);
+
+ if (hadResources || ResourcesCore.HasResources)
+ {
+ Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Gets the style's setters.
+ ///
+ [Content]
+ public IList Setters => SettersCore ??= new List();
+
+ ///
+ /// Gets the style's animations.
+ ///
+ public IList Animations => AnimationsCore ??= new List();
+
+ ///
+ /// Gets the style's child styles.
+ ///
+ ///
+ /// The default implementation is to return an empty list. This can be overridden in a
+ /// derived class by overriding .
+ ///
+ IReadOnlyList IStyle.Children => GetChildrenCore();
+
+ ///
+ /// Gets a value indicating whether the style has resources.
+ ///
+ ///
+ /// The implementation of this property can be overridden in a derived class by overriding
+ /// .
+ ///
+ bool IResourceNode.HasResources => GetHasResourcesCore();
+
+ ///
+ /// Gets the for the control, without creating a collection
+ /// if one does not already exist.
+ ///
+ protected List? AnimationsCore { get; private set; }
+
+ ///
+ /// Gets the for the control, without creating a resource
+ /// dictionary if one does not already exist.
+ ///
+ protected IResourceDictionary? ResourcesCore { get; private set; }
+
+ ///
+ /// Gets the for the control, without creating a collection
+ /// if one does not already exist.
+ ///
+ protected List? SettersCore { get; private set; }
+
+ public event EventHandler? OwnerChanged;
+
+ public abstract SelectorMatchResult TryAttach(IStyleable target, object? host);
+
+ public bool TryGetResource(object key, out object? result)
+ {
+ result = null;
+ return ResourcesCore?.TryGetResource(key, out result) ?? false;
+ }
+
+ protected void Attach(IStyleable target)
+ {
+ if (SettersCore is object || AnimationsCore is object)
+ {
+ var instance = new StyleInstance(this, target, SettersCore, AnimationsCore);
+ target.StyleApplied(instance);
+ instance.Start();
+ }
+ }
+
+ ///
+ /// When overridden in a derived class, gets the value for .
+ ///
+ protected virtual IReadOnlyList GetChildrenCore() => Array.Empty();
+
+ ///
+ /// When overridden in a derived class, gets the value for .
+ ///
+ protected virtual bool GetHasResourcesCore() => ResourcesCore?.Count > 0;
+
+ void IResourceProvider.AddOwner(IResourceHost owner)
+ {
+ owner = owner ?? throw new ArgumentNullException(nameof(owner));
+
+ if (Owner != null)
+ {
+ throw new InvalidOperationException("The Style already has a parent.");
+ }
+
+ Owner = owner;
+ ResourcesCore?.AddOwner(owner);
+ }
+
+ void IResourceProvider.RemoveOwner(IResourceHost owner)
+ {
+ owner = owner ?? throw new ArgumentNullException(nameof(owner));
+
+ if (Owner == owner)
+ {
+ Owner = null;
+ ResourcesCore?.RemoveOwner(owner);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Styling/Styling/Styler.cs b/src/Avalonia.Styling/Styling/Styler.cs
index 74cf77ea40..15da82f4b2 100644
--- a/src/Avalonia.Styling/Styling/Styler.cs
+++ b/src/Avalonia.Styling/Styling/Styler.cs
@@ -1,18 +1,40 @@
using System;
+using Avalonia.Controls;
#nullable enable
namespace Avalonia.Styling
{
+ ///
+ /// Applies styles to controls based on styles found in themes and styles in the logical tree.
+ ///
public class Styler : IStyler
{
+ ///
+ /// Applies all relevant styles to a control.
+ ///
+ /// The control to be styled.
public void ApplyStyles(IStyleable target)
{
target = target ?? throw new ArgumentNullException(nameof(target));
- if (target is IStyleHost styleHost)
+ // If the control has a themed templated parent then first apply the styles from
+ // the templated parent theme.
+ if (target.TemplatedParent is IThemed themedTemplatedParent)
{
- ApplyStyles(target, styleHost);
+ themedTemplatedParent.Theme?.TryAttach(target, themedTemplatedParent);
+ }
+
+ // If the control itself is themed, then next apply the control theme.
+ if (target is IThemed themed)
+ {
+ themed.Theme?.TryAttach(target, target);
+ }
+
+ // Apply styles from the rest of the tree.
+ if (target is IStyleHost host)
+ {
+ ApplyStyles(target, host);
}
}
@@ -20,12 +42,14 @@ namespace Avalonia.Styling
{
var parent = host.StylingParent;
+ // Later styles have precedence so styles are applied from the root of the tree up
+ // towards the control being styled.
if (parent != null)
{
ApplyStyles(target, parent);
}
- if (host.IsStylesInitialized)
+ if (host.HasStyles)
{
host.Styles.TryAttach(target, host);
}
diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs
index 7c79060930..1eefc8e038 100644
--- a/src/Avalonia.Styling/Styling/Styles.cs
+++ b/src/Avalonia.Styling/Styling/Styles.cs
@@ -29,7 +29,7 @@ namespace Avalonia.Styling
_styles.CollectionChanged += OnCollectionChanged;
}
- public Styles(IResourceHost owner)
+ public Styles(IResourceHost? owner)
: this()
{
Owner = owner;
@@ -110,7 +110,7 @@ namespace Avalonia.Styling
set => _styles[index] = value;
}
- public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
+ public SelectorMatchResult TryAttach(IStyleable target, object? host)
{
_cache ??= new Dictionary?>();
diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
index 2b39263ee9..39332f9d22 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
@@ -80,7 +80,7 @@ namespace Avalonia.Markup.Xaml.Styling
}
}
- public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
+ public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host);
public bool TryGetResource(object key, out object? value)
{
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs
new file mode 100644
index 0000000000..87ef0e3d3c
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs
@@ -0,0 +1,83 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+using Xunit;
+
+#nullable enable
+
+namespace Avalonia.Controls.UnitTests.Primitives
+{
+ public class TemplatedControlTests_Theming
+ {
+ [Fact]
+ public void IThemed_Theme_Returns_Default_Theme_If_Theme_Property_Unset()
+ {
+ var theme = CreateTheme();
+ var target = new ThemedControl(theme);
+
+ Assert.Same(theme, ((IThemed)target).Theme);
+ }
+
+ [Fact]
+ public void IThemed_Theme_Returns_Theme_Property_If_Set()
+ {
+ var theme1 = CreateTheme();
+ var theme2 = CreateTheme();
+ var target = new ThemedControl(theme1) { Theme = theme2 };
+
+ Assert.Same(theme2, ((IThemed)target).Theme);
+ }
+
+ [Fact]
+ public void Theme_Is_Applied_When_Attached_To_Logical_Tree()
+ {
+ using var app = UnitTestApplication.Start(TestServices.RealStyler);
+ var target = new ThemedControl();
+
+ Assert.Null(target.Template);
+
+ var root = new TestRoot(target);
+
+ Assert.NotNull(target.Template);
+ }
+
+ [Fact]
+ public void Theme_Is_Detached_When_Theme_Property_Changed()
+ {
+ using var app = UnitTestApplication.Start(TestServices.RealStyler);
+ var target = new ThemedControl();
+ var root = new TestRoot(target);
+
+ target.Theme = CreateTheme();
+
+ Assert.Null(target.Template);
+ }
+
+ private static ControlTheme CreateTheme()
+ {
+ var template = new FuncControlTemplate((o, n) =>
+ new Border { Name = "PART_Border" });
+
+ return new ControlTheme
+ {
+ Setters =
+ {
+ new Setter(ThemedControl.TemplateProperty, template),
+ }
+ };
+ }
+
+ private class ThemedControl : TemplatedControl
+ {
+ private ControlTheme _defaultTheme;
+
+ public ThemedControl(ControlTheme? defaultTheme = null)
+ {
+ _defaultTheme = defaultTheme ?? CreateTheme();
+ }
+
+ protected override IStyle GetDefaultControlTheme() => _defaultTheme;
+ }
+ }
+}