diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index cee30efcf0..7b5cf4b7fa 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -504,7 +504,8 @@ namespace Avalonia void IStyleHost.StylesRemoved(IReadOnlyList styles) { - DetachStylesFromThisAndDescendents(styles); + var allStyles = RecurseStyles(styles); + DetachStylesFromThisAndDescendents(allStyles); } protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -830,5 +831,42 @@ namespace Avalonia { NotifyResourcesChanged(e); } + + private static IReadOnlyList RecurseStyles(IReadOnlyList styles) + { + var count = styles.Count; + List? result = null; + + for (var i = 0; i < count; ++i) + { + var style = styles[i]; + + if (style.Children.Count > 0) + { + if (result is null) + { + result = new List(styles); + } + + RecurseStyles(style.Children, result); + } + } + + return result ?? styles; + } + + private static void RecurseStyles(IReadOnlyList styles, List result) + { + var count = styles.Count; + + result.Capacity += count; + + for (var i = 0; i < count; ++i) + { + var style = styles[i]; + result.Add(style); + RecurseStyles(style.Children, result); + } + } } } diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs index 8151aacf54..18866b0060 100644 --- a/src/Avalonia.Styling/Styling/IStyle.cs +++ b/src/Avalonia.Styling/Styling/IStyle.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Generic; using Avalonia.Controls; #nullable enable @@ -13,7 +14,12 @@ namespace Avalonia.Styling public interface IStyle : IResourceNode { /// - /// Attaches the style to a control if the style's selector matches. + /// Gets a collection of child styles. + /// + IReadOnlyList Children { get; } + + /// + /// Attaches the style and any child styles to a control if the style's selector matches. /// /// The control to attach to. /// The element that hosts the style. diff --git a/src/Avalonia.Styling/Styling/IStyleHost.cs b/src/Avalonia.Styling/Styling/IStyleHost.cs index 73726f3056..182662b32e 100644 --- a/src/Avalonia.Styling/Styling/IStyleHost.cs +++ b/src/Avalonia.Styling/Styling/IStyleHost.cs @@ -33,13 +33,13 @@ namespace Avalonia.Styling IStyleHost StylingParent { get; } /// - /// Called when styles are added to . + /// Called when styles are added to or a nested styles collection. /// /// The added styles. void StylesAdded(IReadOnlyList styles); /// - /// Called when styles are removed from . + /// Called when styles are removed from or a nested styles collection. /// /// The removed styles. void StylesRemoved(IReadOnlyList styles); diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 22adf573e8..2e0f564522 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -90,6 +90,8 @@ namespace Avalonia.Styling /// bool IResourceProvider.HasResources => _resources?.Count > 0; + IReadOnlyList IStyle.Children => Array.Empty(); + /// public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index edd729679d..c9278339b5 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -84,6 +84,8 @@ namespace Avalonia.Styling /// IStyle IReadOnlyList.this[int index] => _styles[index]; + IReadOnlyList IStyle.Children => this; + /// public IStyle this[int index] { @@ -257,10 +259,7 @@ namespace Avalonia.Styling _cache = null; } - if (_parent is IStyleHost host) - { - host.StylesAdded(ToReadOnlyList(items)); - } + GetHost()?.StylesAdded(ToReadOnlyList(items)); } void Remove(IList items) @@ -284,10 +283,7 @@ namespace Avalonia.Styling _cache = null; } - if (_parent is IStyleHost host) - { - host.StylesRemoved(ToReadOnlyList(items)); - } + GetHost()?.StylesRemoved(ToReadOnlyList(items)); } switch (e.Action) @@ -309,6 +305,23 @@ namespace Avalonia.Styling CollectionChanged?.Invoke(this, e); } + private IStyleHost? GetHost() + { + var node = _parent; + + while (node != null) + { + if (node is IStyleHost host) + { + return host; + } + + node = node.ResourceParent; + } + + return null; + } + private void NotifyResourcesChanged(object sender, ResourcesChangedEventArgs e) { NotifyResourcesChanged(e); diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index e5e28b344f..46fc7c36da 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -6,6 +6,8 @@ using System; using Avalonia.Controls; using System.Collections.Generic; +#nullable enable + namespace Avalonia.Markup.Xaml.Styling { /// @@ -14,8 +16,8 @@ namespace Avalonia.Markup.Xaml.Styling public class StyleInclude : IStyle, ISetResourceParent { private Uri _baseUri; - private IStyle _loaded; - private IResourceNode _parent; + private IStyle[]? _loaded; + private IResourceNode? _parent; /// /// Initializes a new instance of the class. @@ -41,7 +43,7 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Gets or sets the source URL. /// - public Uri Source { get; set; } + public Uri? Source { get; set; } /// /// Gets the loaded style. @@ -53,11 +55,12 @@ namespace Avalonia.Markup.Xaml.Styling if (_loaded == null) { var loader = new AvaloniaXamlLoader(); - _loaded = (IStyle)loader.Load(Source, _baseUri); - (_loaded as ISetResourceParent)?.SetParent(this); + var loaded = (IStyle)loader.Load(Source, _baseUri); + (loaded as ISetResourceParent)?.SetParent(this); + _loaded = new[] { loaded }; } - return _loaded; + return _loaded?[0]!; } } @@ -65,13 +68,15 @@ namespace Avalonia.Markup.Xaml.Styling bool IResourceProvider.HasResources => Loaded.HasResources; /// - IResourceNode IResourceNode.ResourceParent => _parent; + IResourceNode? IResourceNode.ResourceParent => _parent; + + IReadOnlyList IStyle.Children => _loaded ?? Array.Empty(); /// - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost host) => Loaded.TryAttach(target, host); + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); /// - public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); + public bool TryGetResource(object key, out object? value) => Loaded.TryGetResource(key, out value); /// void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) diff --git a/tests/Avalonia.Styling.UnitTests/StyleTests.cs b/tests/Avalonia.Styling.UnitTests/StyleTests.cs index ef53e65aa6..619f72aab0 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleTests.cs @@ -245,7 +245,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Style_Should_Be_Detached_From_Control_When_Removed() + public void Removing_Style_Should_Detach_From_Control() { using (UnitTestApplication.Start(TestServices.RealStyler)) { @@ -274,7 +274,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Style_Should_Be_Atttached_To_Control_When_Added() + public void Adding_Style_Should_Attach_To_Control() { using (UnitTestApplication.Start(TestServices.RealStyler)) { @@ -310,6 +310,119 @@ namespace Avalonia.Styling.UnitTests } } + [Fact] + public void Removing_Style_With_Nested_Style_Should_Detach_From_Control() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var border = new Border(); + var root = new TestRoot + { + Styles = + { + new Styles + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(4)), + } + } + } + }, + Child = border, + }; + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(4), border.BorderThickness); + + root.Styles.RemoveAt(0); + Assert.Equal(new Thickness(0), border.BorderThickness); + } + } + + [Fact] + public void Adding_Nested_Style_Should_Attach_To_Control() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var border = new Border(); + var root = new TestRoot + { + Styles = + { + new Styles + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(4)), + } + } + } + }, + Child = border, + }; + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(4), border.BorderThickness); + + ((Styles)root.Styles[0]).Add(new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(6)), + } + }); + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(6), border.BorderThickness); + } + } + + [Fact] + public void Removing_Nested_Style_Should_Detach_From_Control() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var border = new Border(); + var root = new TestRoot + { + Styles = + { + new Styles + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(4)), + } + }, + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(6)), + } + }, + } + }, + Child = border, + }; + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(6), border.BorderThickness); + + ((Styles)root.Styles[0]).RemoveAt(1); + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(4), border.BorderThickness); + } + } + private class Class1 : Control { public static readonly StyledProperty FooProperty =