Browse Source

Merge branch 'master' into fixes/textProcessingFixes

pull/8471/head
Benedikt Stebner 4 years ago
committed by GitHub
parent
commit
a0bacd03d7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      samples/ControlCatalog/Pages/ButtonsPage.xaml
  2. 80
      src/Avalonia.Base/StyledElement.cs
  3. 2
      src/Avalonia.Base/Styling/ChildSelector.cs
  4. 67
      src/Avalonia.Base/Styling/ControlTheme.cs
  5. 2
      src/Avalonia.Base/Styling/DescendentSelector.cs
  6. 2
      src/Avalonia.Base/Styling/IStyle.cs
  7. 7
      src/Avalonia.Base/Styling/IStyleable.cs
  8. 14
      src/Avalonia.Base/Styling/NestingSelector.cs
  9. 2
      src/Avalonia.Base/Styling/NotSelector.cs
  10. 2
      src/Avalonia.Base/Styling/NthChildSelector.cs
  11. 12
      src/Avalonia.Base/Styling/OrSelector.cs
  12. 2
      src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
  13. 31
      src/Avalonia.Base/Styling/Selector.cs
  14. 166
      src/Avalonia.Base/Styling/Style.cs
  15. 124
      src/Avalonia.Base/Styling/StyleBase.cs
  16. 2
      src/Avalonia.Base/Styling/StyleCache.cs
  17. 10
      src/Avalonia.Base/Styling/StyleChildren.cs
  18. 19
      src/Avalonia.Base/Styling/Styler.cs
  19. 7
      src/Avalonia.Base/Styling/Styles.cs
  20. 2
      src/Avalonia.Base/Styling/TemplateSelector.cs
  21. 2
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  22. 22
      src/Avalonia.Controls/Design.cs
  23. 6
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  24. 16
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  25. 21
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  26. 5
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  27. 21
      src/Avalonia.Controls/ItemsControl.cs
  28. 11
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  29. 5
      src/Avalonia.Controls/TreeViewItem.cs
  30. 22
      src/Avalonia.DesignerSupport/DesignWindowLoader.cs
  31. 11
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  32. 2
      src/Avalonia.Themes.Default/SimpleTheme.cs
  33. 2
      src/Avalonia.Themes.Fluent/FluentTheme.cs
  34. 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  35. 39
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs
  36. 27
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs
  37. 2
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  38. 92
      tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs
  39. 25
      tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs
  40. 7
      tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
  41. 418
      tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs
  42. 147
      tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs
  43. 28
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  44. 44
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  45. 29
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  46. 5
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs
  47. 6
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  48. 89
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs
  49. 8
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs

1
samples/ControlCatalog/Pages/ButtonsPage.xaml

@ -90,6 +90,7 @@
</Style> </Style>
</Button.Styles> </Button.Styles>
</Button> </Button>
<Button Classes="accent">Accent</Button>
</StackPanel> </StackPanel>
<StackPanel Orientation="Vertical" <StackPanel Orientation="Vertical"

80
src/Avalonia.Base/StyledElement.cs

@ -12,8 +12,6 @@ using Avalonia.Logging;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling; using Avalonia.Styling;
#nullable enable
namespace Avalonia namespace Avalonia
{ {
/// <summary> /// <summary>
@ -55,7 +53,14 @@ namespace Avalonia
nameof(TemplatedParent), nameof(TemplatedParent),
o => o.TemplatedParent, o => o.TemplatedParent,
(o ,v) => o.TemplatedParent = v); (o ,v) => o.TemplatedParent = v);
/// <summary>
/// Defines the <see cref="Theme"/> property.
/// </summary>
public static readonly StyledProperty<ControlTheme?> ThemeProperty =
AvaloniaProperty.Register<StyledElement, ControlTheme?>(nameof(Theme));
private static readonly ControlTheme s_invalidTheme = new ControlTheme();
private int _initCount; private int _initCount;
private string? _name; private string? _name;
private readonly Classes _classes = new Classes(); private readonly Classes _classes = new Classes();
@ -67,6 +72,8 @@ namespace Avalonia
private List<IStyleInstance>? _appliedStyles; private List<IStyleInstance>? _appliedStyles;
private ITemplatedControl? _templatedParent; private ITemplatedControl? _templatedParent;
private bool _dataContextUpdating; private bool _dataContextUpdating;
private bool _hasPromotedTheme;
private ControlTheme? _implicitTheme;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="StyledElement"/> class. /// Initializes static members of the <see cref="StyledElement"/> class.
@ -230,6 +237,15 @@ namespace Avalonia
internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value);
} }
/// <summary>
/// Gets or sets the theme to be applied to the element.
/// </summary>
public ControlTheme? Theme
{
get => GetValue(ThemeProperty);
set => SetValue(ThemeProperty, value);
}
/// <summary> /// <summary>
/// Gets the styled element's logical children. /// Gets the styled element's logical children.
/// </summary> /// </summary>
@ -302,6 +318,7 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent;
/// <inheritdoc/> /// <inheritdoc/>
public virtual void BeginInit() public virtual void BeginInit()
{ {
@ -341,10 +358,15 @@ namespace Avalonia
} }
finally finally
{ {
_styled = true;
EndBatchUpdate(); EndBatchUpdate();
} }
_styled = true; if (_hasPromotedTheme)
{
_hasPromotedTheme = false;
ClearValue(ThemeProperty);
}
} }
return _styled; return _styled;
@ -475,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) void IStyleable.StyleApplied(IStyleInstance instance)
{ {
instance = instance ?? throw new ArgumentNullException(nameof(instance)); instance = instance ?? throw new ArgumentNullException(nameof(instance));
@ -590,6 +637,30 @@ namespace Avalonia
{ {
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ThemeProperty)
{
// Changing the theme detaches all styles, meaning that if the theme property was
// set via a style, it will get cleared! To work around this, if the value was
// applied at less than local value priority then promote the value to local value
// priority until styling is re-applied.
if (change.Priority > BindingPriority.LocalValue)
{
Theme = change.GetNewValue<ControlTheme?>();
_hasPromotedTheme = true;
}
else if (_hasPromotedTheme && change.Priority == BindingPriority.LocalValue)
{
_hasPromotedTheme = false;
}
InvalidateStyles();
}
}
private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted)
{ {
if (o is StyledElement element) if (o is StyledElement element)
@ -696,6 +767,7 @@ namespace Avalonia
if (_logicalRoot != null) if (_logicalRoot != null)
{ {
_logicalRoot = null; _logicalRoot = null;
_implicitTheme = null;
DetachStyles(); DetachStyles();
OnDetachedFromLogicalTree(e); OnDetachedFromLogicalTree(e);
DetachedFromLogicalTree?.Invoke(this, e); DetachedFromLogicalTree?.Invoke(this, e);
@ -760,7 +832,7 @@ namespace Avalonia
private void DetachStyles() private void DetachStyles()
{ {
if (_appliedStyles is object) if (_appliedStyles?.Count > 0)
{ {
BeginBatchUpdate(); BeginBatchUpdate();

2
src/Avalonia.Base/Styling/ChildSelector.cs

@ -65,6 +65,6 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => null; protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector(); protected override Selector? MovePreviousOrParent() => _parent;
} }
} }

67
src/Avalonia.Base/Styling/ControlTheme.cs

@ -0,0 +1,67 @@
using System;
namespace Avalonia.Styling
{
/// <summary>
/// Defines a switchable theme for a control.
/// </summary>
public class ControlTheme : StyleBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ControlTheme"/> class.
/// </summary>
public ControlTheme() { }
/// <summary>
/// Initializes a new instance of the <see cref="ControlTheme"/> class.
/// </summary>
/// <param name="targetType">The value for <see cref="TargetType"/>.</param>
public ControlTheme(Type targetType) => TargetType = targetType;
/// <summary>
/// Gets or sets the type for which this control theme is intended.
/// </summary>
public Type? TargetType { get; set; }
/// <summary>
/// Gets or sets a control theme that is the basis of the current theme.
/// </summary>
public ControlTheme? BasedOn { get; set; }
public override SelectorMatchResult TryAttach(IStyleable target, object? host)
{
_ = target ?? throw new ArgumentNullException(nameof(target));
if (TargetType is null)
throw new InvalidOperationException("ControlTheme has no TargetType.");
var result = BasedOn?.TryAttach(target, host) ?? SelectorMatchResult.NeverThisType;
if (HasSettersOrAnimations && TargetType.IsAssignableFrom(target.StyleKey))
{
Attach(target, null);
result = SelectorMatchResult.AlwaysThisType;
}
var childResult = TryAttachChildren(target, host);
if (childResult > result)
result = childResult;
return result;
}
public override string ToString()
{
if (TargetType is not null)
return "ControlTheme: " + TargetType.Name;
else
return "ControlTheme";
}
internal override void SetParent(StyleBase? parent)
{
throw new InvalidOperationException("ControlThemes cannot be added as a nested style.");
}
}
}

2
src/Avalonia.Base/Styling/DescendentSelector.cs

@ -70,6 +70,6 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => null; protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector(); protected override Selector? MovePreviousOrParent() => _parent;
} }
} }

