Browse Source

Refactored styling.

- Don't use Rx in the styling system. Instead introduces `IStyleActivator` which is like an `IObservable<bool>`-lite in order to cut down on allocations.
- #nullable enable on touched files
pull/3636/head
Steven Kirk 6 years ago
parent
commit
dc55d65287
  1. 17
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  2. 1
      src/Avalonia.Styling/Avalonia.Styling.csproj
  3. 6
      src/Avalonia.Styling/Controls/NameScopeLocator.cs
  4. 138
      src/Avalonia.Styling/StyledElement.cs
  5. 77
      src/Avalonia.Styling/Styling/ActivatedObservable.cs
  6. 110
      src/Avalonia.Styling/Styling/ActivatedSubject.cs
  7. 133
      src/Avalonia.Styling/Styling/ActivatedValue.cs
  8. 67
      src/Avalonia.Styling/Styling/Activators/AndActivator.cs
  9. 33
      src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs
  10. 17
      src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs
  11. 13
      src/Avalonia.Styling/Styling/Activators/NotActivator.cs
  12. 67
      src/Avalonia.Styling/Styling/Activators/OrActivator.cs
  13. 35
      src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs
  14. 55
      src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs
  15. 72
      src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs
  16. 48
      src/Avalonia.Styling/Styling/DescendentSelector.cs
  17. 19
      src/Avalonia.Styling/Styling/ISetter.cs
  18. 20
      src/Avalonia.Styling/Styling/ISetterInstance.cs
  19. 16
      src/Avalonia.Styling/Styling/IStyle.cs
  20. 22
      src/Avalonia.Styling/Styling/IStyleInstance.cs
  21. 20
      src/Avalonia.Styling/Styling/IStyleable.cs
  22. 16
      src/Avalonia.Styling/Styling/NotSelector.cs
  23. 53
      src/Avalonia.Styling/Styling/OrSelector.cs
  24. 25
      src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs
  25. 48
      src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs
  26. 82
      src/Avalonia.Styling/Styling/PropertySetterInstance.cs
  27. 44
      src/Avalonia.Styling/Styling/Selector.cs
  28. 29
      src/Avalonia.Styling/Styling/SelectorMatch.cs
  29. 141
      src/Avalonia.Styling/Styling/Setter.cs
  30. 165
      src/Avalonia.Styling/Styling/Style.cs
  31. 56
      src/Avalonia.Styling/Styling/StyleActivator.cs
  32. 81
      src/Avalonia.Styling/Styling/StyleInstance.cs
  33. 25
      src/Avalonia.Styling/Styling/Styler.cs
  34. 82
      src/Avalonia.Styling/Styling/Styles.cs
  35. 109
      src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs
  36. 19
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  37. 2
      tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
  38. 6
      tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs
  39. 8
      tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs
  40. 2
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  41. 2
      tests/Avalonia.Controls.UnitTests/UserControlTests.cs
  42. 10
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs
  43. 37
      tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs
  44. 71
      tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs
  45. 92
      tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs
  46. 75
      tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs
  47. 16
      tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs
  48. 2
      tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs
  49. 29
      tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs
  50. 8
      tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs
  51. 2
      tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs
  52. 51
      tests/Avalonia.Styling.UnitTests/SetterTests.cs
  53. 42
      tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs
  54. 169
      tests/Avalonia.Styling.UnitTests/StyleActivatorTests.cs
  55. 58
      tests/Avalonia.Styling.UnitTests/StyleTests.cs
  56. 9
      tests/Avalonia.Styling.UnitTests/StyledElementTests.cs

