Browse Source

Merge branch 'master' into refactor/avaloniapropertystore

refactor/avaloniapropertystore
Steven Kirk 4 years ago
committed by GitHub
parent
commit
122bc0cfbb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.sln
  2. 1
      samples/ControlCatalog/Pages/ButtonsPage.xaml
  3. 80
      src/Avalonia.Base/StyledElement.cs
  4. 2
      src/Avalonia.Base/Styling/ChildSelector.cs
  5. 67
      src/Avalonia.Base/Styling/ControlTheme.cs
  6. 2
      src/Avalonia.Base/Styling/DescendentSelector.cs
  7. 2
      src/Avalonia.Base/Styling/IStyle.cs
  8. 7
      src/Avalonia.Base/Styling/IStyleable.cs
  9. 14
      src/Avalonia.Base/Styling/NestingSelector.cs
  10. 2
      src/Avalonia.Base/Styling/NotSelector.cs
  11. 2
      src/Avalonia.Base/Styling/NthChildSelector.cs
  12. 12
      src/Avalonia.Base/Styling/OrSelector.cs
  13. 2
      src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
  14. 31
      src/Avalonia.Base/Styling/Selector.cs
  15. 166
      src/Avalonia.Base/Styling/Style.cs
  16. 124
      src/Avalonia.Base/Styling/StyleBase.cs
  17. 2
      src/Avalonia.Base/Styling/StyleCache.cs
  18. 10
      src/Avalonia.Base/Styling/StyleChildren.cs
  19. 19
      src/Avalonia.Base/Styling/Styler.cs
  20. 7
      src/Avalonia.Base/Styling/Styles.cs
  21. 2
      src/Avalonia.Base/Styling/TemplateSelector.cs
  22. 2
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  23. 22
      src/Avalonia.Controls/Design.cs
  24. 6
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  25. 16
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  26. 21
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  27. 5
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  28. 61
      src/Avalonia.Controls/ItemsControl.cs
  29. 10
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  30. 11
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  31. 5
      src/Avalonia.Controls/TreeViewItem.cs
  32. 22
      src/Avalonia.DesignerSupport/DesignWindowLoader.cs
  33. 98
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json
  34. 11
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  35. 3
      src/Avalonia.FreeDesktop/DBusHelper.cs
  36. 33
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  37. 2
      src/Avalonia.Themes.Default/SimpleTheme.cs
  38. 2
      src/Avalonia.Themes.Fluent/FluentTheme.cs
  39. 64
      src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs
  40. 2
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  41. 30
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  42. 8
      src/Avalonia.X11/X11Platform.cs
  43. 9
      src/Avalonia.X11/X11Window.cs
  44. 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  45. 39
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs
  46. 27
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs
  47. 2
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  48. 92
      tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs
  49. 25
      tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs
  50. 7
      tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
  51. 418
      tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs
  52. 147
      tests/Avalonia.Benchmarks/Styling/ControlTheme_Apply.cs
  53. 28
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  54. 44
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  55. 63
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  56. 29
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  57. 5
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs
  58. 6
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  59. 89
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs
  60. 8
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs

1
Avalonia.sln