2
src/Avalonia.Base/Styling/IStyle.cs

@ -23,6 +23,6 @@ namespace Avalonia.Styling
/// <returns> /// <returns>
/// A <see cref="SelectorMatchResult"/> describing how the style matches the control. /// A <see cref="SelectorMatchResult"/> describing how the style matches the control.
/// </returns> /// </returns>
SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host); SelectorMatchResult TryAttach(IStyleable target, object? host);
} }
} }

7
src/Avalonia.Base/Styling/IStyleable.cs

@ -3,8 +3,6 @@ using System.Collections.Generic;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Metadata; using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
/// <summary> /// <summary>
@ -28,6 +26,11 @@ namespace Avalonia.Styling
/// </summary> /// </summary>
ITemplatedControl? TemplatedParent { get; } ITemplatedControl? TemplatedParent { get; }
/// <summary>
/// Gets the effective theme for the control as used by the syling system.
/// </summary>
ControlTheme? GetEffectiveTheme();
/// <summary> /// <summary>
/// Notifies the element that a style has been applied. /// Notifies the element that a style has been applied.
/// </summary> /// </summary>

14
src/Avalonia.Base/Styling/NestingSelector.cs

@ -15,9 +15,17 @@ namespace Avalonia.Styling
protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{ {
if (parent is Style s && s.Selector is Selector selector) if (parent is Style s && s.Selector is not null)
{ {
return selector.Match(control, (parent as Style)?.Parent, subscribe); return s.Selector.Match(control, s.Parent, subscribe);
}
else if (parent is ControlTheme theme)
{
if (theme.TargetType is null)
throw new InvalidOperationException("ControlTheme has no TargetType.");
return theme.TargetType.IsAssignableFrom(control.StyleKey) ?
SelectorMatch.AlwaysThisType :
SelectorMatch.NeverThisType;
} }
throw new InvalidOperationException( throw new InvalidOperationException(
@ -25,6 +33,6 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => null; protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector() => true; protected override Selector? MovePreviousOrParent() => null;
} }
} }

2
src/Avalonia.Base/Styling/NotSelector.cs

@ -67,6 +67,6 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => _previous; protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _argument.HasValidNestingSelector(); protected override Selector? MovePreviousOrParent() => _previous;
} }
} }

2
src/Avalonia.Base/Styling/NthChildSelector.cs

@ -105,7 +105,7 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => _previous; protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; protected override Selector? MovePreviousOrParent() => _previous;
public override string ToString() public override string ToString()
{ {

12
src/Avalonia.Base/Styling/OrSelector.cs

@ -103,18 +103,12 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => null; protected override Selector? MovePrevious() => null;
protected override Selector? MovePreviousOrParent() => null;
internal override bool HasValidNestingSelector() internal override void ValidateNestingSelector(bool inControlTheme)
{ {
foreach (var selector in _selectors) foreach (var selector in _selectors)
{ selector.ValidateNestingSelector(inControlTheme);
if (!selector.HasValidNestingSelector())
{
return false;
}
}
return true;
} }
private Type? EvaluateTargetType() private Type? EvaluateTargetType()

2
src/Avalonia.Base/Styling/PropertyEqualsSelector.cs

@ -90,7 +90,7 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => _previous; protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; protected override Selector? MovePreviousOrParent() => _previous;
internal static bool Compare(Type propertyType, object? propertyValue, object? value) internal static bool Compare(Type propertyType, object? propertyValue, object? value)
{ {

31
src/Avalonia.Base/Styling/Selector.cs

@ -86,7 +86,36 @@ namespace Avalonia.Styling
/// </summary> /// </summary>
protected abstract Selector? MovePrevious(); protected abstract Selector? MovePrevious();
internal abstract bool HasValidNestingSelector(); /// <summary>
/// Moves to the previous selector or the parent selector.
/// </summary>
protected abstract Selector? MovePreviousOrParent();
internal virtual void ValidateNestingSelector(bool inControlTheme)
{
var s = this;
var templateCount = 0;
do
{
if (inControlTheme)
{
if (!s.InTemplate && s.IsCombinator)
throw new InvalidOperationException(
"ControlTheme style may not directly contain a child or descendent selector.");
if (s is TemplateSelector && templateCount++ > 0)
throw new InvalidOperationException(
"ControlTemplate styles cannot contain multiple template selectors.");
}
var previous = s.MovePreviousOrParent();
if (previous is null && s is not NestingSelector)
throw new InvalidOperationException("Child styles must have a nesting selector.");
s = previous;
} while (s is not null);
}
private static SelectorMatch MatchUntilCombinator( private static SelectorMatch MatchUntilCombinator(
IStyleable control, IStyleable control,

166
src/Avalonia.Base/Styling/Style.cs

@ -1,22 +1,13 @@
using System; using System;
using System.Collections.Generic;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Metadata;
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
/// <summary> /// <summary>
/// Defines a style. /// Defines a style.
/// </summary> /// </summary>
public class Style : AvaloniaObject, IStyle, IResourceProvider public class Style : StyleBase
{ {
private IResourceHost? _owner; private Selector? _selector;
private StyleChildren? _children;
private IResourceDictionary? _resources;
private List<ISetter>? _setters;
private List<IAnimation>? _animations;
private StyleCache? _childCache;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Style"/> class. /// Initializes a new instance of the <see cref="Style"/> class.
@ -35,113 +26,41 @@ namespace Avalonia.Styling
} }
/// <summary> /// <summary>
/// Gets the children of the style. /// Gets or sets the style's selector.
/// </summary>
public IList<IStyle> Children => _children ??= new(this);
/// <summary>
/// Gets the <see cref="StyledElement"/> or Application that hosts the style.
/// </summary> /// </summary>
public IResourceHost? Owner public Selector? Selector
{ {
get => _owner; get => _selector;
private set set => _selector = ValidateSelector(value);
{
if (_owner != value)
{
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
} }
/// <summary> public override SelectorMatchResult TryAttach(IStyleable target, object? host)
/// Gets the parent style if this style is hosted in a <see cref="Style.Children"/> collection.
/// </summary>
public Style? Parent { get; private set; }
/// <summary>
/// Gets or sets a dictionary of style resources.
/// </summary>
public IResourceDictionary Resources
{ {
get => _resources ?? (Resources = new ResourceDictionary()); _ = target ?? throw new ArgumentNullException(nameof(target));
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);
}
}
}
}
/// <summary>
/// Gets or sets the style's selector.
/// </summary>
public Selector? Selector { get; set; }
/// <summary>
/// Gets the style's setters.
/// </summary>
public IList<ISetter> Setters => _setters ??= new List<ISetter>();
/// <summary>
/// Gets the style's animations.
/// </summary>
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
bool IResourceNode.HasResources => _resources?.Count > 0;
IReadOnlyList<IStyle> IStyle.Children => (IReadOnlyList<IStyle>?)_children ?? Array.Empty<IStyle>();
public event EventHandler? OwnerChanged; var result = SelectorMatchResult.NeverThisType;
public void Add(ISetter setter) => Setters.Add(setter); if (HasSettersOrAnimations)
public void Add(IStyle style) => Children.Add(style); {
var match = Selector?.Match(target, Parent, true) ??
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) (target == host ?
{ SelectorMatch.AlwaysThisInstance :
target = target ?? throw new ArgumentNullException(nameof(target)); SelectorMatch.NeverThisInstance);
var match = Selector is object ? Selector.Match(target, Parent) : if (match.IsMatch)
target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; Attach(target, match.Activator);
if (match.IsMatch && (_setters is object || _animations is object)) result = match.Result;
{
var instance = new StyleInstance(this, target, _setters, _animations, match.Activator);
target.StyleApplied(instance);
instance.Start();
} }
var result = match.Result; var childResult = TryAttachChildren(target, host);
if (_children is not null) if (childResult > result)
{ result = childResult;
_childCache ??= new StyleCache();
var childResult = _childCache.TryAttach(_children, target, host);
if (childResult > result)
result = childResult;
}
return result; return result;
} }
public bool TryGetResource(object key, out object? result)
{
result = null;
return _resources?.TryGetResource(key, out result) ?? false;
}
/// <summary> /// <summary>
/// Returns a string representation of the style. /// Returns a string representation of the style.
/// </summary> /// </summary>
@ -158,41 +77,30 @@ namespace Avalonia.Styling
} }
} }
void IResourceProvider.AddOwner(IResourceHost owner) internal override void SetParent(StyleBase? parent)
{ {
owner = owner ?? throw new ArgumentNullException(nameof(owner)); if (parent is Style parentStyle && parentStyle.Selector is not null)
if (Owner != null)
{ {
throw new InvalidOperationException("The Style already has a parent."); if (Selector is null)
} throw new InvalidOperationException("Child styles must have a selector.");
Selector.ValidateNestingSelector(false);
Owner = owner;
_resources?.AddOwner(owner);
}
void IResourceProvider.RemoveOwner(IResourceHost owner)
{
owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner == owner)
{
Owner = null;
_resources?.RemoveOwner(owner);
} }
} else if (parent is ControlTheme)
internal void SetParent(Style? parent)
{
if (parent?.Selector is not null)
{ {
if (Selector is null) if (Selector is null)
throw new InvalidOperationException("Child styles must have a selector."); throw new InvalidOperationException("Child styles must have a selector.");
if (!Selector.HasValidNestingSelector()) Selector.ValidateNestingSelector(true);
throw new InvalidOperationException("Child styles must have a nesting selector.");
} }
Parent = parent; base.SetParent(parent);
}
private static Selector? ValidateSelector(Selector? selector)
{
if (selector is TemplateSelector)
throw new InvalidOperationException(
"Invalid selector: Template selector must be followed by control selector.");
return selector;
} }
} }
} }