17
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -6,6 +6,8 @@ using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Styling;
using Avalonia.VisualTree;
@ -22,22 +24,25 @@ namespace Avalonia.Diagnostics.ViewModels
Type = visual.GetType().Name;
Visual = visual;
if (visual is IStyleable styleable)
if (visual is IControl control)
{
var removed = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => control.DetachedFromLogicalTree += x,
x => control.DetachedFromLogicalTree -= x);
var classesChanged = Observable.FromEventPattern<
NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>(
x => styleable.Classes.CollectionChanged += x,
x => styleable.Classes.CollectionChanged -= x)
.TakeUntil(((IStyleable)styleable).StyleDetach);
x => control.Classes.CollectionChanged += x,
x => control.Classes.CollectionChanged -= x)
.TakeUntil(removed);
classesChanged.Select(_ => Unit.Default)
.StartWith(Unit.Default)
.Subscribe(_ =>
{
if (styleable.Classes.Count > 0)
if (control.Classes.Count > 0)
{
Classes = "(" + string.Join(" ", styleable.Classes) + ")";
Classes = "(" + string.Join(" ", control.Classes) + ")";
}
else
{

1
src/Avalonia.Styling/Avalonia.Styling.csproj

@ -8,5 +8,4 @@
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
</Project>

6
src/Avalonia.Styling/Controls/NameScopeLocator.cs

@ -1,11 +1,5 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Threading.Tasks;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
using Avalonia.Utilities;
namespace Avalonia.Controls

138
src/Avalonia.Styling/StyledElement.cs

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Animation;
using Avalonia.Collections;
using Avalonia.Controls;
@ -14,6 +12,8 @@ using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Styling;
#nullable enable
namespace Avalonia
{
/// <summary>
@ -29,8 +29,8 @@ namespace Avalonia
/// <summary>
/// Defines the <see cref="DataContext"/> property.
/// </summary>
public static readonly StyledProperty<object> DataContextProperty =
AvaloniaProperty.Register<StyledElement, object>(
public static readonly StyledProperty<object?> DataContextProperty =
AvaloniaProperty.Register<StyledElement, object?>(
nameof(DataContext),
inherits: true,
notifying: DataContextNotifying);
@ -38,34 +38,34 @@ namespace Avalonia
/// <summary>
/// Defines the <see cref="Name"/> property.
/// </summary>
public static readonly DirectProperty<StyledElement, string> NameProperty =
AvaloniaProperty.RegisterDirect<StyledElement, string>(nameof(Name), o => o.Name, (o, v) => o.Name = v);
public static readonly DirectProperty<StyledElement, string?> NameProperty =
AvaloniaProperty.RegisterDirect<StyledElement, string?>(nameof(Name), o => o.Name, (o, v) => o.Name = v);
/// <summary>
/// Defines the <see cref="Parent"/> property.
/// </summary>
public static readonly DirectProperty<StyledElement, IStyledElement> ParentProperty =
AvaloniaProperty.RegisterDirect<StyledElement, IStyledElement>(nameof(Parent), o => o.Parent);
public static readonly DirectProperty<StyledElement, IStyledElement?> ParentProperty =
AvaloniaProperty.RegisterDirect<StyledElement, IStyledElement?>(nameof(Parent), o => o.Parent);
/// <summary>
/// Defines the <see cref="TemplatedParent"/> property.
/// </summary>
public static readonly DirectProperty<StyledElement, ITemplatedControl> TemplatedParentProperty =
AvaloniaProperty.RegisterDirect<StyledElement, ITemplatedControl>(
public static readonly DirectProperty<StyledElement, ITemplatedControl?> TemplatedParentProperty =
AvaloniaProperty.RegisterDirect<StyledElement, ITemplatedControl?>(
nameof(TemplatedParent),
o => o.TemplatedParent,
(o ,v) => o.TemplatedParent = v);
private int _initCount;
private string _name;
private string? _name;
private readonly Classes _classes = new Classes();
private ILogicalRoot _logicalRoot;
private IAvaloniaList<ILogical> _logicalChildren;
private IResourceDictionary _resources;
private Styles _styles;
private ILogicalRoot? _logicalRoot;
private IAvaloniaList<ILogical>? _logicalChildren;
private IResourceDictionary? _resources;
private Styles? _styles;
private bool _styled;
private Subject<IStyleable> _styleDetach = new Subject<IStyleable>();
private ITemplatedControl _templatedParent;
private List<IStyleInstance>? _appliedStyles;
private ITemplatedControl? _templatedParent;
private bool _dataContextUpdating;
/// <summary>
@ -87,12 +87,12 @@ namespace Avalonia
/// <summary>
/// Raised when the styled element is attached to a rooted logical tree.
/// </summary>
public event EventHandler<LogicalTreeAttachmentEventArgs> AttachedToLogicalTree;
public event EventHandler<LogicalTreeAttachmentEventArgs>? AttachedToLogicalTree;
/// <summary>
/// Raised when the styled element is detached from a rooted logical tree.
/// </summary>
public event EventHandler<LogicalTreeAttachmentEventArgs> DetachedFromLogicalTree;
public event EventHandler<LogicalTreeAttachmentEventArgs>? DetachedFromLogicalTree;
/// <summary>
/// Occurs when the <see cref="DataContext"/> property changes.
@ -101,7 +101,7 @@ namespace Avalonia
/// This event will be raised when the <see cref="DataContext"/> property has changed and
/// all subscribers to that change have been notified.
/// </remarks>
public event EventHandler DataContextChanged;
public event EventHandler? DataContextChanged;
/// <summary>
/// Occurs when the styled element has finished initialization.
@ -114,12 +114,12 @@ namespace Avalonia
/// <see cref="ISupportInitialize"/> is not used, it is called when the styled element is attached
/// to the visual tree.
/// </remarks>
public event EventHandler Initialized;
public event EventHandler? Initialized;
/// <summary>
/// Occurs when a resource in this styled element or a parent styled element has changed.
/// </summary>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
/// <summary>
/// Gets or sets the name of the styled element.
@ -128,20 +128,12 @@ namespace Avalonia
/// An element's name is used to uniquely identify an element within the element's name
/// scope. Once the element is added to a logical tree, its name cannot be changed.
/// </remarks>
public string Name
public string? Name
{
get
{
return _name;
}
get => _name;
set
{
if (String.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException("Cannot set Name to null or empty string.");
}
if (_styled)
{
throw new InvalidOperationException("Cannot set Name : styled element already styled.");
@ -189,7 +181,7 @@ namespace Avalonia
/// The data context is an inherited property that specifies the default object that will
/// be used for data binding.
/// </remarks>
public object DataContext
public object? DataContext
{
get { return GetValue(DataContextProperty); }
set { SetValue(DataContextProperty, value); }
@ -214,28 +206,15 @@ namespace Avalonia
/// </remarks>
public Styles Styles
{
get { return _styles ?? (Styles = new Styles()); }
set
get
{
Contract.Requires<ArgumentNullException>(value != null);
if (_styles != value)
if (_styles is null)
{
if (_styles != null)
{
(_styles as ISetResourceParent)?.SetParent(null);
_styles.ResourcesChanged -= ThisResourcesChanged;
}
_styles = value;
if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
{
setParent.SetParent(this);
}
_styles = new Styles(this);
_styles.ResourcesChanged += ThisResourcesChanged;
}
return _styles;
}
}
@ -247,7 +226,7 @@ namespace Avalonia
get => _resources ?? (Resources = new ResourceDictionary());
set
{
Contract.Requires<ArgumentNullException>(value != null);
value = value ?? throw new ArgumentNullException(nameof(value));
var hadResources = false;
@ -270,7 +249,7 @@ namespace Avalonia
/// <summary>
/// Gets the styled element whose lookless template this styled element is part of.
/// </summary>
public ITemplatedControl TemplatedParent
public ITemplatedControl? TemplatedParent
{
get => _templatedParent;
internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value);
@ -312,12 +291,12 @@ namespace Avalonia
/// <summary>
/// Gets the styled element's logical parent.
/// </summary>
public IStyledElement Parent { get; private set; }
public IStyledElement? Parent { get; private set; }
/// <summary>
/// Gets the styled element's logical parent.
/// </summary>
ILogical ILogical.LogicalParent => Parent;
ILogical? ILogical.LogicalParent => Parent;
/// <summary>
/// Gets the styled element's logical children.
@ -328,7 +307,7 @@ namespace Avalonia
bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources;
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode;
IResourceNode? IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode;
/// <inheritdoc/>
IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes;
@ -344,14 +323,11 @@ namespace Avalonia
/// </remarks>
Type IStyleable.StyleKey => GetType();
/// <inheritdoc/>
IObservable<IStyleable> IStyleable.StyleDetach => _styleDetach;
/// <inheritdoc/>
bool IStyleHost.IsStylesInitialized => _styles != null;
/// <inheritdoc/>
IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent;
IStyleHost? IStyleHost.StylingParent => (IStyleHost)InheritanceParent;
/// <inheritdoc/>
public virtual void BeginInit()
@ -397,13 +373,13 @@ namespace Avalonia
/// <inheritdoc/>
void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
this.OnAttachedToLogicalTreeCore(e);
OnAttachedToLogicalTreeCore(e);
}
/// <inheritdoc/>
void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
this.OnDetachedFromLogicalTreeCore(e);
OnDetachedFromLogicalTreeCore(e);
}
/// <inheritdoc/>
@ -413,7 +389,7 @@ namespace Avalonia
}
/// <inheritdoc/>
bool IResourceProvider.TryGetResource(object key, out object value)
bool IResourceProvider.TryGetResource(object key, out object? value)
{
value = null;
return (_resources?.TryGetResource(key, out value) ?? false) ||
@ -424,7 +400,7 @@ namespace Avalonia
/// Sets the styled element's logical parent.
/// </summary>
/// <param name="parent">The parent.</param>
void ISetLogicalParent.SetParent(ILogical parent)
void ISetLogicalParent.SetParent(ILogical? parent)
{
var old = Parent;
@ -440,7 +416,7 @@ namespace Avalonia
InheritanceParent = parent as AvaloniaObject;
}
Parent = (IStyledElement)parent;
Parent = (IStyledElement?)parent;
if (_logicalRoot != null)
{
@ -470,12 +446,13 @@ namespace Avalonia
var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent);
OnAttachedToLogicalTreeCore(e);
}
#nullable disable
RaisePropertyChanged(
ParentProperty,
new Optional<IStyledElement>(old),
new BindingValue<IStyledElement>(Parent),
BindingPriority.LocalValue);
#nullable enable
}
}
@ -488,6 +465,16 @@ namespace Avalonia
InheritanceParent = parent;
}
void IStyleable.StyleApplied(IStyleInstance instance)
{
instance = instance ?? throw new ArgumentNullException(nameof(instance));
_appliedStyles ??= new List<IStyleInstance>();
_appliedStyles.Add(instance);
}
void IStyleable.DetachStyles() => DetachStyles();
protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
@ -597,7 +584,7 @@ namespace Avalonia
}
}
private static ILogicalRoot FindLogicalRoot(IStyleHost e)
private static ILogicalRoot? FindLogicalRoot(IStyleHost e)
{
while (e != null)
{
@ -666,7 +653,7 @@ namespace Avalonia
if (_logicalRoot != null)
{
_logicalRoot = null;
_styleDetach.OnNext(this);
DetachStyles();
OnDetachedFromLogicalTree(e);
DetachedFromLogicalTree?.Invoke(this, e);
@ -682,7 +669,7 @@ namespace Avalonia
}
#if DEBUG
if (((INotifyCollectionChangedDebug)_classes).GetCollectionChangedSubscribers()?.Length > 0)
if (((INotifyCollectionChangedDebug)Classes).GetCollectionChangedSubscribers()?.Length > 0)
{
Logger.TryGet(LogEventLevel.Warning)?.Log(
LogArea.Control,
@ -710,6 +697,19 @@ namespace Avalonia
}
}
private void DetachStyles()
{
if (_appliedStyles is object)
{
foreach (var i in _appliedStyles)
{
i.Dispose();
}
_appliedStyles.Clear();
}
}
private void ClearLogicalParent(IEnumerable<ILogical> children)
{
foreach (var i in children)

77
src/Avalonia.Styling/Styling/ActivatedObservable.cs

@ -1,77 +0,0 @@
// 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;
namespace Avalonia.Styling
{
/// <summary>
/// An observable which is switched on or off according to an activator observable.
/// </summary>
/// <remarks>
/// An <see cref="ActivatedObservable"/> has two inputs: an activator observable and a
/// <see cref="Source"/> observable which produces the activated value. When the activator
/// produces true, the <see cref="ActivatedObservable"/> will produce the current activated
/// value. When the activator produces false it will produce
/// <see cref="AvaloniaProperty.UnsetValue"/>.
/// </remarks>
internal class ActivatedObservable : ActivatedValue, IDescription
{
private IDisposable _sourceSubscription;
/// <summary>
/// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
/// </summary>
/// <param name="activator">The activator.</param>
/// <param name="source">An observable that produces the activated value.</param>
/// <param name="description">The binding description.</param>
public ActivatedObservable(
IObservable<bool> activator,
IObservable<object> source,
string description)
: base(activator, AvaloniaProperty.UnsetValue, description)
{
Contract.Requires<ArgumentNullException>(source != null);
Source = source;
}
/// <summary>
/// Gets an observable which produces the <see cref="ActivatedValue"/>.
/// </summary>
public IObservable<object> Source { get; }
protected override ActivatorListener CreateListener() => new ValueListener(this);
protected override void Deinitialize()
{
base.Deinitialize();
_sourceSubscription.Dispose();
_sourceSubscription = null;
}
protected override void Initialize()
{
base.Initialize();
_sourceSubscription = Source.Subscribe((ValueListener)Listener);
}
protected virtual void NotifyValue(object value)
{
Value = value;
}
private class ValueListener : ActivatorListener, IObserver<object>
{
public ValueListener(ActivatedObservable parent)
: base(parent)
{
}
protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent;
void IObserver<object>.OnCompleted() => Parent.CompletedReceived();
void IObserver<object>.OnError(Exception error) => Parent.ErrorReceived(error);
void IObserver<object>.OnNext(object value) => Parent.NotifyValue(value);
}
}
}

110
src/Avalonia.Styling/Styling/ActivatedSubject.cs

@ -1,110 +0,0 @@
// 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.Reactive.Subjects;
namespace Avalonia.Styling
{
/// <summary>
/// A subject which is switched on or off according to an activator observable.
/// </summary>
/// <remarks>
/// An <see cref="ActivatedSubject"/> extends <see cref="ActivatedObservable"/> to
/// be an <see cref="ISubject{Object}"/>. When the object is active then values
/// received via <see cref="OnNext(object)"/> will be passed to the source subject.
/// </remarks>
internal class ActivatedSubject : ActivatedObservable, ISubject<object>, IDescription
{
private bool _completed;
private object _pushValue;
/// <summary>
/// Initializes a new instance of the <see cref="ActivatedSubject"/> class.
/// </summary>
/// <param name="activator">The activator.</param>
/// <param name="source">An observable that produces the activated value.</param>
/// <param name="description">The binding description.</param>
public ActivatedSubject(
IObservable<bool> activator,
ISubject<object> source,
string description)
: base(activator, source, description)
{
}
/// <summary>
/// Gets the underlying subject.
/// </summary>
public new ISubject<object> Source
{
get { return (ISubject<object>)base.Source; }
}
public void OnCompleted()
{
Source.OnCompleted();
}
public void OnError(Exception error)
{
Source.OnError(error);
}
public void OnNext(object value)
{
_pushValue = value;
if (IsActive == true && !_completed)
{
Source.OnNext(_pushValue);
}
}
protected override void ActiveChanged(bool active)
{
bool first = !IsActive.HasValue;
base.ActiveChanged(active);
if (!first)
{
Source.OnNext(active ? _pushValue : AvaloniaProperty.UnsetValue);
}
}
protected override void CompletedReceived()
{
base.CompletedReceived();
if (!_completed)
{
Source.OnCompleted();
_completed = true;
}
}
protected override void ErrorReceived(Exception error)
{
base.ErrorReceived(error);
if (!_completed)
{
Source.OnError(error);
_completed = true;
}
}
private void ActivatorCompleted()
{
_completed = true;
Source.OnCompleted();
}
private void ActivatorError(Exception e)
{
_completed = true;
Source.OnError(e);
}
}
}

133
src/Avalonia.Styling/Styling/ActivatedValue.cs

@ -1,133 +0,0 @@
// 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 Avalonia.Reactive;
namespace Avalonia.Styling
{
/// <summary>
/// An value which is switched on or off according to an activator observable.
/// </summary>
/// <remarks>
/// An <see cref="ActivatedValue"/> has two inputs: an activator observable and an
/// <see cref="Value"/>. When the activator produces true, the
/// <see cref="ActivatedValue"/> will produce the current value. When the activator
/// produces false it will produce <see cref="AvaloniaProperty.UnsetValue"/>.
/// </remarks>
internal class ActivatedValue : LightweightObservableBase<object>, IDescription
{
private static readonly object NotSent = new object();
private IDisposable _activatorSubscription;
private object _value;
private object _last = NotSent;
/// <summary>
/// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
/// </summary>
/// <param name="activator">The activator.</param>
/// <param name="value">The activated value.</param>
/// <param name="description">The binding description.</param>
public ActivatedValue(
IObservable<bool> activator,
object value,
string description)
{
Contract.Requires<ArgumentNullException>(activator != null);
Activator = activator;
Value = value;
Description = description;
Listener = CreateListener();
}
/// <summary>
/// Gets the activator observable.
/// </summary>
public IObservable<bool> Activator { get; }
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets a value indicating whether the activator is active.
/// </summary>
public bool? IsActive { get; private set; }
/// <summary>
/// Gets the value that will be produced when <see cref="IsActive"/> is true.
/// </summary>
public object Value
{
get => _value;
protected set
{
_value = value;
PublishValue();
}
}
protected ActivatorListener Listener { get; }
protected virtual void ActiveChanged(bool active)
{
IsActive = active;
PublishValue();
}
protected virtual void CompletedReceived() => PublishCompleted();
protected virtual ActivatorListener CreateListener() => new ActivatorListener(this);
protected override void Deinitialize()
{
_activatorSubscription.Dispose();
_activatorSubscription = null;
}
protected virtual void ErrorReceived(Exception error) => PublishError(error);
protected override void Initialize()
{
_activatorSubscription = Activator.Subscribe(Listener);
}
protected override void Subscribed(IObserver<object> observer, bool first)
{
if (IsActive == true && !first)
{
observer.OnNext(Value);
}
}
private void PublishValue()
{
if (IsActive.HasValue)
{
var v = IsActive.Value ? Value : AvaloniaProperty.UnsetValue;
if (!Equals(v, _last))
{
PublishNext(v);
_last = v;
}
}
}
protected class ActivatorListener : IObserver<bool>
{
public ActivatorListener(ActivatedValue parent)
{
Parent = parent;
}
protected ActivatedValue Parent { get; }
void IObserver<bool>.OnCompleted() => Parent.CompletedReceived();
void IObserver<bool>.OnError(Exception error) => Parent.ErrorReceived(error);
void IObserver<bool>.OnNext(bool value) => Parent.ActiveChanged(value);
}
}
}

67
src/Avalonia.Styling/Styling/Activators/AndActivator.cs

@ -0,0 +1,67 @@
#nullable enable
using System.Collections.Generic;
namespace Avalonia.Styling.Activators
{
internal class AndActivator : StyleActivatorBase, IStyleActivatorSink
{
private List<IStyleActivator>? _sources;
private ulong _flags;
private ulong _mask;
public int Count => _sources?.Count ?? 0;
public void Add(IStyleActivator activator)
{
_sources ??= new List<IStyleActivator>();
_sources.Add(activator);
}
void IStyleActivatorSink.OnNext(bool value, int tag)
{
if (value)
{
_flags |= 1ul << tag;
}
else
{
_flags &= ~(1ul << tag);
}
if (_mask != 0)
{
PublishNext(_flags == _mask);
}
}
protected override void Initialize()
{
if (_sources is object)
{
var i = 0;
foreach (var source in _sources)
{
source.Subscribe(this, i++);
}
_mask = (1ul << Count) - 1;
PublishNext(_flags == _mask);
}
}
protected override void Deinitialize()
{
if (_sources is object)
{
foreach (var source in _sources)
{
source.Unsubscribe(this);
}
}
_mask = 0;
}
}
}

33
src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs

@ -0,0 +1,33 @@
#nullable enable
using System;
namespace Avalonia.Styling.Activators
{
/// <summary>
/// Defines a style activator.
/// </summary>
/// <remarks>
/// A style activator is very similar to an `IObservable{bool}` but is optimized for the
/// particular use-case of activating a style according to a selector. It differs from
/// an observable in two major ways:
///
/// - Can only have a single subscription
/// - The subscription can have a tag associated with it, allowing a subscriber to index
/// into a list of subscriptions without having to allocate additional objects.
/// </remarks>
public interface IStyleActivator : IDisposable
{
/// <summary>
/// Subscribes to the activator.
/// </summary>
/// <param name="sink">The listener.</param>
/// <param name="tag">An optional tag.</param>
void Subscribe(IStyleActivatorSink sink, int tag = 0);
/// <summary>
/// Unsubscribes from the activator.
/// </summary>
void Unsubscribe(IStyleActivatorSink sink);
}
}

17
src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs

@ -0,0 +1,17 @@
#nullable enable
namespace Avalonia.Styling.Activators
{
/// <summary>
/// Receives notifications from an <see cref="IStyleActivator"/>.
/// </summary>
public interface IStyleActivatorSink
{
/// <summary>
/// Called when the subscribed activator value changes.
/// </summary>
/// <param name="value">The new value.</param>
/// <param name="tag">The subscription tag.</param>
void OnNext(bool value, int tag);
}
}

13
src/Avalonia.Styling/Styling/Activators/NotActivator.cs

@ -0,0 +1,13 @@
#nullable enable
namespace Avalonia.Styling.Activators
{
internal class NotActivator : StyleActivatorBase, IStyleActivatorSink
{
private readonly IStyleActivator _source;
public NotActivator(IStyleActivator source) => _source = source;
void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value);
protected override void Initialize() => _source.Subscribe(this, 0);
protected override void Deinitialize() => _source.Unsubscribe(this);
}
}

67
src/Avalonia.Styling/Styling/Activators/OrActivator.cs

@ -0,0 +1,67 @@
#nullable enable
using System.Collections.Generic;
namespace Avalonia.Styling.Activators
{
internal class OrActivator : StyleActivatorBase, IStyleActivatorSink
{
private List<IStyleActivator>? _sources;
private ulong _flags;
private bool _initializing;
public int Count => _sources?.Count ?? 0;
public void Add(IStyleActivator activator)
{
_sources ??= new List<IStyleActivator>();
_sources.Add(activator);
}
void IStyleActivatorSink.OnNext(bool value, int tag)
{
if (value)
{
_flags |= 1ul << tag;
}
else
{
_flags &= ~(1ul << tag);
}
if (!_initializing)
{
PublishNext(_flags != 0);
}
}
protected override void Initialize()
{
if (_sources is object)
{
var i = 0;
_initializing = true;
foreach (var source in _sources)
{
source.Subscribe(this, i++);
}
_initializing = false;
PublishNext(_flags != 0);
}
}
protected override void Deinitialize()
{
if (_sources is object)
{
foreach (var source in _sources)
{
source.Unsubscribe(this);
}
}
}
}
}

35
src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs

@ -0,0 +1,35 @@
using System;
#nullable enable
namespace Avalonia.Styling.Activators
{
internal class PropertyEqualsActivator : StyleActivatorBase, IObserver<object>
{
private readonly IStyleable _control;
private readonly AvaloniaProperty _property;
private readonly object? _value;
private IDisposable? _subscription;
public PropertyEqualsActivator(
IStyleable control,
AvaloniaProperty property,
object? value)
{
_control = control ?? throw new ArgumentNullException(nameof(control));
_property = property ?? throw new ArgumentNullException(nameof(property));
_value = value;
}
protected override void Initialize()
{
_subscription = _control.GetObservable(_property).Subscribe(this);
}
protected override void Deinitialize() => _subscription?.Dispose();
void IObserver<object>.OnCompleted() { }
void IObserver<object>.OnError(Exception error) { }
void IObserver<object>.OnNext(object value) => PublishNext(Equals(value, _value));
}
}

55
src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs

@ -0,0 +1,55 @@
#nullable enable
namespace Avalonia.Styling.Activators
{
internal abstract class StyleActivatorBase : IStyleActivator
{
private IStyleActivatorSink? _sink;
private int _tag;
private bool? _value;
public void Subscribe(IStyleActivatorSink sink, int tag = 0)
{
if (_sink is null)
{
_sink = sink;
_tag = tag;
_value = null;
Initialize();
}
else
{
throw new AvaloniaInternalException("Cannot subscribe to a StyleActivator more than once.");
}
}
public void Unsubscribe(IStyleActivatorSink sink)
{
if (_sink != sink)
{
throw new AvaloniaInternalException("StyleActivatorSink is not subscribed.");
}
_sink = null;
Deinitialize();
}
public void PublishNext(bool value)
{
if (_value != value)
{
_value = value;
_sink?.OnNext(value, _tag);
}
}
public void Dispose()
{
_sink = null;
Deinitialize();
}
protected abstract void Initialize();
protected abstract void Deinitialize();
}
}

72
src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs

@ -0,0 +1,72 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
#nullable enable
namespace Avalonia.Styling.Activators
{
internal sealed class StyleClassActivator : StyleActivatorBase
{
private readonly IList<string> _match;
private readonly IAvaloniaReadOnlyList<string> _classes;
public StyleClassActivator(IAvaloniaReadOnlyList<string> classes, IList<string> match)
{
_classes = classes;
_match = match;
}
public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch)
{
int remainingMatches = toMatch.Count;
int classesCount = classes.Count;
// Early bail out - we can't match if control does not have enough classes.
if (classesCount < remainingMatches)
{
return false;
}
for (var i = 0; i < classesCount; i++)
{
var c = classes[i];
if (toMatch.Contains(c))
{
--remainingMatches;
// Already matched so we can skip checking other classes.
if (remainingMatches == 0)
{
break;
}
}
}
return remainingMatches == 0;
}
protected override void Initialize()
{
PublishNext(IsMatching());
_classes.CollectionChanged += ClassesChanged;
}
protected override void Deinitialize()
{
_classes.CollectionChanged -= ClassesChanged;
}
private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Move)
{
PublishNext(IsMatching());
}
}
private bool IsMatching() => AreClassesMatching(_classes, _match);
}
}

48
src/Avalonia.Styling/Styling/DescendentSelector.cs

@ -2,24 +2,21 @@
// 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.LogicalTree;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling
{
internal class DescendantSelector : Selector
{
private readonly Selector _parent;
private string _selectorString;
private string? _selectorString;
public DescendantSelector(Selector parent)
public DescendantSelector(Selector? parent)
{
if (parent == null)
{
throw new InvalidOperationException("Descendant selector must be preceeded by a selector.");
}
_parent = parent;
_parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceeded by a selector.");
}
/// <inheritdoc/>
@ -29,7 +26,7 @@ namespace Avalonia.Styling
public override bool InTemplate => _parent.InTemplate;
/// <inheritdoc/>
public override Type TargetType => null;
public override Type? TargetType => null;
public override string ToString()
{
@ -43,8 +40,9 @@ namespace Avalonia.Styling
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
{
ILogical c = (ILogical)control;
List<IObservable<bool>> descendantMatches = new List<IObservable<bool>>();
var c = (ILogical)control;
IStyleActivator? descendentMatch = null;
OrActivator? descendantMatches = null;
while (c != null)
{
@ -56,7 +54,21 @@ namespace Avalonia.Styling
if (match.Result == SelectorMatchResult.Sometimes)
{
descendantMatches.Add(match.Activator);
if (descendentMatch is null && descendantMatches is null)
{
descendentMatch = match.Activator;
}
else
{
if (descendantMatches is null)
{
descendantMatches = new OrActivator();
descendantMatches.Add(descendentMatch!);
descendentMatch = null;
}
descendantMatches.Add(match.Activator!);
}
}
else if (match.IsMatch)
{
@ -65,9 +77,13 @@ namespace Avalonia.Styling
}
}
if (descendantMatches.Count > 0)
if (descendantMatches is object)
{
return new SelectorMatch(descendantMatches);
}
else if (descendentMatch is object)
{
return new SelectorMatch(StyleActivator.Or(descendantMatches));
return new SelectorMatch(descendentMatch);
}
else
{
@ -75,6 +91,6 @@ namespace Avalonia.Styling
}
}
protected override Selector MovePrevious() => null;
protected override Selector? MovePrevious() => null;
}
}

19
src/Avalonia.Styling/Styling/ISetter.cs

@ -3,6 +3,8 @@
using System;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
@ -11,11 +13,16 @@ namespace Avalonia.Styling
public interface ISetter
{
/// <summary>
/// Applies the setter to a control.
/// Instances a setter on a control.
/// </summary>
/// <param name="style">The style that is being applied.</param>
/// <param name="control">The control.</param>
/// <param name="activator">An optional activator.</param>
IDisposable Apply(IStyle style, IStyleable control, IObservable<bool> activator);
/// <param name="target">The control.</param>
/// <param name="hasActivator">Whether the parent style has an activator.</param>
/// <returns>An <see cref="ISetterInstance"/>.</returns>
/// <remarks>
/// This method should return an <see cref="ISetterInstance"/> which can be used to apply
/// the setter to the specified control. Note that it should not apply the setter value
/// until <see cref="ISetterInstance.Activate"/> is called.
/// </remarks>
ISetterInstance Instance(IStyleable target, bool hasActivator);
}
}
}

20
src/Avalonia.Styling/Styling/ISetterInstance.cs

@ -0,0 +1,20 @@
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
/// Represents a setter that has been instanced on a control.
/// </summary>
public interface ISetterInstance
{
/// <summary>
/// Activates the setter.
/// </summary>
public void Activate();
/// <summary>
/// Deactivates the setter.
/// </summary>
public void Deactivate();
}
}