@ -559,6 +559,7 @@ Global
{2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

1
samples/ControlCatalog/Pages/ButtonsPage.xaml

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

80
src/Avalonia.Base/StyledElement.cs

@ -12,8 +12,6 @@ using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Styling;
#nullable enable
namespace Avalonia
{
/// <summary>
@ -55,7 +53,14 @@ namespace Avalonia
nameof(TemplatedParent),
o => o.TemplatedParent,
(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 string? _name;
private readonly Classes _classes = new Classes();
@ -67,6 +72,8 @@ namespace Avalonia
private List<IStyleInstance>? _appliedStyles;
private ITemplatedControl? _templatedParent;
private bool _dataContextUpdating;
private bool _hasPromotedTheme;
private ControlTheme? _implicitTheme;
/// <summary>
/// Initializes static members of the <see cref="StyledElement"/> class.
@ -230,6 +237,15 @@ namespace Avalonia
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>
/// Gets the styled element's logical children.
/// </summary>
@ -302,6 +318,7 @@ namespace Avalonia
/// <inheritdoc/>
IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent;
/// <inheritdoc/>
public virtual void BeginInit()
{
@ -341,10 +358,15 @@ namespace Avalonia
}
finally
{
_styled = true;
EndBatchUpdate();
}
_styled = true;
if (_hasPromotedTheme)
{
_hasPromotedTheme = false;
ClearValue(ThemeProperty);
}
}
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)
{
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)
{
if (o is StyledElement element)
@ -696,6 +767,7 @@ namespace Avalonia
if (_logicalRoot != null)
{
_logicalRoot = null;
_implicitTheme = null;
DetachStyles();
OnDetachedFromLogicalTree(e);
DetachedFromLogicalTree?.Invoke(this, e);
@ -760,7 +832,7 @@ namespace Avalonia
private void DetachStyles()
{
if (_appliedStyles is object)
if (_appliedStyles?.Count > 0)
{
BeginBatchUpdate();

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

@ -65,6 +65,6 @@ namespace Avalonia.Styling
}
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;
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>
/// A <see cref="SelectorMatchResult"/> describing how the style matches the control.
/// </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.Metadata;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
@ -28,6 +26,11 @@ namespace Avalonia.Styling
/// </summary>
ITemplatedControl? TemplatedParent { get; }
/// <summary>
/// Gets the effective theme for the control as used by the syling system.
/// </summary>
ControlTheme? GetEffectiveTheme();
/// <summary>
/// Notifies the element that a style has been applied.
/// </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)
{
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(
@ -25,6 +33,6 @@ namespace Avalonia.Styling
}
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;
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;
internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
protected override Selector? MovePreviousOrParent() => _previous;
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? MovePreviousOrParent() => null;
internal override bool HasValidNestingSelector()
internal override void ValidateNestingSelector(bool inControlTheme)
{
foreach (var selector in _selectors)
{
if (!selector.HasValidNestingSelector())
{
return false;
}
}
return true;
selector.ValidateNestingSelector(inControlTheme);
}
private Type? EvaluateTargetType()

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

@ -90,7 +90,7 @@ namespace Avalonia.Styling
}
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)
{

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

@ -86,7 +86,36 @@ namespace Avalonia.Styling
/// </summary>
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(
IStyleable control,

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

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

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

@ -5,20 +5,20 @@ namespace Avalonia.Styling
{
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)
{
(item as Style)?.SetParent(_owner);
(item as StyleBase)?.SetParent(_owner);
base.InsertItem(index, item);
}
protected override void RemoveItem(int index)
{
var item = Items[index];
(item as Style)?.SetParent(null);
(item as StyleBase)?.SetParent(null);
if (_owner.Owner is IResourceHost host)
(item as IResourceProvider)?.RemoveOwner(host);
base.RemoveItem(index);
@ -26,7 +26,7 @@ namespace Avalonia.Styling
protected override void SetItem(int index, IStyle item)
{
(item as Style)?.SetParent(_owner);
(item as StyleBase)?.SetParent(_owner);
base.SetItem(index, item);
if (_owner.Owner is IResourceHost host)
(item as IResourceProvider)?.AddOwner(host);

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

@ -1,19 +1,24 @@
using System;
#nullable enable
namespace Avalonia.Styling
{
public class Styler : IStyler
{
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)
{
ApplyStyles(target, styleHost);
}
}
private void ApplyStyles(IStyleable target, IStyleHost host)
@ -21,14 +26,10 @@ namespace Avalonia.Styling
var parent = host.StylingParent;
if (parent != null)
{
ApplyStyles(target, parent);
}
if (host.IsStylesInitialized)
{
host.Styles.TryAttach(target, host);
}
}
}
}

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

@ -26,6 +26,11 @@ namespace Avalonia.Styling
{
_styles.ResetBehavior = ResetBehavior.Remove;
_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)
@ -111,7 +116,7 @@ namespace Avalonia.Styling
set => _styles[index] = value;
}
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
public SelectorMatchResult TryAttach(IStyleable target, object? host)
{
_cache ??= new StyleCache();
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;
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;
internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
protected override Selector? MovePreviousOrParent() => _previous;
private string BuildSelectorString()
{

22
src/Avalonia.Controls/Design.cs

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

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

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Templates;
using Avalonia.Styling;
namespace Avalonia.Controls.Generators
{
@ -14,6 +15,11 @@ namespace Avalonia.Controls.Generators
/// </summary>
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>
/// Gets or sets the data template used to display the items in the control.
/// </summary>

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

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

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

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

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

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

61
src/Avalonia.Controls/ItemsControl.cs

@ -15,6 +15,7 @@ using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.VisualTree;
using Avalonia.Styling;
namespace Avalonia.Controls
{
@ -36,6 +37,12 @@ namespace Avalonia.Controls
public static readonly DirectProperty<ItemsControl, IEnumerable?> ItemsProperty =
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>
/// Defines the <see cref="ItemCount"/> property.
/// </summary>
@ -88,6 +95,7 @@ namespace Avalonia.Controls
{
_itemContainerGenerator = CreateItemContainerGenerator();
_itemContainerGenerator.ItemContainerTheme = ItemContainerTheme;
_itemContainerGenerator.ItemTemplate = ItemTemplate;
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
@ -108,6 +116,15 @@ namespace Avalonia.Controls
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>
/// Gets the number of items in <see cref="Items"/>.
/// </summary>
@ -349,6 +366,10 @@ namespace Avalonia.Controls
{
UpdatePseudoClasses(change.GetNewValue<int>());
}
else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
_itemContainerGenerator.ItemContainerTheme = change.GetNewValue<ControlTheme?>();
}
}
/// <summary>
@ -502,25 +523,47 @@ namespace Avalonia.Controls
IInputElement? from,
bool wrap)
{
IInputElement? result;
var c = from;
var current = from;
do
for (;;)
{
result = container.GetControl(direction, c, wrap);
var result = container.GetControl(direction, current, wrap);
if (result != null &&
result.Focusable &&
if (result is null)
{
return null;
}
if (result.Focusable &&
result.IsEffectivelyEnabled &&
result.IsEffectivelyVisible)
{
return result;
}
c = result;
} while (c != null && c != from && direction != NavigationDirection.First && direction != NavigationDirection.Last);
current = result;
if (current == from)
{
return null;
}
return null;
switch (direction)
{
//We did not find an enabled first item. Move downwards until we find one.
case NavigationDirection.First:
direction = NavigationDirection.Down;
from = result;
break;
//We did not find an enabled last item. Move upwards until we find one.
case NavigationDirection.Last:
direction = NavigationDirection.Up;
from = result;
break;
}
}
}
private void PresenterChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)

10
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -53,7 +53,7 @@ namespace Avalonia.Controls.Platform
Menu.PointerPressed += PointerPressed;
Menu.PointerReleased += PointerReleased;
Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, MenuOpened);
Menu.AddHandler(MenuBase.MenuOpenedEvent, MenuOpened);
Menu.AddHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
Menu.AddHandler(MenuItem.PointerExitedItemEvent, PointerExited);
Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved);
@ -89,7 +89,7 @@ namespace Avalonia.Controls.Platform
Menu.PointerPressed -= PointerPressed;
Menu.PointerReleased -= PointerReleased;
Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, MenuOpened);
Menu.RemoveHandler(MenuBase.MenuOpenedEvent, MenuOpened);
Menu.RemoveHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
Menu.RemoveHandler(MenuItem.PointerExitedItemEvent, PointerExited);
Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved);
@ -175,7 +175,11 @@ namespace Avalonia.Controls.Platform
case Key.Left:
{
if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen)
if (item is { IsSubMenuOpen: true, SelectedItem: null })
{
item.Close();
}
else if (item?.Parent is IMenuItem { IsTopLevel: false, IsSubMenuOpen: true } parent)
{
parent.Close();
parent.Focus();

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>
/// Called when the control's template is applied.
/// </summary>

5
src/Avalonia.Controls/TreeViewItem.cs

@ -121,6 +121,11 @@ namespace Avalonia.Controls
{
ItemTemplate = _treeView.ItemTemplate;
}
if (ItemContainerTheme == null && _treeView?.ItemContainerTheme != null)
{
ItemContainerTheme = _treeView.ItemContainerTheme;
}
}
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 loaded = loader.Load(stream, localAsm, null, baseUri, true);
var style = loaded as IStyle;
var resources = loaded as ResourceDictionary;
if (style != null)
{
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)
control = new TextBlock {Text = "Application can't be previewed in design view"};
else