124
src/Avalonia.Base/Styling/StyleBase.cs

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Metadata;
using Avalonia.Styling.Activators;
namespace Avalonia.Styling
{
/// <summary>
/// Base class for <see cref="Style"/> and <see cref="ControlTheme"/>.
/// </summary>
public abstract class StyleBase : AvaloniaObject, IStyle, IResourceProvider
{
private IResourceHost? _owner;
private StyleChildren? _children;
private IResourceDictionary? _resources;
private List<ISetter>? _setters;
private List<IAnimation>? _animations;
private StyleCache? _childCache;
public IList<IStyle> Children => _children ??= new(this);
public IResourceHost? Owner
{
get => _owner;
private set
{
if (_owner != value)
{
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
}
public IStyle? Parent { get; private set; }
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);
}
}
}
}
public IList<ISetter> Setters => _setters ??= new List<ISetter>();
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
bool IResourceNode.HasResources => _resources?.Count > 0;
IReadOnlyList<IStyle> IStyle.Children => (IReadOnlyList<IStyle>?)_children ?? Array.Empty<IStyle>();
internal bool HasSettersOrAnimations => _setters?.Count > 0 || _animations?.Count > 0;
public void Add(ISetter setter) => Setters.Add(setter);
public void Add(IStyle style) => Children.Add(style);
public event EventHandler? OwnerChanged;
public abstract SelectorMatchResult TryAttach(IStyleable target, object? host);
public bool TryGetResource(object key, out object? result)
{
result = null;
return _resources?.TryGetResource(key, out result) ?? false;
}
internal void Attach(IStyleable target, IStyleActivator? activator)
{
var instance = new StyleInstance(this, target, _setters, _animations, activator);
target.StyleApplied(instance);
instance.Start();
}
internal SelectorMatchResult TryAttachChildren(IStyleable target, object? host)
{
if (_children is null || _children.Count == 0)
return SelectorMatchResult.NeverThisType;
_childCache ??= new StyleCache();
return _childCache.TryAttach(_children, target, host);
}
internal virtual void SetParent(StyleBase? parent) => Parent = parent;
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;
_resources?.AddOwner(owner);
}
void IResourceProvider.RemoveOwner(IResourceHost owner)
{
owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner == owner)
{
Owner = null;
_resources?.RemoveOwner(owner);
}
}
}
}

2
src/Avalonia.Base/Styling/StyleCache.cs