16
src/Avalonia.Styling/Styling/IStyle.cs

@ -3,6 +3,8 @@
using Avalonia.Controls;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
@ -13,17 +15,11 @@ namespace Avalonia.Styling
/// <summary>
/// Attaches the style to a control if the style's selector matches.
/// </summary>
/// <param name="control">The control to attach to.</param>
/// <param name="container">
/// The control that contains this style. May be null.
/// </param>
/// <param name="target">The control to attach to.</param>
/// <param name="host">The element that hosts the style.</param>
/// <returns>
/// True if the style can match a control of type <paramref name="control"/>
/// (even if it does not match this control specifically); false if the style
/// can never match.
/// A <see cref="SelectorMatchResult"/> describing how the style matches the control.
/// </returns>
bool Attach(IStyleable control, IStyleHost container);
void Detach();
SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host);
}
}

22
src/Avalonia.Styling/Styling/IStyleInstance.cs

@ -0,0 +1,22 @@
using System;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
/// Represents a style that has been instanced on a control.
/// </summary>
public interface IStyleInstance : IDisposable
{
/// <summary>
/// Gets the source style.
/// </summary>
IStyle Source { get; }
/// <summary>
/// Instructs the style to start acting upon the control.
/// </summary>
void Start();
}
}

20
src/Avalonia.Styling/Styling/IStyleable.cs

@ -4,6 +4,8 @@
using System;
using Avalonia.Collections;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
@ -11,11 +13,6 @@ namespace Avalonia.Styling
/// </summary>
public interface IStyleable : IAvaloniaObject, INamed
{
/// <summary>
/// Signaled when the control's style should be removed.
/// </summary>
IObservable<IStyleable> StyleDetach { get; }
/// <summary>
/// Gets the list of classes for the control.
/// </summary>
@ -29,6 +26,17 @@ namespace Avalonia.Styling
/// <summary>
/// Gets the template parent of this element if the control comes from a template.
/// </summary>
ITemplatedControl TemplatedParent { get; }
ITemplatedControl? TemplatedParent { get; }
/// <summary>
/// Notifies the element that a style has been applied.
/// </summary>
/// <param name="instance">The style instance.</param>
void StyleApplied(IStyleInstance instance);
/// <summary>
/// Detaches all styles applied to the element.
/// </summary>
void DetachStyles();
}
}

16
src/Avalonia.Styling/Styling/NotSelector.cs

