Browse Source

Handle adding/removing nested styles.

pull/3647/head
Steven Kirk 7 years ago
parent
commit
6db44298bb
  1. 40
      src/Avalonia.Styling/StyledElement.cs
  2. 8
      src/Avalonia.Styling/Styling/IStyle.cs
  3. 4
      src/Avalonia.Styling/Styling/IStyleHost.cs
  4. 2
      src/Avalonia.Styling/Styling/Style.cs
  5. 29
      src/Avalonia.Styling/Styling/Styles.cs
  6. 23
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  7. 117
      tests/Avalonia.Styling.UnitTests/StyleTests.cs

40
src/Avalonia.Styling/StyledElement.cs

@ -504,7 +504,8 @@ namespace Avalonia
void IStyleHost.StylesRemoved(IReadOnlyList<IStyle> 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<IStyle> RecurseStyles(IReadOnlyList<IStyle> styles)
{
var count = styles.Count;
List<IStyle>? 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<IStyle>(styles);
}
RecurseStyles(style.Children, result);
}
}
return result ?? styles;
}
private static void RecurseStyles(IReadOnlyList<IStyle> styles, List<IStyle> 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);
}
}
}
}

8
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
{
/// <summary>
/// Attaches the style to a control if the style's selector matches.
/// Gets a collection of child styles.
/// </summary>
IReadOnlyList<IStyle> Children { get; }
/// <summary>
/// Attaches the style and any child styles to a control if the style's selector matches.
/// </summary>
/// <param name="target">The control to attach to.</param>
/// <param name="host">The element that hosts the style.</param>

4
src/Avalonia.Styling/Styling/IStyleHost.cs

@ -33,13 +33,13 @@ namespace Avalonia.Styling
IStyleHost StylingParent { get; }
/// <summary>
/// Called when styles are added to <see cref="Styles"/>.
/// Called when styles are added to <see cref="Styles"/> or a nested styles collection.
/// </summary>
/// <param name="styles">The added styles.</param>
void StylesAdded(IReadOnlyList<IStyle> styles);
/// <summary>
/// Called when styles are removed from <see cref="Styles"/>.
/// Called when styles are removed from <see cref="Styles"/> or a nested styles collection.
/// </summary>
/// <param name="styles">The removed styles.</param>
void StylesRemoved(IReadOnlyList<IStyle> styles);

2
src/Avalonia.Styling/Styling/Style.cs

@ -90,6 +90,8 @@ namespace Avalonia.Styling
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
IReadOnlyList<IStyle> IStyle.Children => Array.Empty<IStyle>();
/// <inheritdoc/>
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{

29
src/Avalonia.Styling/Styling/Styles.cs

@ -84,6 +84,8 @@ namespace Avalonia.Styling
/// <inheritdoc/>
IStyle IReadOnlyList<IStyle>.this[int index] => _styles[index];
IReadOnlyList<IStyle> IStyle.Children => this;
/// <inheritdoc/>
public IStyle this[int index]
{
@ -257,10 +259,7 @@ namespace Avalonia.Styling
_cache = null;
}
if (_parent is IStyleHost host)
{
host.StylesAdded(ToReadOnlyList<IStyle>(items));
}
GetHost()?.StylesAdded(ToReadOnlyList<IStyle>(items));
}
void Remove(IList items)
@ -284,10 +283,7 @@ namespace Avalonia.Styling
_cache = null;
}
if (_parent is IStyleHost host)
{
host.StylesRemoved(ToReadOnlyList<IStyle>(items));
}
GetHost()?.StylesRemoved(ToReadOnlyList<IStyle>(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);

23
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
{
/// <summary>
@ -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;
/// <summary>
/// Initializes a new instance of the <see cref="StyleInclude"/> class.
@ -41,7 +43,7 @@ namespace Avalonia.Markup.Xaml.Styling
/// <summary>
/// Gets or sets the source URL.
/// </summary>
public Uri Source { get; set; }
public Uri? Source { get; set; }
/// <summary>
/// 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;
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => _parent;
IResourceNode? IResourceNode.ResourceParent => _parent;
IReadOnlyList<IStyle> IStyle.Children => _loaded ?? Array.Empty<IStyle>();
/// <inheritdoc/>
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost host) => Loaded.TryAttach(target, host);
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
/// <inheritdoc/>
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);
/// <inheritdoc/>
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)

117
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<Border>())
{
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<Border>())
{
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<Border>())
{
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<Border>())
{
Setters =
{
new Setter(Border.BorderThicknessProperty, new Thickness(4)),
}
},
new Style(x => x.OfType<Border>())
{
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<string> FooProperty =

Loading…
Cancel
Save