@ -12,7 +12,7 @@ namespace Avalonia.Styling
/// </remarks> /// </remarks>
internal class StyleCache : Dictionary<Type, List<IStyle>?> internal class StyleCache : Dictionary<Type, List<IStyle>?>
{ {
public SelectorMatchResult TryAttach(IList<IStyle> styles, IStyleable target, IStyleHost? host) public SelectorMatchResult TryAttach(IList<IStyle> styles, IStyleable target, object? host)
{ {
if (TryGetValue(target.StyleKey, out var cached)) if (TryGetValue(target.StyleKey, out var cached))
{ {

10
src/Avalonia.Base/Styling/StyleChildren.cs

@ -5,20 +5,20 @@ namespace Avalonia.Styling
{ {
internal class StyleChildren : Collection<IStyle> internal class StyleChildren : Collection<IStyle>
{ {
private readonly Style _owner; private readonly StyleBase _owner;
public StyleChildren(Style owner) => _owner = owner; public StyleChildren(StyleBase owner) => _owner = owner;
protected override void InsertItem(int index, IStyle item) protected override void InsertItem(int index, IStyle item)
{ {
(item as Style)?.SetParent(_owner); (item as StyleBase)?.SetParent(_owner);
base.InsertItem(index, item); base.InsertItem(index, item);
} }
protected override void RemoveItem(int index) protected override void RemoveItem(int index)
{ {
var item = Items[index]; var item = Items[index];
(item as Style)?.SetParent(null); (item as StyleBase)?.SetParent(null);
if (_owner.Owner is IResourceHost host) if (_owner.Owner is IResourceHost host)
(item as IResourceProvider)?.RemoveOwner(host); (item as IResourceProvider)?.RemoveOwner(host);
base.RemoveItem(index); base.RemoveItem(index);
@ -26,7 +26,7 @@ namespace Avalonia.Styling
protected override void SetItem(int index, IStyle item) protected override void SetItem(int index, IStyle item)
{ {
(item as Style)?.SetParent(_owner); (item as StyleBase)?.SetParent(_owner);
base.SetItem(index, item); base.SetItem(index, item);
if (_owner.Owner is IResourceHost host) if (_owner.Owner is IResourceHost host)
(item as IResourceProvider)?.AddOwner(host); (item as IResourceProvider)?.AddOwner(host);

19
src/Avalonia.Base/Styling/Styler.cs

@ -1,19 +1,24 @@
using System; using System;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
public class Styler : IStyler public class Styler : IStyler
{ {
public void ApplyStyles(IStyleable target) public void ApplyStyles(IStyleable target)
{ {
target = target ?? throw new ArgumentNullException(nameof(target)); _ = target ?? throw new ArgumentNullException(nameof(target));
// Apply the control theme.
target.GetEffectiveTheme()?.TryAttach(target, target);
// If the control has a themed templated parent then apply the styles from the
// templated parent theme.
if (target.TemplatedParent is IStyleable styleableParent)
styleableParent.GetEffectiveTheme()?.TryAttach(target, styleableParent);
// Apply styles from the rest of the tree.
if (target is IStyleHost styleHost) if (target is IStyleHost styleHost)
{
ApplyStyles(target, styleHost); ApplyStyles(target, styleHost);
}
} }
private void ApplyStyles(IStyleable target, IStyleHost host) private void ApplyStyles(IStyleable target, IStyleHost host)
@ -21,14 +26,10 @@ namespace Avalonia.Styling
var parent = host.StylingParent; var parent = host.StylingParent;
if (parent != null) if (parent != null)
{
ApplyStyles(target, parent); ApplyStyles(target, parent);
}
if (host.IsStylesInitialized) if (host.IsStylesInitialized)
{
host.Styles.TryAttach(target, host); host.Styles.TryAttach(target, host);
}
} }
} }
} }

7
src/Avalonia.Base/Styling/Styles.cs

@ -26,6 +26,11 @@ namespace Avalonia.Styling
{ {
_styles.ResetBehavior = ResetBehavior.Remove; _styles.ResetBehavior = ResetBehavior.Remove;
_styles.CollectionChanged += OnCollectionChanged; _styles.CollectionChanged += OnCollectionChanged;
_styles.Validate = i =>
{
if (i is ControlTheme)
throw new InvalidOperationException("ControlThemes cannot be added to a Styles collection.");
};
} }
public Styles(IResourceHost owner) public Styles(IResourceHost owner)
@ -111,7 +116,7 @@ namespace Avalonia.Styling
set => _styles[index] = value; set => _styles[index] = value;
} }
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) public SelectorMatchResult TryAttach(IStyleable target, object? host)
{ {
_cache ??= new StyleCache(); _cache ??= new StyleCache();
return _cache.TryAttach(this, target, host); return _cache.TryAttach(this, target, host);

2
src/Avalonia.Base/Styling/TemplateSelector.cs

@ -49,6 +49,6 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => null; protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector() => _parent?.HasValidNestingSelector() ?? false; protected override Selector? MovePreviousOrParent() => _parent;
} }
} }

2
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -141,7 +141,7 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => _previous; protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; protected override Selector? MovePreviousOrParent() => _previous;
private string BuildSelectorString() private string BuildSelectorString()
{ {

22
src/Avalonia.Controls/Design.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Avalonia.Styling; using Avalonia.Styling;
@ -6,6 +7,8 @@ namespace Avalonia.Controls
{ {
public static class Design public static class Design
{ {
private static Dictionary<object, Control?>? _previewWith;
public static bool IsDesignMode { get; internal set; } public static bool IsDesignMode { get; internal set; }
public static readonly AttachedProperty<double> HeightProperty = AvaloniaProperty public static readonly AttachedProperty<double> HeightProperty = AvaloniaProperty
@ -47,19 +50,30 @@ namespace Avalonia.Controls
return control.GetValue(DataContextProperty); return control.GetValue(DataContextProperty);
} }
public static readonly AttachedProperty<Control> PreviewWithProperty = AvaloniaProperty public static readonly AttachedProperty<Control?> PreviewWithProperty = AvaloniaProperty
.RegisterAttached<AvaloniaObject, Control>("PreviewWith", typeof (Design)); .RegisterAttached<AvaloniaObject, Control?>("PreviewWith", typeof (Design));
public static void SetPreviewWith(AvaloniaObject target, Control control) public static void SetPreviewWith(AvaloniaObject target, Control? control)
{ {
target.SetValue(PreviewWithProperty, control); target.SetValue(PreviewWithProperty, control);
} }
public static Control GetPreviewWith(AvaloniaObject target) public static void SetPreviewWith(ResourceDictionary target, Control? control)
{
_previewWith ??= new();
_previewWith[target] = control;
}
public static Control? GetPreviewWith(AvaloniaObject target)
{ {
return target.GetValue(PreviewWithProperty); return target.GetValue(PreviewWithProperty);
} }
public static Control? GetPreviewWith(ResourceDictionary target)
{
return _previewWith?[target];
}
public static readonly AttachedProperty<IStyle> DesignStyleProperty = AvaloniaProperty public static readonly AttachedProperty<IStyle> DesignStyleProperty = AvaloniaProperty
.RegisterAttached<Control, IStyle>("DesignStyle", typeof(Design)); .RegisterAttached<Control, IStyle>("DesignStyle", typeof(Design));

6
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Styling;
namespace Avalonia.Controls.Generators namespace Avalonia.Controls.Generators
{ {
@ -14,6 +15,11 @@ namespace Avalonia.Controls.Generators
/// </summary> /// </summary>
IEnumerable<ItemContainerInfo> Containers { get; } IEnumerable<ItemContainerInfo> Containers { get; }
/// <summary>
/// Gets or sets the theme to be applied to the items in the control.
/// </summary>
ControlTheme? ItemContainerTheme { get; set; }
/// <summary> /// <summary>
/// Gets or sets the data template used to display the items in the control. /// Gets or sets the data template used to display the items in the control.
/// </summary> /// </summary>

16
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -4,6 +4,7 @@ using System.Linq;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Styling;
namespace Avalonia.Controls.Generators namespace Avalonia.Controls.Generators
{ {
@ -35,6 +36,11 @@ namespace Avalonia.Controls.Generators
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs>? Recycled; public event EventHandler<ItemContainerEventArgs>? Recycled;
/// <summary>
/// Gets or sets the theme to be applied to the items in the control.
/// </summary>
public ControlTheme? ItemContainerTheme { get; set; }
/// <summary> /// <summary>
/// Gets or sets the data template used to display the items in the control. /// Gets or sets the data template used to display the items in the control.
/// </summary> /// </summary>
@ -190,10 +196,18 @@ namespace Avalonia.Controls.Generators
result.SetValue( result.SetValue(
ContentPresenter.ContentTemplateProperty, ContentPresenter.ContentTemplateProperty,
ItemTemplate, ItemTemplate,
BindingPriority.TemplatedParent); BindingPriority.Style);
} }
} }
if (ItemContainerTheme != null)
{
result.SetValue(
StyledElement.ThemeProperty,
ItemContainerTheme,
BindingPriority.TemplatedParent);
}
return result; return result;
} }

21
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@ -44,28 +44,29 @@ namespace Avalonia.Controls.Generators
{ {
var container = item as T; var container = item as T;
if (container != null) if (container is null)
{ {
return container; container = new T();
}
else
{
var result = new T();
if (ContentTemplateProperty != null) if (ContentTemplateProperty != null)
{ {
result.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style);
} }
result.SetValue(ContentProperty, item, BindingPriority.Style); container.SetValue(ContentProperty, item, BindingPriority.Style);
if (!(item is IControl)) if (!(item is IControl))
{ {
result.DataContext = item; container.DataContext = item;
} }
}
return result; if (ItemContainerTheme != null)
{
container.SetValue(StyledElement.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
} }
return container;
} }
/// <inheritdoc/> /// <inheritdoc/>

5
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -71,6 +71,11 @@ namespace Avalonia.Controls.Generators
var template = GetTreeDataTemplate(item, ItemTemplate); var template = GetTreeDataTemplate(item, ItemTemplate);
var result = new T(); var result = new T();
if (ItemContainerTheme != null)
{
result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
}
result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style); result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style);
var itemsSelector = template.ItemsSelector(item); var itemsSelector = template.ItemsSelector(item);

21
src/Avalonia.Controls/ItemsControl.cs

@ -15,6 +15,7 @@ using Avalonia.Input;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Styling;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -36,6 +37,12 @@ namespace Avalonia.Controls
public static readonly DirectProperty<ItemsControl, IEnumerable?> ItemsProperty = public static readonly DirectProperty<ItemsControl, IEnumerable?> ItemsProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable?>(nameof(Items), o => o.Items, (o, v) => o.Items = v); AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable?>(nameof(Items), o => o.Items, (o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="ItemContainerTheme"/> property.
/// </summary>
public static readonly StyledProperty<ControlTheme?> ItemContainerThemeProperty =
AvaloniaProperty.Register<ItemsControl, ControlTheme?>(nameof(ItemContainerTheme));
/// <summary> /// <summary>
/// Defines the <see cref="ItemCount"/> property. /// Defines the <see cref="ItemCount"/> property.
/// </summary> /// </summary>
@ -88,6 +95,7 @@ namespace Avalonia.Controls
{ {
_itemContainerGenerator = CreateItemContainerGenerator(); _itemContainerGenerator = CreateItemContainerGenerator();
_itemContainerGenerator.ItemContainerTheme = ItemContainerTheme;
_itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.ItemTemplate = ItemTemplate;
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
@ -108,6 +116,15 @@ namespace Avalonia.Controls
set { SetAndRaise(ItemsProperty, ref _items, value); } set { SetAndRaise(ItemsProperty, ref _items, value); }
} }
/// <summary>
/// Gets or sets the <see cref="ControlTheme"/> that is applied to the container element generated for each item.
/// </summary>
public ControlTheme? ItemContainerTheme
{
get { return GetValue(ItemContainerThemeProperty); }
set { SetValue(ItemContainerThemeProperty, value); }
}
/// <summary> /// <summary>
/// Gets the number of items in <see cref="Items"/>. /// Gets the number of items in <see cref="Items"/>.
/// </summary> /// </summary>
@ -349,6 +366,10 @@ namespace Avalonia.Controls
{ {
UpdatePseudoClasses(change.GetNewValue<int>()); UpdatePseudoClasses(change.GetNewValue<int>());
} }
else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
_itemContainerGenerator.ItemContainerTheme = change.GetNewValue<ControlTheme?>();
}
} }
/// <summary> /// <summary>

11
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -367,6 +367,17 @@ namespace Avalonia.Controls.Primitives
{ {
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ThemeProperty)
{
foreach (var child in this.GetTemplateChildren())
child.InvalidateStyles();
}
}
/// <summary> /// <summary>
/// Called when the control's template is applied. /// Called when the control's template is applied.
/// </summary> /// </summary>

5
src/Avalonia.Controls/TreeViewItem.cs

@ -121,6 +121,11 @@ namespace Avalonia.Controls
{ {
ItemTemplate = _treeView.ItemTemplate; ItemTemplate = _treeView.ItemTemplate;
} }
if (ItemContainerTheme == null && _treeView?.ItemContainerTheme != null)
{
ItemContainerTheme = _treeView.ItemContainerTheme;
}
} }
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)

22
src/Avalonia.DesignerSupport/DesignWindowLoader.cs

@ -37,6 +37,7 @@ namespace Avalonia.DesignerSupport
var localAsm = assemblyPath != null ? Assembly.LoadFile(Path.GetFullPath(assemblyPath)) : null; var localAsm = assemblyPath != null ? Assembly.LoadFile(Path.GetFullPath(assemblyPath)) : null;
var loaded = loader.Load(stream, localAsm, null, baseUri, true); var loaded = loader.Load(stream, localAsm, null, baseUri, true);
var style = loaded as IStyle; var style = loaded as IStyle;
var resources = loaded as ResourceDictionary;
if (style != null) if (style != null)
{ {
var substitute = Design.GetPreviewWith((AvaloniaObject)style); var substitute = Design.GetPreviewWith((AvaloniaObject)style);
@ -58,6 +59,27 @@ namespace Avalonia.DesignerSupport
} }
}; };
} }
else if (resources != null)
{
var substitute = Design.GetPreviewWith(resources);
if (substitute != null)
{
substitute.Resources.MergedDictionaries.Add(resources);
control = substitute;
}
else
control = new StackPanel
{
Children =
{
new TextBlock {Text = "ResourceDictionaries can't be previewed without Design.PreviewWith. Add"},
new TextBlock {Text = "<Design.PreviewWith>"},
new TextBlock {Text = " <Border Padding=20><!-- YOUR CONTROL FOR PREVIEW HERE --></Border>"},
new TextBlock {Text = "</Design.PreviewWith>"},
new TextBlock {Text = "in your resource dictionary"}
}
};
}
else if (loaded is Application) else if (loaded is Application)
control = new TextBlock {Text = "Application can't be previewed in design view"}; control = new TextBlock {Text = "Application can't be previewed in design view"};
else else