@ -2,7 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Linq;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling
{
@ -11,16 +13,16 @@ namespace Avalonia.Styling
/// </summary>
internal class NotSelector : Selector
{
private readonly Selector _previous;
private readonly Selector? _previous;
private readonly Selector _argument;
private string _selectorString;
private string? _selectorString;
/// <summary>
/// Initializes a new instance of the <see cref="NotSelector"/> class.
/// </summary>
/// <param name="previous">The previous selector.</param>
/// <param name="argument">The selector to be not-ed.</param>
public NotSelector(Selector previous, Selector argument)
public NotSelector(Selector? previous, Selector argument)
{
_previous = previous;
_argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument.");
@ -33,7 +35,7 @@ namespace Avalonia.Styling
public override bool IsCombinator => false;
/// <inheritdoc/>
public override Type TargetType => _previous?.TargetType;
public override Type? TargetType => _previous?.TargetType;
/// <inheritdoc/>
public override string ToString()
@ -61,12 +63,12 @@ namespace Avalonia.Styling
case SelectorMatchResult.NeverThisType:
return SelectorMatch.AlwaysThisType;
case SelectorMatchResult.Sometimes:
return new SelectorMatch(innerResult.Activator.Select(x => !x));
return new SelectorMatch(new NotActivator(innerResult.Activator!));
default:
throw new InvalidOperationException("Invalid SelectorMatchResult.");
}
}
protected override Selector MovePrevious() => _previous;
protected override Selector? MovePrevious() => _previous;
}
}

53
src/Avalonia.Styling/Styling/OrSelector.cs

@ -3,6 +3,9 @@
using System;
using System.Collections.Generic;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling
{
@ -12,8 +15,8 @@ namespace Avalonia.Styling
internal class OrSelector : Selector
{
private readonly IReadOnlyList<Selector> _selectors;
private string _selectorString;
private Type _targetType;
private string? _selectorString;
private Type? _targetType;
/// <summary>
/// Initializes a new instance of the <see cref="OrSelector"/> class.
@ -21,8 +24,15 @@ namespace Avalonia.Styling
/// <param name="selectors">The selectors to OR.</param>
public OrSelector(IReadOnlyList<Selector> selectors)
{
Contract.Requires<ArgumentNullException>(selectors != null);
Contract.Requires<ArgumentException>(selectors.Count > 1);
if (selectors is null)
{
throw new ArgumentNullException(nameof(selectors));
}
if (selectors.Count <= 1)
{
throw new ArgumentException("Need more than one selector to OR.");
}
_selectors = selectors;
}
@ -34,7 +44,7 @@ namespace Avalonia.Styling
public override bool IsCombinator => false;
/// <inheritdoc/>
public override Type TargetType
public override Type? TargetType
{
get
{
@ -60,7 +70,8 @@ namespace Avalonia.Styling
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
{
var activators = new List<IObservable<bool>>();
IStyleActivator? activator = null;
OrActivator? activators = null;
var neverThisInstance = false;
foreach (var selector in _selectors)
@ -76,18 +87,32 @@ namespace Avalonia.Styling
neverThisInstance = true;
break;
case SelectorMatchResult.Sometimes:
activators.Add(match.Activator);
if (activator is null && activators is null)
{
activator = match.Activator;
}
else
{
if (activators is null)
{
activators = new OrActivator();
activators.Add(activator!);
activator = null;
}
activators.Add(match.Activator!);
}
break;
}
}
if (activators.Count > 1)
if (activators is object)
{
return new SelectorMatch(StyleActivator.Or(activators));
return new SelectorMatch(activators);
}
else if (activators.Count == 1)
else if (activator is object)
{
return new SelectorMatch(activators[0]);
return new SelectorMatch(activator);
}
else if (neverThisInstance)
{
@ -99,11 +124,11 @@ namespace Avalonia.Styling
}
}
protected override Selector MovePrevious() => null;
protected override Selector? MovePrevious() => null;
private Type EvaluateTargetType()
private Type? EvaluateTargetType()
{
var result = default(Type);
Type? result = null;
foreach (var selector in _selectors)
{

25
src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs

@ -2,8 +2,10 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling
{
@ -13,14 +15,14 @@ namespace Avalonia.Styling
/// </summary>
internal class PropertyEqualsSelector : Selector
{
private readonly Selector _previous;
private readonly Selector? _previous;
private readonly AvaloniaProperty _property;
private readonly object _value;
private string _selectorString;
private readonly object? _value;
private string? _selectorString;
public PropertyEqualsSelector(Selector previous, AvaloniaProperty property, object value)
public PropertyEqualsSelector(Selector? previous, AvaloniaProperty property, object? value)
{
Contract.Requires<ArgumentNullException>(property != null);
property = property ?? throw new ArgumentNullException(nameof(property));
_previous = previous;
_property = property;
@ -33,13 +35,8 @@ namespace Avalonia.Styling
/// <inheritdoc/>
public override bool IsCombinator => false;
/// <summary>
/// Gets the name of the control to match.
/// </summary>
public string Name { get; private set; }
/// <inheritdoc/>
public override Type TargetType => _previous?.TargetType;
public override Type? TargetType => _previous?.TargetType;
/// <inheritdoc/>
public override string ToString()
@ -77,7 +74,7 @@ namespace Avalonia.Styling
{
if (subscribe)
{
return new SelectorMatch(control.GetObservable(_property).Select(v => Equals(v ?? string.Empty, _value)));
return new SelectorMatch(new PropertyEqualsActivator(control, _property, _value));
}
else
{
@ -86,6 +83,6 @@ namespace Avalonia.Styling
}
}
protected override Selector MovePrevious() => _previous;
protected override Selector? MovePrevious() => _previous;
}
}

48
src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs

@ -0,0 +1,48 @@
using System;
using Avalonia.Data;
#nullable enable
namespace Avalonia.Styling
{
internal class PropertySetterBindingInstance : ISetterInstance
{
private readonly IStyleable _target;
private readonly AvaloniaProperty _property;
private readonly BindingPriority _priority;
private readonly InstancedBinding _binding;
private IDisposable? _subscription;
private bool _isActive;
public PropertySetterBindingInstance(
IStyleable target,
AvaloniaProperty property,
BindingPriority priority,
IBinding binding)
{
_target = target;
_property = property;
_priority = priority;
_binding = binding.Initiate(target, property).WithPriority(priority);
}
public void Activate()
{
if (!_isActive)
{
_subscription = BindingOperations.Apply(_target, _property, _binding, null);
_isActive = true;
}
}
public void Deactivate()
{
if (_isActive)
{
_subscription?.Dispose();
_subscription = null;
_isActive = false;
}
}
}
}

82
src/Avalonia.Styling/Styling/PropertySetterInstance.cs

@ -0,0 +1,82 @@
using System;
using Avalonia.Data;
#nullable enable
namespace Avalonia.Styling
{
internal class PropertySetterInstance<T> : ISetterInstance
{
private readonly IStyleable _target;
private readonly StyledPropertyBase<T>? _styledProperty;
private readonly DirectPropertyBase<T>? _directProperty;
private readonly BindingPriority _priority;
private readonly T _value;
private IDisposable? _subscription;
private bool _isActive;
public PropertySetterInstance(
IStyleable target,
StyledPropertyBase<T> property,
BindingPriority priority,
T value)
{
_target = target;
_styledProperty = property;
_priority = priority;
_value = value;
}
public PropertySetterInstance(
IStyleable target,
DirectPropertyBase<T> property,
BindingPriority priority,
T value)
{
_target = target;
_directProperty = property;
_priority = priority;
_value = value;
}
public void Activate()
{
if (!_isActive)
{
if (_styledProperty is object)
{
_subscription = _target.SetValue(_styledProperty, _value, _priority);
}
else
{
_target.SetValue(_directProperty!, _value);
}
_isActive = true;
}
}
public void Deactivate()
{
if (_isActive)
{
if (_subscription is null)
{
if (_styledProperty is object)
{
_target.ClearValue(_styledProperty);
}
else
{
_target.ClearValue(_directProperty!);
}
}
else
{
_subscription.Dispose();
_subscription = null;
}
}
}
}
}

44
src/Avalonia.Styling/Styling/Selector.cs

@ -2,9 +2,9 @@
// 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.Diagnostics;
using Avalonia.Utilities;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling
{
@ -30,7 +30,7 @@ namespace Avalonia.Styling
/// <summary>
/// Gets the target type of the selector, if available.
/// </summary>
public abstract Type TargetType { get; }
public abstract Type? TargetType { get; }
/// <summary>
/// Tries to match the selector with a control.
@ -43,8 +43,8 @@ namespace Avalonia.Styling
/// <returns>A <see cref="SelectorMatch"/>.</returns>
public SelectorMatch Match(IStyleable control, bool subscribe = true)
{
ValueSingleOrList<IObservable<bool>> inputs = default;
IStyleActivator? activator = null;
AndActivator? activators = null;
var selector = this;
var alwaysThisType = true;
var hitCombinator = false;
@ -69,21 +69,39 @@ namespace Avalonia.Styling
}
else if (match.Result == SelectorMatchResult.Sometimes)
{
Debug.Assert(match.Activator != null);
if (match.Activator is null)
{
throw new AvaloniaInternalException(
"SelectorMatch returned Sometimes but there is no activator.");
}
if (activator is null && activators is null)
{
activator = match.Activator;
}
else
{
if (activators is null)
{
activators = new AndActivator();
activators.Add(activator!);
activator = null;
}
inputs.Add(match.Activator);
activators.Add(match.Activator);
}
}
selector = selector.MovePrevious();
}
if (inputs.HasList)
if (activators is object)
{
return new SelectorMatch(StyleActivator.And(inputs.List));
return new SelectorMatch(activators);
}
else if (inputs.IsSingle)
else if (activator is object)
{
return new SelectorMatch(inputs.Single);
return new SelectorMatch(activator);
}
else
{
@ -107,6 +125,6 @@ namespace Avalonia.Styling
/// <summary>
/// Moves to the previous selector.
/// </summary>
protected abstract Selector MovePrevious();
protected abstract Selector? MovePrevious();
}
}

29
src/Avalonia.Styling/Styling/SelectorMatch.cs

@ -2,6 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling
{
@ -21,9 +24,9 @@ namespace Avalonia.Styling
NeverThisInstance,
/// <summary>
/// The selector always matches this type.
/// The selector matches this instance based on the <see cref="SelectorMatch.Activator"/>.
/// </summary>
AlwaysThisType,
Sometimes,
/// <summary>
/// The selector always matches this instance, but doesn't always match this type.
@ -31,9 +34,9 @@ namespace Avalonia.Styling
AlwaysThisInstance,
/// <summary>
/// The selector matches this instance based on the <see cref="SelectorMatch.Activator"/>.
/// The selector always matches this type.
/// </summary>
Sometimes,
AlwaysThisType,
}
/// <summary>
@ -43,7 +46,7 @@ namespace Avalonia.Styling
/// A selector match describes whether and how a <see cref="Selector"/> matches a control, and
/// in addition whether the selector can ever match a control of the same type.
/// </remarks>
public class SelectorMatch
public readonly struct SelectorMatch
{
/// <summary>
/// A selector match with the result of <see cref="SelectorMatchResult.NeverThisType"/>.
@ -70,20 +73,24 @@ namespace Avalonia.Styling
/// <see cref="SelectorMatchResult.Sometimes"/> result.
/// </summary>
/// <param name="match">The match activator.</param>
public SelectorMatch(IObservable<bool> match)
public SelectorMatch(IStyleActivator match)
{
Contract.Requires<ArgumentNullException>(match != null);
match = match ?? throw new ArgumentNullException(nameof(match));
Result = SelectorMatchResult.Sometimes;
Activator = match;
}
private SelectorMatch(SelectorMatchResult result) => Result = result;
private SelectorMatch(SelectorMatchResult result)
{
Result = result;
Activator = null;
}
/// <summary>
/// Gets a value indicating whether the match was positive.
/// </summary>
public bool IsMatch => Result >= SelectorMatchResult.AlwaysThisType;
public bool IsMatch => Result >= SelectorMatchResult.Sometimes;
/// <summary>
/// Gets the result of the match.
@ -91,9 +98,9 @@ namespace Avalonia.Styling
public SelectorMatchResult Result { get; }
/// <summary>
/// Gets an observable which tracks the selector match, in the case of selectors that can
/// Gets an activator which tracks the selector match, in the case of selectors that can
/// change over time.
/// </summary>
public IObservable<bool> Activator { get; }
public IStyleActivator? Activator { get; }
}
}

141
src/Avalonia.Styling/Styling/Setter.cs

@ -2,11 +2,12 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Disposables;
using Avalonia.Animation;
using Avalonia.Data;
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.Utilities;
#nullable enable
namespace Avalonia.Styling
{
@ -17,9 +18,9 @@ namespace Avalonia.Styling
/// A <see cref="Setter"/> is used to set a <see cref="AvaloniaProperty"/> value on a
/// <see cref="AvaloniaObject"/> depending on a condition.
/// </remarks>
public class Setter : ISetter, IAnimationSetter
public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor<Setter.SetterVisitorData>
{
private object _value;
private object? _value;
/// <summary>
/// Initializes a new instance of the <see cref="Setter"/> class.
@ -42,11 +43,7 @@ namespace Avalonia.Styling
/// <summary>
/// Gets or sets the property to set.
/// </summary>
public AvaloniaProperty Property
{
get;
set;
}
public AvaloniaProperty? Property { get; set; }
/// <summary>
/// Gets or sets the property value.
@ -54,13 +51,9 @@ namespace Avalonia.Styling
[Content]
[AssignBinding]
[DependsOn(nameof(Property))]
public object Value
public object? Value
{
get
{
return _value;
}
get => _value;
set
{
(value as ISetterValue)?.Initialize(this);
@ -68,99 +61,71 @@ namespace Avalonia.Styling
}
}
/// <summary>
/// Applies the setter to a control.
/// </summary>
/// <param name="style">The style that is being applied.</param>
/// <param name="control">The control.</param>
/// <param name="activator">An optional activator.</param>
public IDisposable Apply(IStyle style, IStyleable control, IObservable<bool> activator)
public ISetterInstance Instance(IStyleable target, bool hasActivator)
{
Contract.Requires<ArgumentNullException>(control != null);
target = target ?? throw new ArgumentNullException(nameof(target));
if (Property == null)
if (Property is null)
{
throw new InvalidOperationException("Setter.Property must be set.");
}
var value = Value;
var binding = value as IBinding;
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style;
if (binding == null)
if (Value is IBinding binding)
{
if (value is ITemplate template)
{
bool isPropertyOfTypeITemplate = typeof(ITemplate).IsAssignableFrom(Property.PropertyType);
if (!isPropertyOfTypeITemplate)
{
var materialized = template.Build();
value = materialized;
}
}
if (activator == null)
{
return control.Bind(Property, ObservableEx.SingleValue(value), BindingPriority.Style);
}
else
{
var description = style?.ToString();
var activated = new ActivatedValue(activator, value, description);
return control.Bind(Property, activated, BindingPriority.StyleTrigger);
}
return new PropertySetterBindingInstance(target, Property, priority, binding);
}
else
{
var source = binding.Initiate(control, Property);
var value = Value;
if (source != null)
if (value is ITemplate template &&
!typeof(ITemplate).IsAssignableFrom(Property.PropertyType))
{
var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator);
return BindingOperations.Apply(control, Property, cloned, null);
value = template.Build();
}
}
return Disposable.Empty;
var data = new SetterVisitorData
{
target = target,
priority = priority,
value = value,
};
Property.Accept(this, ref data);
return data.result!;
}
}
private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable<bool> activator)
void IAvaloniaPropertyVisitor<SetterVisitorData>.Visit<T>(
StyledPropertyBase<T> property,
ref SetterVisitorData data)
{
if (activator != null)
{
var description = style?.ToString();
data.result = new PropertySetterInstance<T>(
data.target,
property,
data.priority,
(T)data.value);
}
switch (mode)
{
case BindingMode.OneTime:
if (sourceInstance.Observable != null)
{
var activated = new ActivatedObservable(activator, sourceInstance.Observable, description);
return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger);
}
else
{
var activated = new ActivatedValue(activator, sourceInstance.Value, description);
return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger);
}
case BindingMode.OneWay:
{
var activated = new ActivatedObservable(activator, sourceInstance.Observable, description);
return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger);
}
default:
{
var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger);
}
}
void IAvaloniaPropertyVisitor<SetterVisitorData>.Visit<T>(
DirectPropertyBase<T> property,
ref SetterVisitorData data)
{
data.result = new PropertySetterInstance<T>(
data.target,
property,
data.priority,
(T)data.value);
}
}
else
{
return sourceInstance.WithPriority(BindingPriority.Style);
}
private struct SetterVisitorData
{
public IStyleable target;
public BindingPriority priority;
public object? value;
public ISetterInstance? result;
}
}
}