98
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json

@ -10,6 +10,55 @@
"integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==",
"dev": true
},
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true,
"requires": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"dev": true
},
"@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true
},
"@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@types/eslint": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz",
@ -2136,6 +2185,12 @@
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@ -2153,6 +2208,16 @@
"source-map-js": "^1.0.1"
}
},
"source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"string-template": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
@ -2208,13 +2273,14 @@
"dev": true
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"dev": true,
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
@ -2223,30 +2289,6 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
},
"source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
}
}
},

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

@ -67,8 +67,15 @@ namespace Avalonia.Diagnostics.ViewModels
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)
{
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));
}
}

3
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -24,8 +24,7 @@ namespace Avalonia.FreeDesktop
if (_ctx is not null)
_ctx?.Post(d, state);
else
lock (_lock)
d(state);
d(state);
}
}

33
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -15,27 +15,26 @@ namespace Avalonia.FreeDesktop
{
internal class DBusSystemDialog : BclStorageProvider
{
private static readonly Lazy<IFileChooser?> s_fileChooser = new(() =>
private static readonly Lazy<IFileChooser?> s_fileChooser = new(() => DBusHelper.Connection?
.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"));
internal static async Task<IStorageProvider?> TryCreate(IPlatformHandle handle)
{
var fileChooser = DBusHelper.Connection?.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
if (fileChooser is null)
return null;
try
if (handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser)
{
_ = fileChooser.GetVersionAsync();
return fileChooser;
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}");
return null;
try
{
await fileChooser.GetVersionAsync();
return new DBusSystemDialog(fileChooser, handle);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}");
return null;
}
}
});
internal static DBusSystemDialog? TryCreate(IPlatformHandle handle)
{
return handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser
? new DBusSystemDialog(fileChooser, handle) : null;
return null;
}
private readonly IFileChooser _fileChooser;

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);
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)
{

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)
{

64
src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs

@ -0,0 +1,64 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
namespace Avalonia.X11.NativeDialogs;
internal class CompositeStorageProvider : IStorageProvider
{
private readonly IEnumerable<Func<Task<IStorageProvider?>>> _factories;
public CompositeStorageProvider(IEnumerable<Func<Task<IStorageProvider?>>> factories)
{
_factories = factories;
}
public bool CanOpen => true;
public bool CanSave => true;
public bool CanPickFolder => true;
private async Task<IStorageProvider> EnsureStorageProvider()
{
foreach (var factory in _factories)
{
var provider = await factory();
if (provider is not null)
{
return provider;
}
}
throw new InvalidOperationException("Neither DBus nor GTK are available on the system");
}
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.OpenFilePickerAsync(options).ConfigureAwait(false);
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.SaveFilePickerAsync(options).ConfigureAwait(false);
}
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.OpenFolderPickerAsync(options).ConfigureAwait(false);
}
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.OpenFileBookmarkAsync(bookmark).ConfigureAwait(false);
}
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.OpenFolderBookmarkAsync(bookmark).ConfigureAwait(false);
}
}