11
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -67,8 +67,15 @@ namespace Avalonia.Diagnostics.ViewModels
var setters = new List<SetterViewModel>(); var setters = new List<SetterViewModel>();
if (styleSource is Style style) if (styleSource is StyleBase style)
{ {
var selector = style switch
{
Style s => s.Selector?.ToString(),
ControlTheme t => t.TargetType?.Name.ToString(),
_ => null,
};
foreach (var setter in style.Setters) foreach (var setter in style.Setters)
{ {
if (setter is Setter regularSetter if (setter is Setter regularSetter
@ -105,7 +112,7 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters)); AppliedStyles.Add(new StyleViewModel(appliedStyle, selector ?? "No selector", setters));
} }
} }

2
src/Avalonia.Themes.Default/SimpleTheme.cs

@ -103,7 +103,7 @@ namespace Avalonia.Themes.Default
void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner);
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) public bool TryGetResource(object key, out object? value)
{ {

2
src/Avalonia.Themes.Fluent/FluentTheme.cs

@ -174,7 +174,7 @@ namespace Avalonia.Themes.Fluent
} }
} }
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) public bool TryGetResource(object key, out object? value)
{ {

1
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -46,6 +46,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
); );
InsertBefore<ContentConvertTransformer>( InsertBefore<ContentConvertTransformer>(
new AvaloniaXamlIlControlThemeTransformer(),
new AvaloniaXamlIlSelectorTransformer(), new AvaloniaXamlIlSelectorTransformer(),
new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
new AvaloniaXamlIlBindingPathParser(), new AvaloniaXamlIlBindingPathParser(),

39
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs

@ -0,0 +1,39 @@
using System.Linq;
using XamlX;
using XamlX.Ast;
using XamlX.Transform;
using XamlX.Transform.Transformers;
using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
class AvaloniaXamlIlControlThemeTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.ControlTheme"))
return node;
// Check if we've already transformed this node.
if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlTargetTypeMetadataNode)
return node;
var targetTypeNode = on.Children.OfType<XamlAstXamlPropertyValueNode>()
.FirstOrDefault(p => p.Property.GetClrProperty().Name == "TargetType") ??
throw new XamlParseException("ControlTheme must have a TargetType.", node);
IXamlType targetType;
if (targetTypeNode.Values[0] is XamlTypeExtensionNode extension)
targetType = extension.Value.GetClrType();
else if (targetTypeNode.Values[0] is XamlAstTextNode text)
targetType = TypeReferenceResolver.ResolveType(context, text.Text, false, text, true).GetClrType();
else
throw new XamlParseException("Could not determine TargetType for ControlTheme.", targetTypeNode);
return new AvaloniaXamlIlTargetTypeMetadataNode(on,
new XamlAstClrTypeReference(targetTypeNode, targetType, false),
AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style);
}
}
}

27
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs

@ -1,19 +1,14 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Data.Core;
using XamlX;
using XamlX.Ast; using XamlX.Ast;
using XamlX.Emit; using XamlX.Emit;
using XamlX.IL; using XamlX.IL;
using XamlX.Transform; using XamlX.Transform;
using XamlX.Transform.Transformers;
using XamlX.TypeSystem; using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{ {
using XamlParseException = XamlX.XamlParseException; using XamlParseException = XamlX.XamlParseException;
using XamlLoadException = XamlX.XamlLoadException;
class AvaloniaXamlIlSetterTransformer : IXamlAstTransformer class AvaloniaXamlIlSetterTransformer : IXamlAstTransformer
{ {
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
@ -22,35 +17,23 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
&& on.Type.GetClrType().FullName == "Avalonia.Styling.Setter")) && on.Type.GetClrType().FullName == "Avalonia.Styling.Setter"))
return node; return node;
var parent = context.ParentNodes().OfType<XamlAstObjectNode>() var targetTypeNode = context.ParentNodes()
.FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style"); .OfType<AvaloniaXamlIlTargetTypeMetadataNode>()
.FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) ??
if (parent == null) throw new XamlParseException("Can not find parent Style Selector or ControlTemplate TargetType", node);
throw new XamlParseException(
"Avalonia.Styling.Setter is only valid inside Avalonia.Styling.Style", node);
var selectorProperty = parent.Children.OfType<XamlAstXamlPropertyValueNode>()
.FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector");
if (selectorProperty == null)
throw new XamlParseException(
"Can not find parent Style Selector", node);
var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode;
if (selector?.TargetType == null)
throw new XamlParseException(
"Can not resolve parent Style Selector type", node);
IXamlType propType = null; IXamlType propType = null;
var property = @on.Children.OfType<XamlAstXamlPropertyValueNode>() var property = @on.Children.OfType<XamlAstXamlPropertyValueNode>()
.FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property"); .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property");
if (property != null) if (property != null)
{ {
var propertyName = property.Values.OfType<XamlAstTextNode>().FirstOrDefault()?.Text; var propertyName = property.Values.OfType<XamlAstTextNode>().FirstOrDefault()?.Text;
if (propertyName == null) if (propertyName == null)
throw new XamlParseException("Setter.Property must be a string", node); throw new XamlParseException("Setter.Property must be a string", node);
var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName,
new XamlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); new XamlAstClrTypeReference(targetTypeNode, targetTypeNode.TargetType.GetClrType(), false), property.Values[0]);
property.Values = new List<IXamlAstValueNode> {avaloniaPropertyNode}; property.Values = new List<IXamlAstValueNode> {avaloniaPropertyNode};
propType = avaloniaPropertyNode.AvaloniaPropertyType; propType = avaloniaPropertyNode.AvaloniaPropertyType;
} }

2
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@ -82,7 +82,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) public bool TryGetResource(object key, out object? value)
{ {

92
tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs

@ -0,0 +1,92 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using Xunit;
namespace Avalonia.Base.UnitTests.Styling
{
public class ControlThemeTests
{
[Fact]
public void ControlTheme_Cannot_Be_Added_To_Styles()
{
var target = new ControlTheme(typeof(Button));
var styles = new Styles();
Assert.Throws<InvalidOperationException>(() => styles.Add(target));
}
[Fact]
public void ControlTheme_Cannot_Be_Added_To_Style_Children()
{
var target = new ControlTheme(typeof(Button));
var style = new Style();
Assert.Throws<InvalidOperationException>(() => style.Children.Add(target));
}
[Fact]
public void ControlTheme_Cannot_Be_Added_To_ControlTheme_Children()
{
var target = new ControlTheme(typeof(Button));
var other = new ControlTheme(typeof(CheckBox));
Assert.Throws<InvalidOperationException>(() => other.Children.Add(target));
}
[Fact]
public void Style_Without_Selector_Cannot_Be_Added_To_Children()
{
var target = new ControlTheme(typeof(Button));
var child = new Style();
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child));
}
[Fact]
public void Style_Without_Nesting_Selector_Cannot_Be_Added_To_Children()
{
var target = new ControlTheme(typeof(Button));
var child = new Style(x => x.OfType<Button>().Template().OfType<Border>());
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child));
}
[Fact]
public void Style_With_NonTemplate_Child_Selector_Cannot_Be_Added_To_Children()
{
var target = new ControlTheme(typeof(Button));
var child = new Style(x => x.Nesting().Child().OfType<Border>());
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child));
}
[Fact]
public void Style_With_NonTemplate_Descendent_Selector_Cannot_Be_Added_To_Children()
{
var target = new ControlTheme(typeof(Button));
var child = new Style(x => x.Nesting().Descendant().OfType<Border>());
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child));
}
[Fact]
public void Style_With_NonTemplate_Child_Template_Selector_Cannot_Be_Added_To_Children()
{
var target = new ControlTheme(typeof(Button));
var child = new Style(x => x.Nesting().Child().Template().OfType<Border>());
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child));
}
[Fact]
public void Style_With_Double_Template_Selector_Cannot_Be_Added_To_Children()
{
var target = new ControlTheme(typeof(Button));
var child = new Style(x => x.Nesting().Template().OfType<ToggleButton>().Template().OfType<Border>());
Assert.Throws<InvalidOperationException>(() => target.Children.Add(child));
}
}
}

25
tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Styling.Activators; using Avalonia.Styling.Activators;
using Xunit; using Xunit;
@ -257,6 +258,30 @@ namespace Avalonia.Base.UnitTests.Styling
parent.Children.Add(child); parent.Children.Add(child);
} }
[Fact]
public void Nesting_Not_Class_Matches()
{
var control = new Control1 { Classes = { "foo" } };
Style nested;
var parent = new Style(x => x.OfType<Control1>())
{
Children =
{
(nested = new Style(x => x.Nesting().Not(y => y.Class("foo")))),
}
};
var match = nested.Selector.Match(control, parent);
Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
var sink = new ActivatorSink(match.Activator);
Assert.False(sink.Active);
control.Classes.Clear();
Assert.True(sink.Active);
}
public class Control1 : Control public class Control1 : Control
{ {
} }

7
tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs

@ -69,6 +69,13 @@ namespace Avalonia.Base.UnitTests.Styling
Assert.Equal("Foo", target.Foo); Assert.Equal("Foo", target.Foo);
} }
[Fact]
public void Should_Throw_For_Selector_With_Trailing_Template_Selector()
{
Assert.Throws<InvalidOperationException>(() =>
new Style(x => x.OfType<Button>().Template()));
}
[Fact] [Fact]
public void Style_With_No_Selector_Should_Not_Apply_To_Other_Control() public void Style_With_No_Selector_Should_Not_Apply_To_Other_Control()
{ {

418
tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs

@ -0,0 +1,418 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
#nullable enable
namespace Avalonia.Base.UnitTests.Styling;
public class StyledElementTests_Theming
{
public class InlineTheme
{
[Fact]
public void Theme_Is_Applied_When_Attached_To_Logical_Tree()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = CreateTarget();
Assert.Null(target.Template);
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 Theme_Is_Applied_To_Derived_Class_When_Attached_To_Logical_Tree()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = new DerivedThemedControl
{
Theme = CreateTheme(),
};
Assert.Null(target.Template);
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 Theme_Is_Detached_When_Theme_Property_Cleared()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = CreateTarget();
var root = CreateRoot(target);
Assert.NotNull(target.Template);
target.Theme = null;
Assert.Null(target.Template);
}
[Fact]
public void Theme_Is_Detached_From_Template_Controls_When_Theme_Property_Cleared()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var theme = new ControlTheme
{
TargetType = typeof(ThemedControl),
Children =
{
new Style(x => x.Nesting().Template().OfType<Canvas>())
{
Setters =
{
new Setter(Panel.BackgroundProperty, Brushes.Red),
}
},
}
};
var target = CreateTarget(theme);
target.Template = new FuncControlTemplate<ThemedControl>((o, n) => new Canvas());
var root = CreateRoot(target);
var canvas = Assert.IsType<Canvas>(target.VisualChild);
Assert.Equal(Brushes.Red, canvas.Background);
target.Theme = null;
Assert.IsType<Canvas>(target.VisualChild);
Assert.Null(canvas.Background);
}
[Fact]
public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = new ThemedControl();
var root = CreateRoot(target);
Assert.Null(target.Template);
target.Theme = CreateTheme();
Assert.Null(target.Template);
root.LayoutManager.ExecuteLayoutPass();
var border = Assert.IsType<Border>(target.VisualChild);
Assert.NotNull(target.Template);
Assert.Equal(Brushes.Red, border.Background);
}
[Fact]
public void BasedOn_Theme_Is_Applied_When_Attached_To_Logical_Tree()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = CreateTarget(CreateDerivedTheme());
Assert.Null(target.Template);
var root = CreateRoot(target);
Assert.NotNull(target.Template);
Assert.Equal(Brushes.Blue, target.BorderBrush);
var border = Assert.IsType<Border>(target.VisualChild);
Assert.Equal(Brushes.Red, border.Background);
Assert.Equal(Brushes.Yellow, border.BorderBrush);
target.Classes.Add("foo");
Assert.Equal(Brushes.Green, border.Background);
Assert.Equal(Brushes.Cyan, border.BorderBrush);
}
private static ThemedControl CreateTarget(ControlTheme? theme = null)
{
return new ThemedControl
{
Theme = theme ?? CreateTheme(),
};
}
private static TestRoot CreateRoot(IControl child)
{
var result = new TestRoot(child);
result.LayoutManager.ExecuteInitialLayoutPass();
return result;
}
}
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());
}
[Fact]
public void Nested_Style_Can_Override_Property_In_Inner_Templated_Control()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = new ThemedControl2
{
Theme = new ControlTheme(typeof(ThemedControl2))
{
Setters =
{
new Setter(
TemplatedControl.TemplateProperty,
new FuncControlTemplate<ThemedControl2>((o, n) => new ThemedControl())),
},
Children =
{
new Style(x => x.Nesting().Template().OfType<ThemedControl>())
{
Setters = { new Setter(TemplatedControl.CornerRadiusProperty, new CornerRadius(7)), }
},
}
},
};
var root = CreateRoot(target);
var inner = Assert.IsType<ThemedControl>(target.VisualChild);
Assert.Equal(new CornerRadius(7), inner.CornerRadius);
}
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]
public void Theme_Is_Applied_When_Attached_To_Logical_Tree()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = CreateTarget();
Assert.Null(target.Theme);
Assert.Null(target.Template);
var root = CreateRoot(target);
Assert.NotNull(target.Theme);
Assert.NotNull(target.Template);
var border = Assert.IsType<Border>(target.VisualChild);
Assert.Equal(border.Background, Brushes.Red);
target.Classes.Add("foo");
Assert.Equal(border.Background, Brushes.Green);
}
[Fact]
public void Theme_Can_Be_Changed_By_Style_Class()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = CreateTarget();
var theme1 = CreateTheme();
var theme2 = new ControlTheme(typeof(ThemedControl));
var root = new TestRoot()
{
Styles =
{
new Style(x => x.OfType<ThemedControl>())
{
Setters = { new Setter(StyledElement.ThemeProperty, theme1) }
},
new Style(x => x.OfType<ThemedControl>().Class("bar"))
{
Setters = { new Setter(StyledElement.ThemeProperty, theme2) }
},
}
};
root.Child = target;
root.LayoutManager.ExecuteInitialLayoutPass();
Assert.Same(theme1, target.Theme);
Assert.NotNull(target.Template);
target.Classes.Add("bar");
Assert.Same(theme2, target.Theme);
Assert.Null(target.Template);
}
[Fact]
public void Theme_Can_Be_Set_To_LocalValue_While_Updating_Due_To_Style_Class()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = CreateTarget();
var theme1 = CreateTheme();
var theme2 = new ControlTheme(typeof(ThemedControl));
var theme3 = new ControlTheme(typeof(ThemedControl));
var root = new TestRoot()
{
Styles =
{
new Style(x => x.OfType<ThemedControl>())
{
Setters = { new Setter(StyledElement.ThemeProperty, theme1) }
},
new Style(x => x.OfType<ThemedControl>().Class("bar"))
{
Setters = { new Setter(StyledElement.ThemeProperty, theme2) }
},
}
};
root.Child = target;
root.LayoutManager.ExecuteInitialLayoutPass();
Assert.Same(theme1, target.Theme);
Assert.NotNull(target.Template);
target.Classes.Add("bar");
// At this point, theme2 has been promoted to a local value internally in StyledElement;
// make sure that setting a new local value here doesn't cause it to be cleared when we
// do a layout pass because StyledElement thinks its clearing the promoted theme.
target.Theme = theme3;
root.LayoutManager.ExecuteLayoutPass();
Assert.Same(target.Theme, theme3);
}
private static ThemedControl CreateTarget()
{
return new ThemedControl();
}
private static TestRoot CreateRoot(IControl child)
{
var result = new TestRoot()
{
Styles =
{
new Style(x => x.OfType<ThemedControl>())
{
Setters = { new Setter(StyledElement.ThemeProperty, CreateTheme()) }
}
}
};
result.Child = child;
result.LayoutManager.ExecuteInitialLayoutPass();
return result;
}
}
private static ControlTheme CreateTheme()
{
var template = new FuncControlTemplate<ThemedControl>((o, n) => new Border());
return new ControlTheme
{
TargetType = typeof(ThemedControl),
Setters =
{
new Setter(TemplatedControl.TemplateProperty, template),
new Setter(TemplatedControl.CornerRadiusProperty, new CornerRadius(5)),
},
Children =
{
new Style(x => x.Nesting().Template().OfType<Border>())
{
Setters = { new Setter(Border.BackgroundProperty, Brushes.Red) }
},
new Style(x => x.Nesting().Class("foo").Template().OfType<Border>())
{
Setters = { new Setter(Border.BackgroundProperty, Brushes.Green) }
},
}
};
}
private static ControlTheme CreateDerivedTheme()
{
return new ControlTheme
{
TargetType = typeof(ThemedControl),
BasedOn = CreateTheme(),
Setters =
{
new Setter(Border.BorderBrushProperty, Brushes.Blue),
},
Children =
{
new Style(x => x.Nesting().Template().OfType<Border>())
{
Setters = { new Setter(Border.BorderBrushProperty, Brushes.Yellow) }
},
new Style(x => x.Nesting().Class("foo").Template().OfType<Border>())
{
Setters = { new Setter(Border.BorderBrushProperty, Brushes.Cyan) }
},
}
};
}
private class ThemedControl : TemplatedControl
{
public IVisual? VisualChild => VisualChildren?.SingleOrDefault();
}
private class ThemedControl2 : TemplatedControl
{
public IVisual? VisualChild => VisualChildren?.SingleOrDefault();
}
private class DerivedThemedControl : ThemedControl
{
}
}

147
tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs

@ -0,0 +1,147 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Media;
using Avalonia.Styling;
using BenchmarkDotNet.Attributes;
#nullable enable
namespace Avalonia.Benchmarks.Styling
{
[MemoryDiagnoser]
public class ControlTheme_Apply
{
private ControlTheme _theme;
private ControlTheme _otherTheme;
private List<Style> _styles = new();
public ControlTheme_Apply()
{
RuntimeHelpers.RunClassConstructor(typeof(TestControl).TypeHandle);
_theme = CreateControlTheme(Brushes.Red);
_otherTheme = CreateControlTheme(Brushes.Orange);
for (var i = 0; i < 100; ++i)
{
_styles.Add(new Style(x => x.OfType<TestControl>())
{
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Yellow) }
});
}
}
[Benchmark]
public void Apply_Control_Theme()
{
var target = new TestControl();
target.BeginBatchUpdate();
_theme.TryAttach(target, null);
target.ApplyTemplate();
_theme.TryAttach(target.VisualChild, null);
target.EndBatchUpdate();
}
[Benchmark]
public void Apply_Remove_Control_Theme()
{
var target = new TestControl();
target.BeginBatchUpdate();
_theme.TryAttach(target, null);
target.ApplyTemplate();
_theme.TryAttach(target.VisualChild, null);
target.EndBatchUpdate();
// Switching to another theme will cause the current theme to be removed but won't
// immediately apply the new theme, so for the benefit of the benchmark it has the
// effect of simply removing the theme.
target.Theme = _otherTheme;
}
[Benchmark]
public void Apply_Control_Theme_With_Styles()
{
var target = new TestControl();
target.BeginBatchUpdate();
_theme.TryAttach(target, null);
target.ApplyTemplate();
_theme.TryAttach(target.VisualChild, null);
foreach (var style in _styles)
style.TryAttach(target, null);
target.EndBatchUpdate();
}
[Benchmark]
public void Apply_Remove_Control_Theme_With_Styles()
{
var target = new TestControl();
target.BeginBatchUpdate();
_theme.TryAttach(target, null);
target.ApplyTemplate();
_theme.TryAttach(target.VisualChild, null);
foreach (var style in _styles)
style.TryAttach(target, null);
target.EndBatchUpdate();
// Switching to another theme will cause the current theme to be removed but won't
// immediately apply the new theme, so for the benefit of the benchmark it has the
// effect of simply removing the theme.
target.Theme = _otherTheme;
}
private static ControlTheme CreateControlTheme(IBrush background)
{
return new ControlTheme(typeof(TestControl))
{
Setters =
{
new Setter(TestControl.BackgroundProperty, Brushes.Transparent),
new Setter(TestControl.TemplateProperty, new FuncControlTemplate<TestControl>((_, x) =>
new Border())),
},
Children =
{
new Style(x => x.Nesting().Template().OfType<Border>())
{
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Red), }
},
new Style(x => x.Nesting().Class(":pointerover").Template().OfType<Border>())
{
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Green), }
},
new Style(x => x.Nesting().Class(":pressed").Template().OfType<Border>())
{
Setters = { new Setter(TestControl.BackgroundProperty, Brushes.Blue), }
},
}
};
}
private class TestControl : TemplatedControl
{
public IStyleable VisualChild => (IStyleable)VisualChildren[0];
}
private class TestClass2 : Control
{
}
}
}

28
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -1,15 +1,14 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.VisualTree; using Avalonia.Styling;
using Xunit;
using System.Collections.ObjectModel;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Input; using Xunit;
using System.Collections.Generic;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
{ {
@ -62,6 +61,25 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(container.TemplatedParent); Assert.Null(container.TemplatedParent);
} }
[Fact]
public void Container_Should_Have_Theme_Set_To_ItemContainerTheme()
{
var theme = new ControlTheme();
var target = new ItemsControl
{
ItemContainerTheme = theme,
};
target.Template = GetTemplate();
target.Items = new[] { "Foo" };
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var container = (ContentPresenter)target.Presenter.Panel.Children[0];
Assert.Same(container.Theme, theme);
}
[Fact] [Fact]
public void Container_Should_Have_LogicalParent_Set_To_ItemsControl() public void Container_Should_Have_LogicalParent_Set_To_ItemsControl()
{ {

44
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -94,6 +94,50 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void Container_Should_Have_Theme_Set_To_ItemContainerTheme()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var items = new[] { "Foo", "Bar", "Baz " };
var theme = new ControlTheme();
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
ItemContainerTheme = theme,
};
Prepare(target);
var container = (ListBoxItem)target.Presenter.Panel.Children[0];
Assert.Same(container.Theme, theme);
}
}
[Fact]
public void Inline_Item_Should_Have_Theme_Set_To_ItemContainerTheme()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var items = new[] { "Foo", "Bar", "Baz " };
var theme = new ControlTheme();
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = new[] { new ListBoxItem() },
ItemContainerTheme = theme,
};
Prepare(target);
var container = (ListBoxItem)target.Presenter.Panel.Children[0];
Assert.Same(container.Theme, theme);
}
}
[Fact] [Fact]
public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items() public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items()
{ {

29
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -72,6 +72,35 @@ namespace Avalonia.Controls.UnitTests
Assert.All(items, x => Assert.IsType<Canvas>(x.HeaderPresenter.Child)); Assert.All(items, x => Assert.IsType<Canvas>(x.HeaderPresenter.Child));
} }
[Fact]
public void Items_Should_Be_Created_Using_ItemConatinerTheme_If_Present()
{
TreeView target;
var theme = new ControlTheme();
var root = new TestRoot
{
Child = target = new TreeView
{
Template = CreateTreeViewTemplate(),
Items = CreateTestTreeData(),
ItemContainerTheme = theme,
ItemTemplate = new FuncTreeDataTemplate<Node>(
(_, __) => new Canvas(),
x => x.Children),
}
};
ApplyTemplates(target);
var items = target.ItemContainerGenerator.Index.Containers
.OfType<TreeViewItem>()
.ToList();
Assert.Equal(5, items.Count);
Assert.All(items, x => Assert.Same(theme, x.ItemContainerTheme));
}
[Fact] [Fact]
public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers() public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers()
{ {

5
tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs

@ -137,6 +137,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
get { throw new NotImplementedException(); } get { throw new NotImplementedException(); }
} }
public ControlTheme GetEffectiveTheme()
{
throw new NotImplementedException();
}
public void DetachStyles() public void DetachStyles()
{ {
throw new NotImplementedException(); throw new NotImplementedException();

6
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@ -845,7 +845,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal("bar", border.Tag); Assert.Equal("bar", border.Tag);
var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0]; var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0];
Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources); Assert.Contains("bar", resourceProvider.RequestedResources);
Assert.DoesNotContain("foo", resourceProvider.RequestedResources);
} }
[Fact] [Fact]
@ -883,7 +884,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal("bar", border.Tag); Assert.Equal("bar", border.Tag);
var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0]; 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) private IDisposable StyledWindow(params (string, string)[] assets)

89
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs

@ -0,0 +1,89 @@
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
public class ControlThemeTests : XamlTestBase
{
[Fact]
public void ControlTheme_Can_Be_StaticResource()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = $@"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:u='using:Avalonia.Markup.Xaml.UnitTests.Xaml'>
<Window.Resources>
{ControlThemeXaml}
</Window.Resources>
<u:TestTemplatedControl Theme='{{StaticResource MyTheme}}'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = Assert.IsType<TestTemplatedControl>(window.Content);
window.LayoutManager.ExecuteInitialLayoutPass();
Assert.NotNull(button.Template);
var child = Assert.Single(button.GetVisualChildren());
var border = Assert.IsType<Border>(child);
Assert.Equal(Brushes.Red, border.Background);
}
}
[Fact]
public void ControlTheme_Can_Be_Set_In_Style()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = $@"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:u='using:Avalonia.Markup.Xaml.UnitTests.Xaml'>
<Window.Resources>
{ControlThemeXaml}
</Window.Resources>
<Window.Styles>
<Style Selector='u|TestTemplatedControl'>
<Setter Property='Theme' Value='{{StaticResource MyTheme}}'/>
</Style>
</Window.Styles>
<u:TestTemplatedControl/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = Assert.IsType<TestTemplatedControl>(window.Content);
window.LayoutManager.ExecuteInitialLayoutPass();
Assert.NotNull(button.Template);
var child = Assert.Single(button.GetVisualChildren());
var border = Assert.IsType<Border>(child);
Assert.Equal(Brushes.Red, border.Background);
}
}
private const string ControlThemeXaml = @"
<ControlTheme x:Key='MyTheme' TargetType='u:TestTemplatedControl'>
<Setter Property='Template'>
<ControlTemplate>
<Border/>
</ControlTemplate>
</Setter>
<Style Selector='^ /template/ Border'>
<Setter Property='Background' Value='Red'/>
</Style>
</ControlTheme>";
}
}

8
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs

@ -0,0 +1,8 @@
using Avalonia.Controls.Primitives;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
public class TestTemplatedControl : TemplatedControl
{
}
}
Loading…
Cancel
Save