165
src/Avalonia.Styling/Styling/Style.cs

@ -3,12 +3,12 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
@ -16,15 +16,10 @@ namespace Avalonia.Styling
/// </summary>
public class Style : AvaloniaObject, IStyle, ISetResourceParent
{
private static Dictionary<IStyleable, CompositeDisposable> _applied =
new Dictionary<IStyleable, CompositeDisposable>();
private IResourceNode _parent;
private CompositeDisposable _subscriptions;
private IResourceDictionary _resources;
private IList<IAnimation> _animations;
private IResourceNode? _parent;
private IResourceDictionary? _resources;
private List<ISetter>? _setters;
private List<IAnimation>? _animations;
/// <summary>
/// Initializes a new instance of the <see cref="Style"/> class.
@ -37,13 +32,13 @@ namespace Avalonia.Styling
/// Initializes a new instance of the <see cref="Style"/> class.
/// </summary>
/// <param name="selector">The style selector.</param>
public Style(Func<Selector, Selector> selector)
public Style(Func<Selector?, Selector> selector)
{
Selector = selector(null);
}
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
/// <summary>
/// Gets or sets a dictionary of style resources.
@ -53,7 +48,7 @@ namespace Avalonia.Styling
get => _resources ?? (Resources = new ResourceDictionary());
set
{
Contract.Requires<ArgumentNullException>(value != null);
value = value ?? throw new ArgumentNullException(nameof(value));
var hadResources = false;
@ -76,117 +71,45 @@ namespace Avalonia.Styling
/// <summary>
/// Gets or sets the style's selector.
/// </summary>
public Selector Selector { get; set; }
public Selector? Selector { get; set; }
/// <summary>
/// Gets or sets the style's setters.
/// Gets the style's setters.
/// </summary>
[Content]
public IList<ISetter> Setters { get; set; } = new List<ISetter>();
public IList<ISetter> Setters => _setters ??= new List<ISetter>();
public IList<IAnimation> Animations
{
get
{
return _animations ?? (_animations = new List<IAnimation>());
}
}
private CompositeDisposable Subscriptions
{
get
{
return _subscriptions ?? (_subscriptions = new CompositeDisposable(2));
}
}
/// <summary>
/// Gets the style's animations.
/// </summary>
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => _parent;
IResourceNode? IResourceNode.ResourceParent => _parent;
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
/// <inheritdoc/>
public bool Attach(IStyleable control, IStyleHost container)
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{
if (Selector != null)
{
var match = Selector.Match(control);
if (match.IsMatch)
{
var controlSubscriptions = GetSubscriptions(control);
var animatable = control as Animatable;
var setters = Setters;
var settersCount = setters.Count;
var animations = Animations;
var animationsCount = animations.Count;
var subs = new CompositeDisposable(settersCount + (animatable != null ? animationsCount : 0) + 1);
if (animatable != null)
{
for (var i = 0; i < animationsCount; i++)
{
var animation = animations[i];
var obsMatch = match.Activator;
if (match.Result == SelectorMatchResult.AlwaysThisType ||
match.Result == SelectorMatchResult.AlwaysThisInstance)
{
obsMatch = Observable.Return(true);
}
var sub = animation.Apply(animatable, null, obsMatch);
subs.Add(sub);
}
}
for (var i = 0; i < settersCount; i++)
{
var setter = setters[i];
var sub = setter.Apply(this, control, match.Activator);
subs.Add(sub);
}
target = target ?? throw new ArgumentNullException(nameof(target));
subs.Add(Disposable.Create((subs, Subscriptions) , state => state.Subscriptions.Remove(state.subs)));
var match = Selector is object ? Selector.Match(target) :
target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
controlSubscriptions.Add(subs);
Subscriptions.Add(subs);
}
return match.Result != SelectorMatchResult.NeverThisType;
}
else if (control == container)
if (match.IsMatch && _setters is object)
{
var setters = Setters;
var settersCount = setters.Count;
var controlSubscriptions = GetSubscriptions(control);
var subs = new CompositeDisposable(settersCount + 1);
for (var i = 0; i < settersCount; i++)
{
var setter = setters[i];
var sub = setter.Apply(this, control, null);
subs.Add(sub);
}
subs.Add(Disposable.Create((subs, Subscriptions), state => state.Subscriptions.Remove(state.subs)));
controlSubscriptions.Add(subs);
Subscriptions.Add(subs);
return true;
var instance = new StyleInstance(this, target, _setters, match.Activator);
target.StyleApplied(instance);
instance.Start();
}
return false;
return match.Result;
}
/// <inheritdoc/>
public bool TryGetResource(object key, out object result)
public bool TryGetResource(object key, out object? result)
{
result = null;
return _resources?.TryGetResource(key, out result) ?? false;
@ -224,44 +147,12 @@ namespace Avalonia.Styling
if (parent == null)
{
Detach();
//Detach();
}
_parent = parent;
}
public void Detach()
{
_subscriptions?.Dispose();
_subscriptions = null;
}
private static CompositeDisposable GetSubscriptions(IStyleable control)
{
if (!_applied.TryGetValue(control, out var subscriptions))
{
subscriptions = new CompositeDisposable(3);
subscriptions.Add(control.StyleDetach.Subscribe(ControlDetach));
_applied.Add(control, subscriptions);
}
return subscriptions;
}
/// <summary>
/// Called when a control's <see cref="IStyleable.StyleDetach"/> is signaled to remove
/// all applied styles.
/// </summary>
/// <param name="control">The control.</param>
private static void ControlDetach(IStyleable control)
{
var subscriptions = _applied[control];
subscriptions.Dispose();
_applied.Remove(control);
}
private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, e);

56
src/Avalonia.Styling/Styling/StyleActivator.cs

@ -1,56 +0,0 @@
// 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;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
namespace Avalonia.Styling
{
public enum ActivatorMode
{
And,
Or,
}
public static class StyleActivator
{
public static IObservable<bool> And(IList<IObservable<bool>> inputs)
{
if (inputs.Count == 0)
{
throw new ArgumentException("StyleActivator.And inputs may not be empty.");
}
else if (inputs.Count == 1)
{
return inputs[0];
}
else
{
return inputs.CombineLatest()
.Select(values => values.All(x => x))
.DistinctUntilChanged();
}
}
public static IObservable<bool> Or(IList<IObservable<bool>> inputs)
{
if (inputs.Count == 0)
{
throw new ArgumentException("StyleActivator.Or inputs may not be empty.");
}
else if (inputs.Count == 1)
{
return inputs[0];
}
else
{
return inputs.CombineLatest()
.Select(values => values.Any(x => x))
.DistinctUntilChanged();
}
}
}
}

81
src/Avalonia.Styling/Styling/StyleInstance.cs

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling
{
internal class StyleInstance : IStyleInstance, IStyleActivatorSink
{
private readonly List<ISetterInstance> _setters;
private readonly IStyleActivator? _activator;
private bool _active;
public StyleInstance(
IStyle source,
IStyleable target,
IReadOnlyList<ISetter> setters,
IStyleActivator? activator = null)
{
setters = setters ?? throw new ArgumentNullException(nameof(setters));
Source = source ?? throw new ArgumentNullException(nameof(source));
Target = target ?? throw new ArgumentNullException(nameof(target));
_setters = new List<ISetterInstance>(setters.Count);
_activator = activator;
foreach (var setter in setters)
{
_setters.Add(setter.Instance(target, activator is object));
}
}
public IStyle Source { get; }
public IStyleable Target { get; }
public void Start()
{
if (_activator == null)
{
ActivatorChanged(true);
}
else
{
_activator.Subscribe(this, 0);
}
}
public void Dispose()
{
ActivatorChanged(false);
_activator?.Dispose();
}
private void ActivatorChanged(bool value)
{
if (_active != value)
{
_active = value;
if (_active)
{
foreach (var setter in _setters)
{
setter.Activate();
}
}
else
{
foreach (var setter in _setters)
{
setter.Deactivate();
}
}
}
}
void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value);
}
}

