diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index d7b1aec0b7..f8838e8cfb 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Reactive.Concurrency; using System.Threading; using Avalonia.Animation; @@ -45,6 +46,8 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; private bool _notifyingResourcesChanged; + private Action> _stylesAdded; + private Action> _stylesRemoved; /// /// Defines the property. @@ -201,6 +204,18 @@ namespace Avalonia /// public IApplicationLifetime ApplicationLifetime { get; set; } + event Action> IGlobalStyles.GlobalStylesAdded + { + add => _stylesAdded += value; + remove => _stylesAdded -= value; + } + + event Action> IGlobalStyles.GlobalStylesRemoved + { + add => _stylesRemoved += value; + remove => _stylesRemoved -= value; + } + /// /// Initializes the application by loading XAML etc. /// @@ -214,6 +229,16 @@ namespace Avalonia Styles.TryGetResource(key, out value); } + void IStyleHost.StylesAdded(IReadOnlyList styles) + { + _stylesAdded?.Invoke(styles); + } + + void IStyleHost.StylesRemoved(IReadOnlyList styles) + { + _stylesRemoved?.Invoke(styles); + } + /// /// Register's the services needed by Avalonia. /// @@ -286,6 +311,5 @@ namespace Avalonia get => _name; set => SetAndRaise(NameProperty, ref _name, value); } - } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index cabadac7d6..f477379777 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -50,6 +50,7 @@ namespace Avalonia.Controls private readonly IAccessKeyHandler _accessKeyHandler; private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; private readonly IPlatformRenderInterface _renderInterface; + private readonly IGlobalStyles _globalStyles; private Size _clientSize; private ILayoutManager _layoutManager; @@ -94,6 +95,7 @@ namespace Avalonia.Controls _inputManager = TryGetService(dependencyResolver); _keyboardNavigationHandler = TryGetService(dependencyResolver); _renderInterface = TryGetService(dependencyResolver); + _globalStyles = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); @@ -112,6 +114,13 @@ namespace Avalonia.Controls _keyboardNavigationHandler?.SetOwner(this); _accessKeyHandler?.SetOwner(this); + + if (_globalStyles is object) + { + _globalStyles.GlobalStylesAdded += ((IStyleHost)this).StylesAdded; + _globalStyles.GlobalStylesRemoved += ((IStyleHost)this).StylesRemoved; + } + styler?.ApplyStyles(this); ClientSize = impl.ClientSize; @@ -215,10 +224,7 @@ namespace Avalonia.Controls /// double IRenderRoot.RenderScaling => PlatformImpl?.Scaling ?? 1; - IStyleHost IStyleHost.StylingParent - { - get { return AvaloniaLocator.Current.GetService(); } - } + IStyleHost IStyleHost.StylingParent => _globalStyles; IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget(); @@ -267,6 +273,12 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { + if (_globalStyles is object) + { + _globalStyles.GlobalStylesAdded -= ((IStyleHost)this).StylesAdded; + _globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved; + } + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 9d0a5c57ee..5fbcb45d4b 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -507,6 +507,7 @@ namespace Avalonia.Layout { var margin = Margin; + ApplyStyling(); ApplyTemplate(); var constrained = LayoutHelper.ApplyLayoutConstraints( @@ -692,6 +693,12 @@ namespace Avalonia.Layout return finalSize; } + protected sealed override void InvalidateStyles() + { + base.InvalidateStyles(); + InvalidateMeasure(); + } + /// protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 84df9d4b94..241ca1d0ec 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -213,6 +213,7 @@ namespace Avalonia { _styles = new Styles(this); _styles.ResourcesChanged += ThisResourcesChanged; + NotifyResourcesChanged(new ResourcesChangedEventArgs()); } return _styles; @@ -352,21 +353,34 @@ namespace Avalonia if (--_initCount == 0 && _logicalRoot != null) { - InitializeStylesIfNeeded(); - + ApplyStyling(); InitializeIfNeeded(); } } - private void InitializeStylesIfNeeded(bool force = false) + /// + /// Applies styling to the control if the control is initialized and styling is not + /// already applied. + /// + /// + /// A value indicating whether styling is now applied to the control. + /// + protected bool ApplyStyling() { - if (_initCount == 0 && (!_styled || force)) + if (_initCount == 0 && !_styled) { - ApplyStyling(); + AvaloniaLocator.Current.GetService()?.ApplyStyles(this); _styled = true; } + + return _styled; } + /// + /// Detaches all styles from the element and queues a restyle. + /// + protected virtual void InvalidateStyles() => DetachStyles(); + protected void InitializeIfNeeded() { if (_initCount == 0 && !IsInitialized) @@ -480,6 +494,21 @@ namespace Avalonia void IStyleable.DetachStyles() => DetachStyles(); + void IStyleable.DetachStyles(IReadOnlyList styles) => DetachStyles(styles); + + void IStyleable.InvalidateStyles() => InvalidateStyles(); + + void IStyleHost.StylesAdded(IReadOnlyList styles) + { + InvalidateStylesOnThisAndDescendents(); + } + + void IStyleHost.StylesRemoved(IReadOnlyList styles) + { + var allStyles = RecurseStyles(styles); + DetachStylesFromThisAndDescendents(allStyles); + } + protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -604,11 +633,6 @@ namespace Avalonia return null; } - private void ApplyStyling() - { - AvaloniaLocator.Current.GetService()?.ApplyStyles(this); - } - private static void ValidateLogicalChild(ILogical c) { if (c == null) @@ -635,7 +659,7 @@ namespace Avalonia { _logicalRoot = e.Root; - InitializeStylesIfNeeded(true); + ApplyStyling(); OnAttachedToLogicalTree(e); AttachedToLogicalTree?.Invoke(this, e); @@ -713,6 +737,64 @@ namespace Avalonia _appliedStyles.Clear(); } + + _styled = false; + } + + private void DetachStyles(IReadOnlyList styles) + { + styles = styles ?? throw new ArgumentNullException(nameof(styles)); + + if (_appliedStyles is null) + { + return; + } + + var count = styles.Count; + + for (var i = 0; i < count; ++i) + { + for (var j = _appliedStyles.Count - 1; j >= 0; --j) + { + var applied = _appliedStyles[j]; + + if (applied.Source == styles[i]) + { + applied.Dispose(); + _appliedStyles.RemoveAt(j); + } + } + } + } + + private void InvalidateStylesOnThisAndDescendents() + { + InvalidateStyles(); + + if (_logicalChildren is object) + { + var childCount = _logicalChildren.Count; + + for (var i = 0; i < childCount; ++i) + { + (_logicalChildren[i] as StyledElement)?.InvalidateStylesOnThisAndDescendents(); + } + } + } + + private void DetachStylesFromThisAndDescendents(IReadOnlyList styles) + { + DetachStyles(styles); + + if (_logicalChildren is object) + { + var childCount = _logicalChildren.Count; + + for (var i = 0; i < childCount; ++i) + { + (_logicalChildren[i] as StyledElement)?.DetachStylesFromThisAndDescendents(styles); + } + } } private void ClearLogicalParent(IEnumerable children) @@ -750,5 +832,40 @@ 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; + + 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/IGlobalStyles.cs b/src/Avalonia.Styling/Styling/IGlobalStyles.cs index 51393ef0b3..6152755a05 100644 --- a/src/Avalonia.Styling/Styling/IGlobalStyles.cs +++ b/src/Avalonia.Styling/Styling/IGlobalStyles.cs @@ -1,6 +1,11 @@ // 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; +using System.Collections.Generic; + +#nullable enable + namespace Avalonia.Styling { /// @@ -8,5 +13,14 @@ namespace Avalonia.Styling /// public interface IGlobalStyles : IStyleHost { + /// + /// Raised when styles are added to or a nested styles collection. + /// + public event Action> GlobalStylesAdded; + + /// + /// Raised when styles are removed from or a nested styles collection. + /// + public event Action> GlobalStylesRemoved; } } 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 fa722aeb41..182662b32e 100644 --- a/src/Avalonia.Styling/Styling/IStyleHost.cs +++ b/src/Avalonia.Styling/Styling/IStyleHost.cs @@ -2,6 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Generic; + +#nullable enable + namespace Avalonia.Styling { /// @@ -27,5 +31,17 @@ namespace Avalonia.Styling /// Gets the parent style host element. /// IStyleHost StylingParent { get; } + + /// + /// Called when styles are added to or a nested styles collection. + /// + /// The added styles. + void StylesAdded(IReadOnlyList styles); + + /// + /// 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/IStyleable.cs b/src/Avalonia.Styling/Styling/IStyleable.cs index b01c779bcc..34687b070c 100644 --- a/src/Avalonia.Styling/Styling/IStyleable.cs +++ b/src/Avalonia.Styling/Styling/IStyleable.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using Avalonia.Collections; #nullable enable @@ -38,5 +39,15 @@ namespace Avalonia.Styling /// Detaches all styles applied to the element. /// void DetachStyles(); + + /// + /// Detaches a collection of styles, if applied to the element. + /// + void DetachStyles(IReadOnlyList styles); + + /// + /// Detaches all styles from the element and queues a restyle. + /// + void InvalidateStyles(); } } 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 05cbba5ad2..c9278339b5 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -27,40 +27,7 @@ namespace Avalonia.Styling public Styles() { _styles.ResetBehavior = ResetBehavior.Remove; - _styles.ForEachItem( - x => - { - if (x.ResourceParent == null && x is ISetResourceParent setParent) - { - setParent.SetParent(this); - setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); - } - - if (x.HasResources) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } - - x.ResourcesChanged += NotifyResourcesChanged; - _cache = null; - }, - x => - { - if (x.ResourceParent == this && x is ISetResourceParent setParent) - { - setParent.SetParent(null); - setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); - } - - if (x.HasResources) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } - - x.ResourcesChanged -= NotifyResourcesChanged; - _cache = null; - }, - () => { }); + _styles.CollectionChanged += OnCollectionChanged; } public Styles(IResourceNode parent) @@ -69,11 +36,7 @@ namespace Avalonia.Styling _parent = parent; } - public event NotifyCollectionChangedEventHandler CollectionChanged - { - add => _styles.CollectionChanged += value; - remove => _styles.CollectionChanged -= value; - } + public event NotifyCollectionChangedEventHandler? CollectionChanged; /// public event EventHandler? ResourcesChanged; @@ -121,6 +84,8 @@ namespace Avalonia.Styling /// IStyle IReadOnlyList.this[int index] => _styles[index]; + IReadOnlyList IStyle.Children => this; + /// public IStyle this[int index] { @@ -257,6 +222,106 @@ namespace Avalonia.Styling NotifyResourcesChanged(e); } + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + static IReadOnlyList ToReadOnlyList(IList list) + { + if (list is IReadOnlyList) + { + return (IReadOnlyList)list; + } + else + { + var result = new T[list.Count]; + list.CopyTo(result, 0); + return result; + } + } + + void Add(IList items) + { + for (var i = 0; i < items.Count; ++i) + { + var style = (IStyle)items[i]; + + if (style.ResourceParent == null && style is ISetResourceParent setParent) + { + setParent.SetParent(this); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + } + + if (style.HasResources) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + + style.ResourcesChanged += NotifyResourcesChanged; + _cache = null; + } + + GetHost()?.StylesAdded(ToReadOnlyList(items)); + } + + void Remove(IList items) + { + for (var i = 0; i < items.Count; ++i) + { + var style = (IStyle)items[i]; + + if (style.ResourceParent == this && style is ISetResourceParent setParent) + { + setParent.SetParent(null); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + } + + if (style.HasResources) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + + style.ResourcesChanged -= NotifyResourcesChanged; + _cache = null; + } + + GetHost()?.StylesRemoved(ToReadOnlyList(items)); + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + Remove(e.OldItems); + break; + case NotifyCollectionChangedAction.Replace: + Remove(e.OldItems); + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Reset: + throw new InvalidOperationException("Reset should not be called on Styles."); + } + + 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.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 91592b1ff7..26bf604187 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -9,6 +9,7 @@ using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Platform; +using Avalonia.Styling; using Avalonia.UnitTests; using Moq; using Xunit; @@ -269,6 +270,44 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Reacts_To_Changes_In_Global_Styles() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + impl.SetupGet(x => x.Scaling).Returns(1); + + var child = new Border { Classes = { "foo" } }; + var target = new TestTopLevel(impl.Object) + { + Template = CreateTemplate(), + Content = child, + }; + + target.LayoutManager.ExecuteInitialLayoutPass(target); + + Assert.Equal(new Thickness(0), child.BorderThickness); + + var style = new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(2)) + } + }; + + Application.Current.Styles.Add(style); + target.LayoutManager.ExecuteInitialLayoutPass(target); + + Assert.Equal(new Thickness(2), child.BorderThickness); + + Application.Current.Styles.Remove(style); + + Assert.Equal(new Thickness(0), child.BorderThickness); + } + } + private FuncControlTemplate CreateTemplate() { return new FuncControlTemplate((x, scope) => diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index ec9a6ba77f..d4953bf47e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -9,6 +9,7 @@ using Avalonia.Styling; using Xunit; using System.ComponentModel; using Avalonia.Markup.Xaml.XamlIl.Runtime; +using System.Collections.Generic; namespace Avalonia.Markup.Xaml.UnitTests.Converters { @@ -144,6 +145,16 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters throw new NotImplementedException(); } + public void DetachStyles(IReadOnlyList styles) + { + throw new NotImplementedException(); + } + + public void InvalidateStyles() + { + throw new NotImplementedException(); + } + public void StyleApplied(IStyleInstance instance) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Styling.UnitTests/StyleTests.cs b/tests/Avalonia.Styling.UnitTests/StyleTests.cs index 4c9b18c886..619f72aab0 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleTests.cs @@ -244,6 +244,185 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(new Thickness(0), border.BorderThickness); } + [Fact] + public void Removing_Style_Should_Detach_From_Control() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var border = new Border(); + var root = new TestRoot + { + 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_Style_Should_Attach_To_Control() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var border = new Border(); + var root = new TestRoot + { + 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.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_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 =