2
src/Avalonia.X11/NativeDialogs/Gtk.cs

@ -264,8 +264,6 @@ namespace Avalonia.X11.NativeDialogs
public static Task<bool> StartGtk()
{
return StartGtkCore();
lock (s_startGtkLock)
return s_startGtkTask ??= StartGtkCore();
}
private static void GtkThread(TaskCompletionSource<bool> tcs)

30
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -17,10 +17,10 @@ namespace Avalonia.X11.NativeDialogs
{
internal class GtkSystemDialog : BclStorageProvider
{
private Task<bool>? _initialized;
private static Task<bool>? _initialized;
private readonly X11Window _window;
public GtkSystemDialog(X11Window window)
private GtkSystemDialog(X11Window window)
{
_window = window;
}
@ -31,10 +31,15 @@ namespace Avalonia.X11.NativeDialogs
public override bool CanPickFolder => true;
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
internal static async Task<IStorageProvider?> TryCreate(X11Window window)
{
await EnsureInitialized();
_initialized ??= StartGtk();
return await _initialized ? new GtkSystemDialog(window) : null;
}
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Open,
@ -46,8 +51,6 @@ namespace Avalonia.X11.NativeDialogs
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.SelectFolder,
@ -59,8 +62,6 @@ namespace Avalonia.X11.NativeDialogs
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
@ -225,19 +226,6 @@ namespace Avalonia.X11.NativeDialogs
return tcs.Task;
}
private async Task EnsureInitialized()
{
if (_initialized == null)
{
_initialized = StartGtk();
}
if (!(await _initialized))
{
throw new Exception("Unable to initialize GTK on separate thread");
}
}
private static void UpdateParent(IntPtr chooser, IWindowImpl parentWindow)
{
var xid = parentWindow.Handle.Handle;

8
src/Avalonia.X11/X11Platform.cs

@ -216,16 +216,16 @@ namespace Avalonia
public bool OverlayPopups { get; set; }
/// <summary>
/// Enables native file dialogs as well as global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc).
/// Enables global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc).
/// The default value is true.
/// </summary>
public bool UseDBusMenu { get; set; } = true;
/// <summary>
/// Enables GTK file picker instead of default FreeDesktop.
/// The default value is true. And FreeDesktop file picker is used instead if available.
/// Enables DBus file picker instead of GTK.
/// The default value is true.
/// </summary>
public bool UseGtkFilePicker { get; set; } = false;
public bool UseDBusFilePicker { get; set; } = true;
/// <summary>
/// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.

9
src/Avalonia.X11/X11Window.cs

@ -22,6 +22,7 @@ using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.X11.Glx;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.XLib;
// ReSharper disable IdentifierTypo
// ReSharper disable StringLiteralTypo
@ -215,9 +216,11 @@ namespace Avalonia.X11
_x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1);
}
var canUseFreeDekstopPicker = !platform.Options.UseGtkFilePicker && platform.Options.UseDBusMenu;
StorageProvider = canUseFreeDekstopPicker && DBusSystemDialog.TryCreate(Handle) is {} dBusStorage
? dBusStorage : new NativeDialogs.GtkSystemDialog(this);
StorageProvider = new CompositeStorageProvider(new Func<Task<IStorageProvider>>[]
{
() => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreate(Handle) : Task.FromResult<IStorageProvider>(null),
() => GtkSystemDialog.TryCreate(this),
});
}
class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo

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