25
src/Avalonia.Styling/Styling/Styler.cs

@ -3,35 +3,34 @@
using System;
#nullable enable
namespace Avalonia.Styling
{
public class Styler : IStyler
{
public void ApplyStyles(IStyleable control)
public void ApplyStyles(IStyleable target)
{
var styleHost = control as IStyleHost;
target = target ?? throw new ArgumentNullException(nameof(target));
if (styleHost != null)
if (target is IStyleHost styleHost)
{
ApplyStyles(control, styleHost);
ApplyStyles(target, styleHost);
}
}
private void ApplyStyles(IStyleable control, IStyleHost styleHost)
private void ApplyStyles(IStyleable target, IStyleHost host)
{
Contract.Requires<ArgumentNullException>(control != null);
Contract.Requires<ArgumentNullException>(styleHost != null);
var parentContainer = styleHost.StylingParent;
var parent = host.StylingParent;
if (parentContainer != null)
if (parent != null)
{
ApplyStyles(control, parentContainer);
ApplyStyles(target, parent);
}
if (styleHost.IsStylesInitialized)
if (host.IsStylesInitialized)
{
styleHost.Styles.Attach(control, styleHost);
host.Styles.TryAttach(target, host);
}
}
}

82
src/Avalonia.Styling/Styling/Styles.cs

@ -9,6 +9,8 @@ using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
@ -16,10 +18,10 @@ namespace Avalonia.Styling
/// </summary>
public class Styles : AvaloniaObject, IAvaloniaList<IStyle>, IStyle, ISetResourceParent
{
private IResourceNode _parent;
private IResourceDictionary _resources;
private AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
private Dictionary<Type, List<IStyle>> _cache;
private readonly AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
private IResourceNode? _parent;
private IResourceDictionary? _resources;
private Dictionary<Type, List<IStyle>?>? _cache;
public Styles()
{
@ -60,6 +62,12 @@ namespace Avalonia.Styling
() => { });
}
public Styles(IResourceNode parent)
: this()
{
_parent = parent;
}
public event NotifyCollectionChangedEventHandler CollectionChanged
{
add => _styles.CollectionChanged += value;
@ -67,7 +75,7 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
/// <inheritdoc/>
public int Count => _styles.Count;
@ -83,7 +91,7 @@ namespace Avalonia.Styling
get => _resources ?? (Resources = new ResourceDictionary());
set
{
Contract.Requires<ArgumentNullException>(value != null);
value = value ?? throw new ArgumentNullException(nameof(Resources));
var hadResources = false;
@ -104,7 +112,7 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => _parent;
IResourceNode? IResourceNode.ResourceParent => _parent;
/// <inheritdoc/>
bool ICollection<IStyle>.IsReadOnly => false;
@ -119,66 +127,50 @@ namespace Avalonia.Styling
set => _styles[index] = value;
}
/// <summary>
/// Attaches the style to a control if the style's selector matches.
/// </summary>
/// <param name="control">The control to attach to.</param>
/// <param name="container">
/// The control that contains this style. May be null.
/// </param>
public bool Attach(IStyleable control, IStyleHost container)
/// <inheritdoc/>
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{
if (_cache == null)
{
_cache = new Dictionary<Type, List<IStyle>>();
}
_cache ??= new Dictionary<Type, List<IStyle>?>();
if (_cache.TryGetValue(control.StyleKey, out var cached))
if (_cache.TryGetValue(target.StyleKey, out var cached))
{
if (cached != null)
if (cached is object)
{
foreach (var style in cached)
{
style.Attach(control, container);
style.TryAttach(target, host);
}
return true;
return SelectorMatchResult.AlwaysThisType;
}
else
{
return SelectorMatchResult.NeverThisType;
}
return false;
}
else
{
List<IStyle> result = null;
List<IStyle>? matches = null;
foreach (var style in this)
foreach (var child in this)
{
if (style.Attach(control, container))
if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType)
{
if (result == null)
{
result = new List<IStyle>();
}
result.Add(style);
matches ??= new List<IStyle>();
matches.Add(child);
}
}
_cache.Add(control.StyleKey, result);
return result != null;
}
}
public void Detach()
{
foreach (IStyle style in this)
{
style.Detach();
_cache.Add(target.StyleKey, matches);
return matches is null ?
SelectorMatchResult.NeverThisType :
SelectorMatchResult.AlwaysThisType;
}
}
/// <inheritdoc/>
public bool TryGetResource(object key, out object value)
public bool TryGetResource(object key, out object? value)
{
if (_resources != null && _resources.TryGetResource(key, out value))
{

109
src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs

@ -3,11 +3,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reflection;
using System.Text;
using Avalonia.Collections;
using Avalonia.Reactive;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling
{
@ -17,13 +16,12 @@ namespace Avalonia.Styling
/// </summary>
internal class TypeNameAndClassSelector : Selector
{
private readonly Selector _previous;
private readonly Selector? _previous;
private readonly Lazy<List<string>> _classes = new Lazy<List<string>>(() => new List<string>());
private Type _targetType;
private string _selectorString;
private Type? _targetType;
private string? _selectorString;
public static TypeNameAndClassSelector OfType(Selector previous, Type targetType)
public static TypeNameAndClassSelector OfType(Selector? previous, Type targetType)
{
var result = new TypeNameAndClassSelector(previous);
result._targetType = targetType;
@ -32,7 +30,7 @@ namespace Avalonia.Styling
return result;
}
public static TypeNameAndClassSelector Is(Selector previous, Type targetType)
public static TypeNameAndClassSelector Is(Selector? previous, Type targetType)
{
var result = new TypeNameAndClassSelector(previous);
result._targetType = targetType;
@ -41,7 +39,7 @@ namespace Avalonia.Styling
return result;
}
public static TypeNameAndClassSelector ForName(Selector previous, string name)
public static TypeNameAndClassSelector ForName(Selector? previous, string name)
{
var result = new TypeNameAndClassSelector(previous);
result.Name = name;
@ -49,7 +47,7 @@ namespace Avalonia.Styling
return result;
}
public static TypeNameAndClassSelector ForClass(Selector previous, string className)
public static TypeNameAndClassSelector ForClass(Selector? previous, string className)
{
var result = new TypeNameAndClassSelector(previous);
result.Classes.Add(className);
@ -57,7 +55,7 @@ namespace Avalonia.Styling
return result;
}
protected TypeNameAndClassSelector(Selector previous)
protected TypeNameAndClassSelector(Selector? previous)
{
_previous = previous;
}
@ -68,10 +66,10 @@ namespace Avalonia.Styling
/// <summary>
/// Gets the name of the control to match.
/// </summary>
public string Name { get; set; }
public string? Name { get; set; }
/// <inheritdoc/>
public override Type TargetType => _targetType ?? _previous?.TargetType;
public override Type? TargetType => _targetType ?? _previous?.TargetType;
/// <inheritdoc/>
public override bool IsCombinator => false;
@ -130,12 +128,12 @@ namespace Avalonia.Styling
{
if (subscribe)
{
var observable = new ClassObserver(control.Classes, _classes.Value);
var observable = new StyleClassActivator(control.Classes, _classes.Value);
return new SelectorMatch(observable);
}
if (!AreClassesMatching(control.Classes, Classes))
if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes))
{
return SelectorMatch.NeverThisInstance;
}
@ -144,7 +142,7 @@ namespace Avalonia.Styling
return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance;
}
protected override Selector MovePrevious() => _previous;
protected override Selector? MovePrevious() => _previous;
private string BuildSelectorString()
{
@ -190,80 +188,5 @@ namespace Avalonia.Styling
return builder.ToString();
}
private static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch)
{
int remainingMatches = toMatch.Count;
int classesCount = classes.Count;
// Early bail out - we can't match if control does not have enough classes.
if (classesCount < remainingMatches)
{
return false;
}
for (var i = 0; i < classesCount; i++)
{
var c = classes[i];
if (toMatch.Contains(c))
{
--remainingMatches;
// Already matched so we can skip checking other classes.
if (remainingMatches == 0)
{
break;
}
}
}
return remainingMatches == 0;
}
private sealed class ClassObserver : LightweightObservableBase<bool>
{
private readonly IList<string> _match;
private readonly IAvaloniaReadOnlyList<string> _classes;
private bool _hasMatch;
public ClassObserver(IAvaloniaReadOnlyList<string> classes, IList<string> match)
{
_classes = classes;
_match = match;
}
protected override void Deinitialize() => _classes.CollectionChanged -= ClassesChanged;
protected override void Initialize()
{
_hasMatch = IsMatching();
_classes.CollectionChanged += ClassesChanged;
}
protected override void Subscribed(IObserver<bool> observer, bool first)
{
observer.OnNext(_hasMatch);
}
private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Move)
{
var hasMatch = IsMatching();
if (hasMatch != _hasMatch)
{
PublishNext(hasMatch);
_hasMatch = hasMatch;
}
}
}
private bool IsMatching()
{
return AreClassesMatching(_classes, _match);
}
}
}
}

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

@ -4,6 +4,7 @@
using Avalonia.Styling;
using System;
using Avalonia.Controls;
using System.Collections.Generic;
namespace Avalonia.Markup.Xaml.Styling
{
@ -67,23 +68,7 @@ namespace Avalonia.Markup.Xaml.Styling
IResourceNode IResourceNode.ResourceParent => _parent;
/// <inheritdoc/>
public bool Attach(IStyleable control, IStyleHost container)
{
if (Source != null)
{
return Loaded.Attach(control, container);
}
return false;
}
public void Detach()
{
if (Source != null)
{
Loaded.Detach();
}
}
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);

2
tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs

@ -45,7 +45,7 @@ namespace Avalonia.Benchmarks.Styling
{
_window.Styles.Add(new Style(x => x.OfType<TextBox>().Class("foo").Class("bar").Class("baz"))
{
Setters = new[]
Setters =
{
new Setter(TextBox.TextProperty, "foo"),
}

6
tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs

@ -1,6 +1,7 @@
using System;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Styling;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
@ -34,9 +35,8 @@ namespace Avalonia.Benchmarks.Styling
{
var styles = UnitTestApplication.Current.Styles;
styles.Attach(_control, UnitTestApplication.Current);
styles.Detach();
styles.TryAttach(_control, UnitTestApplication.Current);
((IStyleable)_control).DetachStyles();
}
public void Dispose()

8
tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs

@ -363,7 +363,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
new Style(x => x.OfType<TestTemplatedControl>())
{
Setters = new[]
Setters =
{
new Setter(
TemplatedControl.TemplateProperty,
@ -399,7 +399,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
new Style(x => x.OfType<TestTemplatedControl>())
{
Setters = new[]
Setters =
{
new Setter(
TemplatedControl.TemplateProperty,
@ -438,7 +438,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
new Style(x => x.OfType<TestTemplatedControl>())
{
Setters = new[]
Setters =
{
new Setter(
TemplatedControl.TemplateProperty,
@ -458,7 +458,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
new Style(x => x.OfType<TestTemplatedControl>())
{
Setters = new[]
Setters =
{
new Setter(
TemplatedControl.TemplateProperty,

2
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -163,7 +163,7 @@ namespace Avalonia.Controls.UnitTests
{
new Style(x => x.OfType<TabItem>())
{
Setters = new[]
Setters =
{
new Setter(TemplatedControl.TemplateProperty, template)
}

2
tests/Avalonia.Controls.UnitTests/UserControlTests.cs

@ -25,7 +25,7 @@ namespace Avalonia.Controls.UnitTests
{
new Style(x => x.OfType<UserControl>())
{
Setters = new[]
Setters =
{
new Setter(TemplatedControl.TemplateProperty, GetTemplate())
}

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

@ -139,7 +139,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
get { throw new NotImplementedException(); }
}
IObservable<IStyleable> IStyleable.StyleDetach { get; }
public void DetachStyles()
{
throw new NotImplementedException();
}
public void StyleApplied(IStyleInstance instance)
{
throw new NotImplementedException();
}
}
private class AttachedOwner

37
tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs

@ -53,42 +53,7 @@ namespace Avalonia.Markup.Xaml.UnitTests
}
};
setter.Apply(null, control, null);
Assert.Equal("foo", control.Text);
control.Text = "bar";
Assert.Equal("bar", data.Foo);
}
}
[Fact]
public void Setter_With_TwoWay_Binding_And_Activator_Should_Update_Source()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var data = new Data
{
Foo = "foo",
};
var control = new TextBox
{
DataContext = data,
};
var setter = new Setter
{
Property = TextBox.TextProperty,
Value = new Binding
{
Path = "Foo",
Mode = BindingMode.TwoWay
}
};
var activator = Observable.Never<bool>().StartWith(true);
setter.Apply(null, control, activator);
setter.Instance(control, false).Activate();
Assert.Equal("foo", control.Text);
control.Text = "bar";

71
tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs

@ -1,71 +0,0 @@
// 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;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class ActivatedObservableTests
{
[Fact]
public void Should_Produce_Correct_Values()
{
var activator = new BehaviorSubject<bool>(false);
var source = new BehaviorSubject<object>(1);
var target = new ActivatedObservable(activator, source, string.Empty);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
activator.OnNext(true);
source.OnNext(2);
activator.OnNext(false);
source.OnNext(3);
activator.OnNext(true);
Assert.Equal(
new[]
{
AvaloniaProperty.UnsetValue,
1,
2,
AvaloniaProperty.UnsetValue,
3,
},
result);
}
[Fact]
public void Should_Complete_When_Source_Completes()
{
var activator = new BehaviorSubject<bool>(false);
var source = new BehaviorSubject<object>(1);
var target = new ActivatedObservable(activator, source, string.Empty);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
source.OnCompleted();
Assert.True(completed);
}
[Fact]
public void Should_Error_When_Source_Errors()
{
var activator = new BehaviorSubject<bool>(false);
var source = new BehaviorSubject<object>(1);
var target = new ActivatedObservable(activator, source, string.Empty);
var error = new Exception();
var completed = false;
target.Subscribe(_ => { }, x => completed = true);
source.OnError(error);
Assert.True(completed);
}
}
}

92
tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs

@ -1,92 +0,0 @@
// 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.Reactive.Disposables;
using System.Reactive.Subjects;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class ActivatedSubjectTests
{
[Fact]
public void Should_Set_Values()
{
var activator = new BehaviorSubject<bool>(false);
var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty);
target.Subscribe();
target.OnNext("bar");
Assert.Equal(AvaloniaProperty.UnsetValue, source.Value);
activator.OnNext(true);
target.OnNext("baz");
Assert.Equal("baz", source.Value);
activator.OnNext(false);
Assert.Equal(AvaloniaProperty.UnsetValue, source.Value);
target.OnNext("bax");
activator.OnNext(true);
Assert.Equal("bax", source.Value);
}
[Fact]
public void Should_Invoke_OnCompleted_On_Activator_Completed()
{
var activator = new BehaviorSubject<bool>(false);
var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty);
target.Subscribe();
activator.OnCompleted();
Assert.True(source.Completed);
}
[Fact]
public void Should_Invoke_OnError_On_Activator_Error()
{
var activator = new BehaviorSubject<bool>(false);
var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty);
var targetError = default(Exception);
var error = new Exception();
target.Subscribe(_ => { }, e => targetError = e);
activator.OnError(error);
Assert.Same(error, source.Error);
Assert.Same(error, targetError);
}
private class TestSubject : ISubject<object>
{
private IObserver<object> _observer;
public bool Completed { get; set; }
public Exception Error { get; set; }
public object Value { get; set; } = AvaloniaProperty.UnsetValue;
public void OnCompleted()
{
Completed = true;
}
public void OnError(Exception error)
{
Error = error;
}
public void OnNext(object value)
{
Value = value;
}
public IDisposable Subscribe(IObserver<object> observer)
{
_observer = observer;
return Disposable.Empty;
}
}
}
}

75
tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs

@ -1,75 +0,0 @@
// 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;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Microsoft.Reactive.Testing;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class ActivatedValueTests
{
[Fact]
public void Should_Produce_Correct_Values()
{
var activator = new BehaviorSubject<bool>(false);
var target = new ActivatedValue(activator, 1, string.Empty);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
activator.OnNext(true);
activator.OnNext(false);
Assert.Equal(new[] { AvaloniaProperty.UnsetValue, 1, AvaloniaProperty.UnsetValue }, result);
}
[Fact]
public void Should_Complete_When_Activator_Completes()
{
var activator = new BehaviorSubject<bool>(false);
var target = new ActivatedValue(activator, 1, string.Empty);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
activator.OnCompleted();
Assert.True(completed);
}
[Fact]
public void Should_Error_When_Activator_Errors()
{
var activator = new BehaviorSubject<bool>(false);
var target = new ActivatedValue(activator, 1, string.Empty);
var error = new Exception();
var completed = false;
target.Subscribe(_ => { }, x => completed = true);
activator.OnError(error);
Assert.True(completed);
}
[Fact]
public void Should_Unsubscribe_From_Activator_When_All_Subscriptions_Disposed()
{
var scheduler = new TestScheduler();
var activator1 = scheduler.CreateColdObservable<bool>();
var activator2 = scheduler.CreateColdObservable<bool>();
var activator = StyleActivator.And(new[] { activator1, activator2 });
var target = new ActivatedValue(activator, 1, string.Empty);
var subscription = target.Subscribe(_ => { });
Assert.Equal(1, activator1.Subscriptions.Count);
Assert.Equal(Subscription.Infinite, activator1.Subscriptions[0].Unsubscribe);
subscription.Dispose();
Assert.Equal(1, activator1.Subscriptions.Count);
Assert.Equal(0, activator1.Subscriptions[0].Unsubscribe);
}
}
}

16
tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs

@ -36,7 +36,7 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Control1
{
Classes = new Classes { "foo" },
Classes = { "foo" },
};
var target = default(Selector).Class("foo");
@ -51,7 +51,7 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Control1
{
Classes = new Classes { "bar" },
Classes = { "bar" },
};
var target = default(Selector).Class("foo");
@ -66,7 +66,7 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Control1
{
Classes = new Classes { "foo" },
Classes = { "foo" },
TemplatedParent = new Mock<ITemplatedControl>().Object,
};
@ -83,7 +83,7 @@ namespace Avalonia.Styling.UnitTests
var control = new Control1();
var target = default(Selector).Class("foo");
var activator = target.Match(control).Activator;
var activator = target.Match(control).Activator.ToObservable();
Assert.False(await activator.Take(1));
control.Classes.Add("foo");
@ -95,11 +95,11 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Control1
{
Classes = new Classes { "foo" },
Classes = { "foo" },
};
var target = default(Selector).Class("foo");
var activator = target.Match(control).Activator;
var activator = target.Match(control).Activator.ToObservable();
Assert.True(await activator.Take(1));
control.Classes.Remove("foo");
@ -111,7 +111,7 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Control1();
var target = default(Selector).Class("foo").Class("bar");
var activator = target.Match(control).Activator;
var activator = target.Match(control).Activator.ToObservable();
Assert.False(await activator.Take(1));
control.Classes.Add("foo");
@ -128,7 +128,7 @@ namespace Avalonia.Styling.UnitTests
// Test for #1698
var control = new Control1
{
Classes = new Classes { "foo" },
Classes = { "foo" },
};
var target = default(Selector).Class("foo");

2
tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs

@ -90,7 +90,7 @@ namespace Avalonia.Styling.UnitTests
child.LogicalParent = parent;
var selector = default(Selector).OfType<TestLogical1>().Class("foo").Descendant().OfType<TestLogical3>();
var activator = selector.Match(child).Activator;
var activator = selector.Match(child).Activator.ToObservable();
Assert.False(await activator.Take(1));
parent.Classes.Add("foo");

29
tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs

@ -85,6 +85,35 @@ namespace Avalonia.Styling.UnitTests
Assert.Equal(SelectorMatchResult.NeverThisType, match.Result);
}
[Fact]
public void Control_With_Class_Descendent_Of_Control_With_Two_Classes()
{
var textBlock = new TextBlock();
var control = new Button { Content = textBlock };
control.ApplyTemplate();
var selector = default(Selector)
.OfType<Button>()
.Class("foo")
.Class("bar")
.Descendant()
.OfType<TextBlock>()
.Class("baz");
var values = new List<bool>();
var match = selector.Match(textBlock);
Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
match.Activator.Subscribe(x => values.Add(x));
Assert.Equal(new[] { false }, values);
control.Classes.AddRange(new[] { "foo", "bar" });
Assert.Equal(new[] { false }, values);
textBlock.Classes.Add("baz");
Assert.Equal(new[] { false, true }, values);
}
[Fact]
public void Named_Class_Template_Child_Of_Control()
{

8
tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs

@ -41,7 +41,7 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Control1
{
Classes = new Classes { "foo" },
Classes = { "foo" },
};
var target = default(Selector).Not(x => x.Class("foo"));
@ -56,7 +56,7 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Control1
{
Classes = new Classes { "bar" },
Classes = { "bar" },
};
var target = default(Selector).Not(x => x.Class("foo"));
@ -71,7 +71,7 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Control1
{
Classes = new Classes { "bar" },
Classes = { "bar" },
};
var target = default(Selector).OfType<Control1>().Not(x => x.Class("foo"));
@ -86,7 +86,7 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Control2
{
Classes = new Classes { "foo" },
Classes = { "foo" },
};
var target = default(Selector).OfType<Control1>().Not(x => x.Class("foo"));

2
tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs

@ -16,7 +16,7 @@ namespace Avalonia.Styling.UnitTests
{
var control = new TextBlock();
var target = default(Selector).PropertyEquals(TextBlock.TextProperty, "foo");
var activator = target.Match(control).Activator;
var activator = target.Match(control).Activator.ToObservable();
Assert.False(await activator.Take(1));
control.Text = "foo";

51
tests/Avalonia.Styling.UnitTests/SetterTests.cs

@ -33,7 +33,7 @@ namespace Avalonia.Styling.UnitTests
var style = Mock.Of<IStyle>();
var setter = new Setter(TextBlock.TextProperty, binding);
setter.Apply(style, control, null);
setter.Instance(control, false).Activate();
Assert.Equal("foo", control.Text);
}
@ -46,7 +46,7 @@ namespace Avalonia.Styling.UnitTests
var style = Mock.Of<IStyle>();
var setter = new Setter(Decorator.ChildProperty, template);
setter.Apply(style, control, null);
setter.Instance(control, false).Activate();
Assert.IsType<Canvas>(control.Child);
}
@ -62,13 +62,13 @@ namespace Avalonia.Styling.UnitTests
RelativeSource = new RelativeSource(RelativeSourceMode.Self),
};
var setter = new Setter(Decorator.TagProperty, binding);
var activator = new BehaviorSubject<bool>(true);
setter.Apply(style, control, activator);
var instance = setter.Instance(control, true);
instance.Activate();
Assert.Equal("foobar", control.Tag);
// Issue #1218 caused TestConverter.ConvertBack to throw here.
activator.OnNext(false);
instance.Deactivate();
Assert.Null(control.Tag);
}
@ -77,13 +77,14 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Mock<IStyleable>();
var style = Mock.Of<Style>();
var setter = new Setter(TextBlock.TextProperty, "foo");
var setter = new Setter(TextBlock.TagProperty, "foo");
setter.Apply(style, control.Object, null);
setter.Instance(control.Object, false).Activate();
control.Verify(x => x.Bind(
TextBlock.TextProperty,
It.IsAny<IObservable<BindingValue<string>>>()));
control.Verify(x => x.SetValue(
TextBlock.TagProperty,
"foo",
BindingPriority.Style));
}
[Fact]
@ -91,14 +92,15 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Mock<IStyleable>();
var style = Mock.Of<Style>();
var setter = new Setter(TextBlock.TextProperty, "foo");
var setter = new Setter(TextBlock.TagProperty, "foo");
var activator = new Subject<bool>();
setter.Apply(style, control.Object, activator);
setter.Instance(control.Object, true).Activate();
control.Verify(x => x.Bind(
TextBlock.TextProperty,
It.IsAny<IObservable<BindingValue<string>>>()));
control.Verify(x => x.SetValue(
TextBlock.TagProperty,
"foo",
BindingPriority.StyleTrigger));
}
[Fact]
@ -106,13 +108,14 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Mock<IStyleable>();
var style = Mock.Of<Style>();
var setter = new Setter(TextBlock.TextProperty, CreateMockBinding(TextBlock.TextProperty));
var setter = new Setter(TextBlock.TagProperty, CreateMockBinding(TextBlock.TagProperty));
setter.Apply(style, control.Object, null);
setter.Instance(control.Object, false).Activate();
control.Verify(x => x.Bind(
TextBlock.TextProperty,
It.IsAny<IObservable<BindingValue<string>>>()));
TextBlock.TagProperty,
It.IsAny<IObservable<BindingValue<object>>>(),
BindingPriority.Style));
}
[Fact]
@ -120,14 +123,14 @@ namespace Avalonia.Styling.UnitTests
{
var control = new Mock<IStyleable>();
var style = Mock.Of<Style>();
var setter = new Setter(TextBlock.TextProperty, CreateMockBinding(TextBlock.TextProperty));
var activator = new Subject<bool>();
var setter = new Setter(TextBlock.TagProperty, CreateMockBinding(TextBlock.TagProperty));
setter.Apply(style, control.Object, activator);
setter.Instance(control.Object, true).Activate();
control.Verify(x => x.Bind(
TextBlock.TextProperty,
It.IsAny<IObservable<BindingValue<string>>>()));
TextBlock.TagProperty,
It.IsAny<IObservable<BindingValue<object>>>(),
BindingPriority.StyleTrigger));
}
private IBinding CreateMockBinding(AvaloniaProperty property)