@ -46,6 +46,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
);
InsertBefore<ContentConvertTransformer>(
new AvaloniaXamlIlControlThemeTransformer(),
new AvaloniaXamlIlSelectorTransformer(),
new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
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.Linq;
using Avalonia.Data.Core;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
using XamlX.Transform.Transformers;
using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
using XamlParseException = XamlX.XamlParseException;
using XamlLoadException = XamlX.XamlLoadException;
class AvaloniaXamlIlSetterTransformer : IXamlAstTransformer
{
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"))
return node;
var parent = context.ParentNodes().OfType<XamlAstObjectNode>()
.FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style");
if (parent == null)
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);
var targetTypeNode = context.ParentNodes()
.OfType<AvaloniaXamlIlTargetTypeMetadataNode>()
.FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) ??
throw new XamlParseException("Can not find parent Style Selector or ControlTemplate TargetType", node);
IXamlType propType = null;
var property = @on.Children.OfType<XamlAstXamlPropertyValueNode>()
.FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property");
if (property != null)
{
var propertyName = property.Values.OfType<XamlAstTextNode>().FirstOrDefault()?.Text;
if (propertyName == null)
throw new XamlParseException("Setter.Property must be a string", node);
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};
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)
{

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 Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Styling;
using Avalonia.Styling.Activators;
using Xunit;
@ -257,6 +258,30 @@ namespace Avalonia.Base.UnitTests.Styling
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
{
}

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

@ -69,6 +69,13 @@ namespace Avalonia.Base.UnitTests.Styling
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]
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.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using Xunit;
using System.Collections.ObjectModel;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.Input;
using System.Collections.Generic;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
@ -62,6 +61,25 @@ namespace Avalonia.Controls.UnitTests
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]
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]
public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items()
{

63
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -1595,8 +1595,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { "Bar" }, selectedItems);
}
[Fact]
public void MoveSelection_Wrap_Does_Not_Hang_With_No_Focusable_Controls()
[Fact(Timeout = 2000)]
public async Task MoveSelection_Wrap_Does_Not_Hang_With_No_Focusable_Controls()
{
// Issue #3094.
var target = new TestSelector
@ -1612,11 +1612,34 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
target.MoveSelection(NavigationDirection.Next, true);
// Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below.
// https://github.com/xunit/xunit/issues/2222
await Task.Run(() => target.MoveSelection(NavigationDirection.Next, true));
}
[Fact(Timeout = 2000)]
public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_First_Item()
[Fact]
public void MoveSelection_Skips_Non_Focusable_Controls_When_Moving_To_Last_Item()
{
var target = new TestSelector
{
Template = Template(),
Items = new[]
{
new ListBoxItem(),
new ListBoxItem { Focusable = false },
}
};
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
target.MoveSelection(NavigationDirection.Last, true);
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void MoveSelection_Skips_Non_Focusable_Controls_When_Moving_To_First_Item()
{
var target = new TestSelector
{
@ -1630,22 +1653,43 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
target.MoveSelection(NavigationDirection.Last, true);
Assert.Equal(1, target.SelectedIndex);
}
// Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below.
[Fact(Timeout = 2000)]
public async Task MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_First_Item()
{
var target = new TestSelector
{
Template = Template(),
Items = new[]
{
new ListBoxItem { Focusable = false },
new ListBoxItem { Focusable = false },
}
};
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
// Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below.
// https://github.com/xunit/xunit/issues/2222
await Task.Run(() => target.MoveSelection(NavigationDirection.First, true));
Assert.Equal(-1, target.SelectedIndex);
}
[Fact(Timeout = 2000)]
public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_Last_Item()
public async Task MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_Last_Item()
{
var target = new TestSelector
{
Template = Template(),
Items = new[]
{
new ListBoxItem(),
new ListBoxItem { Focusable = false },
new ListBoxItem { Focusable = false },
}
};
@ -1653,9 +1697,10 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
// Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below.
// Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below.
// https://github.com/xunit/xunit/issues/2222
await Task.Run(() => target.MoveSelection(NavigationDirection.Last, true));
Assert.Equal(-1, target.SelectedIndex);
}

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

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