42
tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs

@ -0,0 +1,42 @@
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Reactive;
using Avalonia.Styling.Activators;
namespace Avalonia.Styling.UnitTests
{
internal static class StyleActivatorExtensions
{
public static IDisposable Subscribe(this IStyleActivator activator, Action<bool> action)
{
return activator.ToObservable().Subscribe(action);
}
public static async Task<bool> Take(this IStyleActivator activator, int value)
{
return await activator.ToObservable().Take(value);
}
public static IObservable<bool> ToObservable(this IStyleActivator activator)
{
return new ObservableAdapter(activator);
}
private class ObservableAdapter : LightweightObservableBase<bool>, IStyleActivatorSink
{
private readonly IStyleActivator _source;
private bool _value;
public ObservableAdapter(IStyleActivator source) => _source = source;
protected override void Initialize() => _source.Subscribe(this);
protected override void Deinitialize() => _source.Unsubscribe(this);
void IStyleActivatorSink.OnNext(bool value, int tag)
{
_value = value;
PublishNext(value);
}
}
}
}

169
tests/Avalonia.Styling.UnitTests/StyleActivatorTests.cs

@ -1,169 +0,0 @@
// 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;
using System.Reactive;
using System.Reactive.Linq;
using Microsoft.Reactive.Testing;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class StyleActivatorTests : ReactiveTest
{
[Fact]
public void Activator_Should_Subscribe_To_Inputs_On_First_Subscription()
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable<bool>();
var target = StyleActivator.And(new[] { source });
Assert.Equal(0, source.Subscriptions.Count);
target.Subscribe(_ => { });
Assert.Equal(1, source.Subscriptions.Count);
}
[Fact]
public void Activator_Should_Unsubscribe_From_Inputs_After_Last_Subscriber_Completes()
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable<bool>();
var target = StyleActivator.And(new[] { source });
var dispose = target.Subscribe(_ => { });
Assert.Equal(1, source.Subscriptions.Count);
Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe);
dispose.Dispose();
Assert.Equal(1, source.Subscriptions.Count);
Assert.Equal(0, source.Subscriptions[0].Unsubscribe);
}
[Fact]
public void Activator_And_Should_Follow_Single_Input()
{
var inputs = new[] { new TestSubject<bool>(false) };
var target = StyleActivator.And(inputs);
var result = new TestObserver<bool>();
target.Subscribe(result);
Assert.False(result.GetValue());
inputs[0].OnNext(true);
Assert.True(result.GetValue());
inputs[0].OnNext(false);
Assert.False(result.GetValue());
inputs[0].OnNext(true);
Assert.True(result.GetValue());
Assert.Equal(1, inputs[0].SubscriberCount);
}
[Fact]
public void Activator_And_Should_AND_Multiple_Inputs()
{
var inputs = new[]
{
new TestSubject<bool>(false),
new TestSubject<bool>(false),
new TestSubject<bool>(true),
};
var target = StyleActivator.And(inputs);
var result = new TestObserver<bool>();
target.Subscribe(result);
Assert.False(result.GetValue());
inputs[0].OnNext(true);
inputs[1].OnNext(true);
Assert.True(result.GetValue());
inputs[0].OnNext(false);
Assert.False(result.GetValue());
Assert.Equal(1, inputs[0].SubscriberCount);
Assert.Equal(1, inputs[1].SubscriberCount);
Assert.Equal(1, inputs[2].SubscriberCount);
}
[Fact]
public void Activator_Or_Should_Follow_Single_Input()
{
var inputs = new[] { new TestSubject<bool>(false) };
var target = StyleActivator.Or(inputs);
var result = new TestObserver<bool>();
target.Subscribe(result);
Assert.False(result.GetValue());
inputs[0].OnNext(true);
Assert.True(result.GetValue());
inputs[0].OnNext(false);
Assert.False(result.GetValue());
inputs[0].OnNext(true);
Assert.True(result.GetValue());
Assert.Equal(1, inputs[0].SubscriberCount);
}
[Fact]
public void Activator_Or_Should_OR_Multiple_Inputs()
{
var inputs = new[]
{
new TestSubject<bool>(false),
new TestSubject<bool>(false),
new TestSubject<bool>(true),
};
var target = StyleActivator.Or(inputs);
var result = new TestObserver<bool>();
target.Subscribe(result);
Assert.True(result.GetValue());
inputs[2].OnNext(false);
Assert.False(result.GetValue());
inputs[0].OnNext(true);
Assert.True(result.GetValue());
Assert.Equal(1, inputs[0].SubscriberCount);
Assert.Equal(1, inputs[1].SubscriberCount);
Assert.Equal(1, inputs[2].SubscriberCount);
}
[Fact]
public void Activator_Or_Should_Not_Unsubscribe_All_When_Input_Completes_On_False()
{
var inputs = new[]
{
new TestSubject<bool>(false),
new TestSubject<bool>(false),
new TestSubject<bool>(true),
};
var target = StyleActivator.Or(inputs);
var result = new TestObserver<bool>();
target.Subscribe(result);
Assert.True(result.GetValue());
inputs[2].OnNext(false);
Assert.False(result.GetValue());
inputs[2].OnCompleted();
Assert.Equal(1, inputs[0].SubscriberCount);
Assert.Equal(1, inputs[1].SubscriberCount);
Assert.Equal(0, inputs[2].SubscriberCount);
}
[Fact]
public void Completed_Activator_Should_Signal_OnCompleted()
{
var inputs = new[]
{
Observable.Return(false),
};
var target = StyleActivator.Or(inputs);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
Assert.True(completed);
}
}
}

58
tests/Avalonia.Styling.UnitTests/StyleTests.cs

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
using Avalonia.Controls;
using Avalonia.UnitTests;
using Xunit;
@ -17,7 +16,7 @@ namespace Avalonia.Styling.UnitTests
{
Style style = new Style(x => x.OfType<Class1>())
{
Setters = new[]
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
@ -25,7 +24,7 @@ namespace Avalonia.Styling.UnitTests
var target = new Class1();
style.Attach(target, null);
style.TryAttach(target, null);
Assert.Equal("Foo", target.Foo);
}
@ -35,7 +34,7 @@ namespace Avalonia.Styling.UnitTests
{
Style style = new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters = new[]
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
@ -43,7 +42,7 @@ namespace Avalonia.Styling.UnitTests
var target = new Class1();
style.Attach(target, null);
style.TryAttach(target, null);
Assert.Equal("foodefault", target.Foo);
target.Classes.Add("foo");
Assert.Equal("Foo", target.Foo);
@ -56,7 +55,7 @@ namespace Avalonia.Styling.UnitTests
{
Style style = new Style
{
Setters = new[]
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
@ -64,7 +63,7 @@ namespace Avalonia.Styling.UnitTests
var target = new Class1();
style.Attach(target, target);
style.TryAttach(target, target);
Assert.Equal("Foo", target.Foo);
}
@ -74,7 +73,7 @@ namespace Avalonia.Styling.UnitTests
{
Style style = new Style
{
Setters = new[]
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
@ -83,7 +82,7 @@ namespace Avalonia.Styling.UnitTests
var target = new Class1();
var other = new Class1();
style.Attach(target, other);
style.TryAttach(target, other);
Assert.Equal("foodefault", target.Foo);
}
@ -93,7 +92,7 @@ namespace Avalonia.Styling.UnitTests
{
Style style = new Style(x => x.OfType<Class1>())
{
Setters = new[]
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
@ -104,7 +103,7 @@ namespace Avalonia.Styling.UnitTests
Foo = "Original",
};
style.Attach(target, null);
style.TryAttach(target, null);
Assert.Equal("Original", target.Foo);
}
@ -115,7 +114,7 @@ namespace Avalonia.Styling.UnitTests
{
new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters = new[]
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
@ -123,7 +122,7 @@ namespace Avalonia.Styling.UnitTests
new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters = new[]
Setters =
{
new Setter(Class1.FooProperty, "Bar"),
},
@ -135,7 +134,7 @@ namespace Avalonia.Styling.UnitTests
List<string> values = new List<string>();
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
styles.Attach(target, null);
styles.TryAttach(target, null);
target.Classes.Add("foo");
target.Classes.Remove("foo");
@ -143,13 +142,13 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void Style_Should_Detach_When_Removed_From_Logical_Tree()
public void Style_Should_Detach_When_Control_Removed_From_Logical_Tree()
{
Border border;
var style = new Style(x => x.OfType<Border>())
{
Setters = new[]
Setters =
{
new Setter(Border.BorderThicknessProperty, new Thickness(4)),
}
@ -160,38 +159,13 @@ namespace Avalonia.Styling.UnitTests
Child = border = new Border(),
};
style.Attach(border, null);
style.TryAttach(border, null);
Assert.Equal(new Thickness(4), border.BorderThickness);
root.Child = null;
Assert.Equal(new Thickness(0), border.BorderThickness);
}
[Fact]
public void Style_Should_Detach_Setters_When_Detach_Is_Called()
{
Border border;
var style = new Style(x => x.OfType<Border>())
{
Setters = new[]
{
new Setter(Border.BorderThicknessProperty, new Thickness(4)),
}
};
var root = new TestRoot
{
Child = border = new Border(),
};
style.Attach(border, null);
Assert.Equal(new Thickness(4), border.BorderThickness);
style.Detach();
Assert.Equal(new Thickness(0), border.BorderThickness);
}
private class Class1 : Control
{
public static readonly StyledProperty<string> FooProperty =

9
tests/Avalonia.Styling.UnitTests/StyledElementTests.cs

@ -352,7 +352,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void StyleDetach_Is_Triggered_When_Control_Removed_From_Logical_Tree()
public void StyleInstance_Is_Disposed_When_Control_Removed_From_Logical_Tree()
{
using (AvaloniaLocator.EnterScope())
{
@ -361,11 +361,12 @@ namespace Avalonia.Styling.UnitTests
root.Child = child;
bool styleDetachTriggered = false;
((IStyleable)child).StyleDetach.Subscribe(_ => styleDetachTriggered = true);
var styleInstance = new Mock<IStyleInstance>();
((IStyleable)child).StyleApplied(styleInstance.Object);
root.Child = null;
Assert.True(styleDetachTriggered);
styleInstance.Verify(x => x.Dispose(), Times.Once);
}
}

Loading…
Cancel
Save