Browse Source

Merge branch 'master' into TextBoxImprovements

pull/9490/head
Max Katz 3 years ago
committed by GitHub
parent
commit
2c6ba92886
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  2. 25
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  3. 22
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  4. 2
      src/Avalonia.Base/AvaloniaProperty.cs
  5. 22
      src/Avalonia.Base/Data/BindingPriority.cs
  6. 11
      src/Avalonia.Base/Layout/Layoutable.cs
  7. 36
      src/Avalonia.Base/PropertyStore/FramePriority.cs
  8. 4
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  9. 20
      src/Avalonia.Base/PropertyStore/ValueFrame.cs
  10. 59
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  11. 210
      src/Avalonia.Base/StyledElement.cs
  12. 7
      src/Avalonia.Base/Styling/ControlTheme.cs
  13. 2
      src/Avalonia.Base/Styling/IStyleable.cs
  14. 5
      src/Avalonia.Base/Styling/Style.cs
  15. 6
      src/Avalonia.Base/Styling/StyleBase.cs
  16. 13
      src/Avalonia.Base/Styling/StyleInstance.cs
  17. 3
      src/Avalonia.Base/Styling/Styles.cs
  18. 2
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  19. 2
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  20. 59
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  21. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs
  22. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs
  23. 2
      src/Markup/Avalonia.Markup/Data/TemplateBinding.cs
  24. 3
      tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs
  25. 2
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs
  26. 2
      tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs
  27. 3
      tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
  28. 80
      tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
  29. 218
      tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs
  30. 2
      tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs
  31. 149
      tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs
  32. 5
      tests/Avalonia.Benchmarks/Styling/Style_Activation.cs
  33. 4
      tests/Avalonia.Benchmarks/Styling/Style_Apply.cs
  34. 2
      tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs
  35. 10
      tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs
  36. 4
      tests/Avalonia.Benchmarks/Styling/Style_NonActive.cs
  37. 4
      tests/Avalonia.Controls.UnitTests/ContentControlTests.cs
  38. 4
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  39. 4
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  40. 2
      tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs
  41. 2
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs
  42. 3
      tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs
  43. 12
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs
  44. 14
      tests/Avalonia.UnitTests/StyleHelpers.cs

4
src/Android/Avalonia.Android/AndroidInputMethod.cs

@ -95,6 +95,10 @@ namespace Avalonia.Android
_imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
else
{
_imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.ImplicitOnly);
}
}
private void SurroundingTextChanged(object sender, EventArgs e)

25
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@ -4,9 +4,12 @@ using Android.Content;
using Android.Content.Res;
using Android.OS;
using Android.Runtime;
using Android.Views;
using AndroidX.AppCompat.App;
using AndroidX.Lifecycle;
using AndroidRect = Android.Graphics.Rect;
namespace Avalonia.Android
{
public abstract class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler
@ -15,6 +18,7 @@ namespace Avalonia.Android
public Action<int, Result, Intent> ActivityResult { get; set; }
internal AvaloniaView View;
private GlobalLayoutListener _listener;
protected override void OnCreate(Bundle savedInstanceState)
{
@ -32,6 +36,10 @@ namespace Avalonia.Android
base.OnCreate(savedInstanceState);
SetContentView(View);
_listener = new GlobalLayoutListener(View);
View.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener);
}
public object Content
@ -57,6 +65,8 @@ namespace Avalonia.Android
{
View.Content = null;
View.ViewTreeObserver?.RemoveOnGlobalLayoutListener(_listener);
base.OnDestroy();
}
@ -66,5 +76,20 @@ namespace Avalonia.Android
ActivityResult?.Invoke(requestCode, resultCode, data);
}
class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener
{
private AvaloniaView _view;
public GlobalLayoutListener(AvaloniaView view)
{
_view = view;
}
public void OnGlobalLayout()
{
_view.TopLevelImpl?.Resize(_view.TopLevelImpl.ClientSize);
}
}
}
}

22
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -26,6 +26,7 @@ using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Java.Lang;
using Math = System.Math;
using AndroidRect = Android.Graphics.Rect;
namespace Avalonia.Android.Platform.SkiaPlatform
{
@ -63,7 +64,21 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IInputRoot InputRoot { get; private set; }
public virtual Size ClientSize => Size.ToSize(RenderScaling);
public virtual Size ClientSize
{
get
{
AndroidRect rect = new AndroidRect();
AndroidRect intersection = new AndroidRect();
_view.GetWindowVisibleDisplayFrame(intersection);
_view.GetGlobalVisibleRect(rect);
intersection.Intersect(rect);
return new PixelSize(intersection.Right - intersection.Left, intersection.Bottom - intersection.Top).ToSize(RenderScaling);
}
}
public Size? FrameSize => null;
@ -149,6 +164,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
Resized?.Invoke(size, PlatformResizeReason.Unspecified);
}
internal void Resize(Size size)
{
Resized?.Invoke(size, PlatformResizeReason.Layout);
}
class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo
{
private readonly TopLevelImpl _tl;

2
src/Avalonia.Base/AvaloniaProperty.cs

@ -177,7 +177,7 @@ namespace Avalonia
{
return new IndexerDescriptor
{
Priority = BindingPriority.TemplatedParent,
Priority = BindingPriority.Template,
Property = property,
};
}

22
src/Avalonia.Base/Data/BindingPriority.cs

@ -1,7 +1,9 @@
using System;
namespace Avalonia.Data
{
/// <summary>
/// The priority of a binding.
/// The priority of a value or binding.
/// </summary>
public enum BindingPriority
{
@ -16,23 +18,22 @@ namespace Avalonia.Data
LocalValue = 0,
/// <summary>
/// A triggered style binding.
/// A triggered style value.
/// </summary>
/// <remarks>
/// A style trigger is a selector such as .class which overrides a
/// <see cref="TemplatedParent"/> binding. In this way, a basic control can have
/// for example a Background from the templated parent which changes when the
/// control has the :pointerover class.
/// <see cref="Template"/> value. In this way, a control can have, e.g. a Background from
/// the template which changes when the control has the :pointerover class.
/// </remarks>
StyleTrigger,
/// <summary>
/// A binding to a property on the templated parent.
/// A value from the control's template.
/// </summary>
TemplatedParent,
Template,
/// <summary>
/// A style binding.
/// A style value.
/// </summary>
Style,
@ -42,8 +43,11 @@ namespace Avalonia.Data
Inherited,
/// <summary>
/// The binding is uninitialized.
/// The value is uninitialized.
/// </summary>
Unset = int.MaxValue,
[Obsolete("Use Template priority")]
TemplatedParent = Template,
}
}

11
src/Avalonia.Base/Layout/Layoutable.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Logging;
using Avalonia.Styling;
using Avalonia.VisualTree;
#nullable enable
@ -719,9 +720,9 @@ namespace Avalonia.Layout
return finalSize;
}
protected sealed override void InvalidateStyles()
internal sealed override void InvalidateStyles(bool recurse)
{
base.InvalidateStyles();
base.InvalidateStyles(recurse);
InvalidateMeasure();
}
@ -795,6 +796,12 @@ namespace Avalonia.Layout
base.OnVisualParentChanged(oldParent, newParent);
}
private protected override void OnControlThemeChanged()
{
base.OnControlThemeChanged();
InvalidateMeasure();
}
/// <summary>
/// Called when the layout manager raises a LayoutUpdated event.
/// </summary>

36
src/Avalonia.Base/PropertyStore/FramePriority.cs

@ -0,0 +1,36 @@
using System.Diagnostics;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal enum FramePriority : sbyte
{
Animation,
AnimationTemplatedParentTheme,
AnimationTheme,
StyleTrigger,
StyleTriggerTemplatedParentTheme,
StyleTriggerTheme,
Template,
TemplateTemplatedParentTheme,
TemplateTheme,
Style,
StyleTemplatedParentTheme,
StyleTheme,
}
internal static class FramePriorityExtensions
{
public static FramePriority ToFramePriority(this BindingPriority priority, FrameType type = FrameType.Style)
{
Debug.Assert(priority != BindingPriority.LocalValue);
var p = (int)(priority > 0 ? priority : priority + 1);
return (FramePriority)(p * 3 + (int)type);
}
public static bool IsType(this FramePriority priority, FrameType type)
{
return (FrameType)((int)priority % 3) == type;
}
}
}

4
src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs

@ -7,11 +7,11 @@ namespace Avalonia.PropertyStore
/// Holds values in a <see cref="ValueStore"/> set by one of the SetValue or AddBinding
/// overloads with non-LocalValue priority.
/// </summary>
internal class ImmediateValueFrame : ValueFrame
internal sealed class ImmediateValueFrame : ValueFrame
{
public ImmediateValueFrame(BindingPriority priority)
: base(priority, FrameType.Style)
{
Priority = priority;
}
public TypedBindingEntry<T> AddBinding<T>(

20
src/Avalonia.Base/PropertyStore/ValueFrame.cs

@ -1,13 +1,18 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Utilities;
using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
namespace Avalonia.PropertyStore
{
internal enum FrameType
{
Style,
TemplatedParentTheme,
Theme,
}
internal abstract class ValueFrame
{
private List<IValueEntry>? _entries;
@ -15,11 +20,18 @@ namespace Avalonia.PropertyStore
private ValueStore? _owner;
private bool _isShared;
protected ValueFrame(BindingPriority priority, FrameType type)
{
Priority = priority;
FramePriority = priority.ToFramePriority(type);
}
public int EntryCount => _index.Count;
public bool IsActive => GetIsActive(out _);
public ValueStore? Owner => !_isShared ? _owner :
throw new AvaloniaInternalException("Cannot get owner for shared ValueFrame");
public BindingPriority Priority { get; protected set; }
public BindingPriority Priority { get; }
public FramePriority FramePriority { get; }
public bool Contains(AvaloniaProperty property) => _index.ContainsKey(property);

59
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -2,10 +2,12 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.Styling;
using Avalonia.Utilities;
using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
namespace Avalonia.PropertyStore
{
@ -580,6 +582,53 @@ namespace Avalonia.PropertyStore
return false;
}
public void RemoveFrames(FrameType type)
{
var removed = false;
for (var i = _frames.Count - 1; i >= 0; --i)
{
var frame = _frames[i];
if (frame is not ImmediateValueFrame && frame.FramePriority.IsType(type))
{
_frames.RemoveAt(i);
frame.Dispose();
removed = true;
}
}
if (removed)
{
++_frameGeneration;
ReevaluateEffectiveValues();
}
}
public void RemoveFrames(IReadOnlyList<IStyle> styles)
{
var removed = false;
for (var i = _frames.Count - 1; i >= 0; --i)
{
var frame = _frames[i];
if (frame is StyleInstance style && styles.Contains(style.Source))
{
_frames.RemoveAt(i);
frame.Dispose();
removed = true;
}
}
if (removed)
{
++_frameGeneration;
ReevaluateEffectiveValues();
}
}
public AvaloniaPropertyValue GetDiagnostic(AvaloniaProperty property)
{
object? value;
@ -612,7 +661,7 @@ namespace Avalonia.PropertyStore
{
Debug.Assert(!_frames.Contains(frame));
var index = BinarySearchFrame(frame.Priority);
var index = BinarySearchFrame(frame.FramePriority);
_frames.Insert(index, frame);
++_frameGeneration;
frame.SetOwner(this);
@ -626,7 +675,7 @@ namespace Avalonia.PropertyStore
{
Debug.Assert(priority != BindingPriority.LocalValue);
var index = BinarySearchFrame(priority);
var index = BinarySearchFrame(priority.ToFramePriority());
if (index > 0 && _frames[index - 1] is ImmediateValueFrame f &&
f.Priority == priority &&
@ -914,7 +963,7 @@ namespace Avalonia.PropertyStore
}
}
private int BinarySearchFrame(BindingPriority priority)
private int BinarySearchFrame(FramePriority priority)
{
var lo = 0;
var hi = _frames.Count - 1;
@ -923,7 +972,7 @@ namespace Avalonia.PropertyStore
while (lo <= hi)
{
var i = lo + ((hi - lo) >> 1);
var order = priority - _frames[i].Priority;
var order = priority - _frames[i].FramePriority;
if (order <= 0)
{

210
src/Avalonia.Base/StyledElement.cs

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using Avalonia.Animation;
using Avalonia.Collections;
@ -11,6 +12,7 @@ using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.PropertyStore;
using Avalonia.Styling;
namespace Avalonia
@ -69,10 +71,10 @@ namespace Avalonia
private IAvaloniaList<ILogical>? _logicalChildren;
private IResourceDictionary? _resources;
private Styles? _styles;
private bool _styled;
private bool _stylesApplied;
private bool _themeApplied;
private ITemplatedControl? _templatedParent;
private bool _dataContextUpdating;
private bool _hasPromotedTheme;
private ControlTheme? _implicitTheme;
/// <summary>
@ -141,7 +143,7 @@ namespace Avalonia
set
{
if (_styled)
if (_stylesApplied)
{
throw new InvalidOperationException("Cannot set Name : styled element already styled.");
}
@ -353,38 +355,33 @@ namespace Avalonia
/// </returns>
public bool ApplyStyling()
{
if (_initCount == 0 && !_styled)
if (_initCount == 0 && (!_stylesApplied || !_themeApplied))
{
var hasPromotedTheme = _hasPromotedTheme;
GetValueStore().BeginStyling();
try
{
ApplyControlTheme();
ApplyStyles(this);
if (!_themeApplied)
{
ApplyControlTheme();
_themeApplied = true;
}
if (!_stylesApplied)
{
ApplyStyles(this);
_stylesApplied = true;
}
}
finally
{
_styled = true;
GetValueStore().EndStyling();
}
if (hasPromotedTheme)
{
_hasPromotedTheme = false;
ClearValue(ThemeProperty);
}
}
return _styled;
return _stylesApplied;
}
/// <summary>
/// Detaches all styles from the element and queues a restyle.
/// </summary>
protected virtual void InvalidateStyles() => DetachStyles();
protected void InitializeIfNeeded()
{
if (_initCount == 0 && !IsInitialized)
@ -506,17 +503,16 @@ namespace Avalonia
};
}
void IStyleable.DetachStyles() => DetachStyles();
void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles)
{
InvalidateStylesOnThisAndDescendents();
if (HasSettersOrAnimations(styles))
InvalidateStyles(recurse: true);
}
void IStyleHost.StylesRemoved(IReadOnlyList<IStyle> styles)
{
var allStyles = RecurseStyles(styles);
DetachStylesFromThisAndDescendents(allStyles);
if (FlattenStyles(styles) is { } allStyles)
DetachStyles(allStyles);
}
protected virtual void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
@ -615,31 +611,25 @@ namespace Avalonia
if (change.Property == ThemeProperty)
{
var (oldValue, newValue) = change.GetOldAndNewValue<ControlTheme?>();
// Changing the theme detaches all styles, meaning that if the theme property was
// set via a style, it will get cleared! To work around this, if the value was
// applied at less than local value priority then promote the value to local value
// priority until styling is re-applied.
if (change.Priority > BindingPriority.LocalValue)
{
Theme = newValue;
_hasPromotedTheme = true;
}
else if (_hasPromotedTheme && change.Priority == BindingPriority.LocalValue)
{
_hasPromotedTheme = false;
}
InvalidateStyles();
if (oldValue is not null)
DetachControlThemeFromTemplateChildren(oldValue);
OnControlThemeChanged();
_themeApplied = false;
}
}
internal virtual void DetachControlThemeFromTemplateChildren(ControlTheme theme)
private protected virtual void OnControlThemeChanged()
{
var values = GetValueStore();
values.BeginStyling();
try { values.RemoveFrames(FrameType.Theme); }
finally { values.EndStyling(); }
}
internal virtual void OnTemplatedParentControlThemeChanged()
{
var values = GetValueStore();
values.BeginStyling();
try { values.RemoveFrames(FrameType.TemplatedParentTheme); }
finally { values.EndStyling(); }
}
internal ControlTheme? GetEffectiveTheme()
@ -667,6 +657,23 @@ namespace Avalonia
return null;
}
internal virtual void InvalidateStyles(bool recurse)
{
var values = GetValueStore();
values.BeginStyling();
try { values.RemoveFrames(FrameType.Style); }
finally { values.EndStyling(); }
_stylesApplied = false;
if (recurse && GetInheritanceChildren() is { } children)
{
var childCount = children.Count;
for (var i = 0; i < childCount; ++i)
(children[i] as StyledElement)?.InvalidateStyles(recurse);
}
}
private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted)
{
if (o is StyledElement element)
@ -736,26 +743,28 @@ namespace Avalonia
var theme = GetEffectiveTheme();
if (theme is not null)
ApplyControlTheme(theme);
ApplyControlTheme(theme, FrameType.Theme);
if (TemplatedParent is StyledElement styleableParent &&
styleableParent.GetEffectiveTheme() is { } parentTheme)
{
ApplyControlTheme(parentTheme);
ApplyControlTheme(parentTheme, FrameType.TemplatedParentTheme);
}
}
private void ApplyControlTheme(ControlTheme theme)
private void ApplyControlTheme(ControlTheme theme, FrameType type)
{
Debug.Assert(type is FrameType.Theme or FrameType.TemplatedParentTheme);
if (theme.BasedOn is ControlTheme basedOn)
ApplyControlTheme(basedOn);
ApplyControlTheme(basedOn, type);
theme.TryAttach(this, null);
theme.TryAttach(this, type);
if (theme.HasChildren)
{
foreach (var child in theme.Children)
ApplyStyle(child, null);
ApplyStyle(child, null, type);
}
}
@ -768,17 +777,17 @@ namespace Avalonia
if (host.IsStylesInitialized)
{
foreach (var style in host.Styles)
ApplyStyle(style, host);
ApplyStyle(style, host, FrameType.Style);
}
}
private void ApplyStyle(IStyle style, IStyleHost? host)
private void ApplyStyle(IStyle style, IStyleHost? host, FrameType type)
{
if (style is Style s)
s.TryAttach(this, host);
s.TryAttach(this, host, type);
foreach (var child in style.Children)
ApplyStyle(child, host);
ApplyStyle(child, host, type);
}
private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e)
@ -824,7 +833,7 @@ namespace Avalonia
{
_logicalRoot = null;
_implicitTheme = null;
DetachStyles();
InvalidateStyles(recurse: false);
OnDetachedFromLogicalTree(e);
DetachedFromLogicalTree?.Invoke(this, e);
@ -886,70 +895,81 @@ namespace Avalonia
}
}
private void DetachStyles(IReadOnlyList<StyleBase>? styles = null)
private void DetachStyles(IReadOnlyList<Style> styles)
{
var valueStore = GetValueStore();
var values = GetValueStore();
values.BeginStyling();
try { values.RemoveFrames(styles); }
finally { values.EndStyling(); }
valueStore.BeginStyling();
for (var i = valueStore.Frames.Count - 1; i >= 0; --i)
if (_logicalChildren is not null)
{
if (valueStore.Frames[i] is StyleInstance si &&
(styles is null || styles.Contains(si.Source)))
var childCount = _logicalChildren.Count;
for (var i = 0; i < childCount; ++i)
{
valueStore.RemoveFrame(si);
(_logicalChildren[i] as StyledElement)?.DetachStyles(styles);
}
}
valueStore.EndStyling();
_styled = false;
}
private void InvalidateStylesOnThisAndDescendents()
private void NotifyResourcesChanged(
ResourcesChangedEventArgs? e = null,
bool propagate = true)
{
InvalidateStyles();
if (_logicalChildren is object)
if (ResourcesChanged is object)
{
var childCount = _logicalChildren.Count;
e ??= ResourcesChangedEventArgs.Empty;
ResourcesChanged(this, e);
}
for (var i = 0; i < childCount; ++i)
{
(_logicalChildren[i] as StyledElement)?.InvalidateStylesOnThisAndDescendents();
}
if (propagate)
{
e ??= ResourcesChangedEventArgs.Empty;
NotifyChildResourcesChanged(e);
}
}
private void DetachStylesFromThisAndDescendents(IReadOnlyList<StyleBase> styles)
private static IReadOnlyList<Style>? FlattenStyles(IReadOnlyList<IStyle> styles)
{
DetachStyles(styles);
List<Style>? result = null;
if (_logicalChildren is object)
static void FlattenStyle(IStyle style, ref List<Style>? result)
{
var childCount = _logicalChildren.Count;
if (style is Style s)
(result ??= new()).Add(s);
FlattenStyles(style.Children, ref result);
}
for (var i = 0; i < childCount; ++i)
{
(_logicalChildren[i] as StyledElement)?.DetachStylesFromThisAndDescendents(styles);
}
static void FlattenStyles(IReadOnlyList<IStyle> styles, ref List<Style>? result)
{
var count = styles.Count;
for (var i = 0; i < count; ++i)
FlattenStyle(styles[i], ref result);
}
FlattenStyles(styles, ref result);
return result;
}
private void NotifyResourcesChanged(
ResourcesChangedEventArgs? e = null,
bool propagate = true)
private static bool HasSettersOrAnimations(IReadOnlyList<IStyle> styles)
{
if (ResourcesChanged is object)
static bool StyleHasSettersOrAnimations(IStyle style)
{
e ??= ResourcesChangedEventArgs.Empty;
ResourcesChanged(this, e);
if (style is StyleBase s && s.HasSettersOrAnimations)
return true;
return HasSettersOrAnimations(style.Children);
}
if (propagate)
var count = styles.Count;
for (var i = 0; i < count; ++i)
{
e ??= ResourcesChangedEventArgs.Empty;
NotifyChildResourcesChanged(e);
if (StyleHasSettersOrAnimations(styles[i]))
return true;
}
return false;
}
private static IReadOnlyList<StyleBase> RecurseStyles(IReadOnlyList<IStyle> styles)

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

@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using Avalonia.PropertyStore;
namespace Avalonia.Styling
@ -36,8 +37,10 @@ namespace Avalonia.Styling
throw new InvalidOperationException("ControlThemes cannot be added as a nested style.");
}
internal override SelectorMatchResult TryAttach(IStyleable target, object? host)
internal SelectorMatchResult TryAttach(IStyleable target, FrameType type)
{
Debug.Assert(type is FrameType.Theme or FrameType.TemplatedParentTheme);
_ = target ?? throw new ArgumentNullException(nameof(target));
if (TargetType is null)
@ -45,7 +48,7 @@ namespace Avalonia.Styling
if (HasSettersOrAnimations && TargetType.IsAssignableFrom(target.StyleKey))
{
Attach(target, null);
Attach(target, null, type);
return SelectorMatchResult.AlwaysThisType;
}

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

@ -24,7 +24,5 @@ namespace Avalonia.Styling
/// Gets the template parent of this element if the control comes from a template.
/// </summary>
ITemplatedControl? TemplatedParent { get; }
void DetachStyles();
}
}

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

@ -1,4 +1,5 @@
using System;
using Avalonia.PropertyStore;
namespace Avalonia.Styling
{
@ -58,7 +59,7 @@ namespace Avalonia.Styling
base.SetParent(parent);
}
internal override SelectorMatchResult TryAttach(IStyleable target, object? host)
internal SelectorMatchResult TryAttach(IStyleable target, object? host, FrameType type)
{
_ = target ?? throw new ArgumentNullException(nameof(target));
@ -73,7 +74,7 @@ namespace Avalonia.Styling
if (match.IsMatch)
{
Attach(target, match.Activator);
Attach(target, match.Activator, type);
}
result = match.Result;

6
src/Avalonia.Base/Styling/StyleBase.cs

@ -92,9 +92,7 @@ namespace Avalonia.Styling
return false;
}
internal abstract SelectorMatchResult TryAttach(IStyleable target, object? host);
internal ValueFrame Attach(IStyleable target, IStyleActivator? activator)
internal ValueFrame Attach(IStyleable target, IStyleActivator? activator, FrameType type)
{
if (target is not AvaloniaObject ao)
throw new InvalidOperationException("Styles can only be applied to AvaloniaObjects.");
@ -109,7 +107,7 @@ namespace Avalonia.Styling
{
var canShareInstance = activator is null;
instance = new StyleInstance(this, activator);
instance = new StyleInstance(this, activator, type);
if (_setters is not null)
{

13
src/Avalonia.Base/Styling/StyleInstance.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reactive.Subjects;
using Avalonia.Animation;
using Avalonia.Data;
@ -27,10 +26,13 @@ namespace Avalonia.Styling
private List<IAnimation>? _animations;
private Subject<bool>? _animationTrigger;
public StyleInstance(IStyle style, IStyleActivator? activator)
public StyleInstance(
IStyle style,
IStyleActivator? activator,
FrameType type)
: base(GetPriority(activator), type)
{
_activator = activator;
Priority = activator is object ? BindingPriority.StyleTrigger : BindingPriority.Style;
Source = style;
}
@ -99,5 +101,10 @@ namespace Avalonia.Styling
hasChanged = _isActive != previous;
return _isActive;
}
private static BindingPriority GetPriority(IStyleActivator? activator)
{
return activator is not null ? BindingPriority.StyleTrigger : BindingPriority.Style;
}
}
}

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

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.PropertyStore;
namespace Avalonia.Styling
{
@ -233,7 +234,7 @@ namespace Avalonia.Styling
{
if (s is not Style style)
continue;
var r = style.TryAttach(target, host);
var r = style.TryAttach(target, host, FrameType.Style);
if (r > result)
result = r;
}

2
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -1296,7 +1296,7 @@ namespace Avalonia.Controls
public event EventHandler<SelectionChangedEventArgs> SelectionChanged
{
add { AddHandler(SelectionChangedEvent, value); }
remove { AddHandler(SelectionChangedEvent, value); }
remove { RemoveHandler(SelectionChangedEvent, value); }
}
/// <summary>

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

@ -205,7 +205,7 @@ namespace Avalonia.Controls.Generators
result.SetValue(
StyledElement.ThemeProperty,
ItemContainerTheme,
BindingPriority.TemplatedParent);
BindingPriority.Template);
}
return result;

59
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -5,6 +5,7 @@ using Avalonia.Interactivity;
using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.VisualTree;
@ -395,56 +396,36 @@ namespace Avalonia.Controls.Primitives
}
}
internal override void DetachControlThemeFromTemplateChildren(ControlTheme theme)
private protected override void OnControlThemeChanged()
{
static ControlTheme? GetControlTheme(StyleBase style)
{
var s = style;
base.OnControlThemeChanged();
while (s is not null)
var count = VisualChildren.Count;
for (var i = 0; i < count; ++i)
{
if (VisualChildren[i] is StyledElement child &&
child.TemplatedParent == this)
{
if (s is ControlTheme c)
return c;
s = s.Parent as StyleBase;
child.OnTemplatedParentControlThemeChanged();
}
return null;
}
}
static void Detach(Visual control, ITemplatedControl templatedParent, ControlTheme theme)
{
var valueStore = control.GetValueStore();
var count = valueStore.Frames.Count;
if (control != templatedParent)
{
valueStore.BeginStyling();
for (var i = count - 1; i >= 0; --i)
{
if (valueStore.Frames[i] is StyleInstance si &&
si.Source is StyleBase style &&
GetControlTheme(style) == theme)
{
valueStore.RemoveFrame(si);
}
}
valueStore.EndStyling();
}
internal override void OnTemplatedParentControlThemeChanged()
{
base.OnTemplatedParentControlThemeChanged();
var children = ((IVisual)control).VisualChildren;
count = children.Count;
var count = VisualChildren.Count;
var templatedParent = TemplatedParent;
for (var i = 0; i < count; i++)
for (var i = 0; i < count; ++i)
{
if (VisualChildren[i] is TemplatedControl child &&
child.TemplatedParent == templatedParent)
{
if (children[i] is Visual v &&
v.TemplatedParent == templatedParent)
Detach(v, templatedParent, theme);
child.OnTemplatedParentControlThemeChanged();
}
}
Detach(this, this, theme);
}
}
}

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs

@ -42,7 +42,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
if (priorityValueSetters.Count > 0)
{
prop.PossibleSetters = priorityValueSetters;
prop.Values.Insert(0, new XamlConstantNode(node, bindingPriorityType, (int)BindingPriority.TemplatedParent));
prop.Values.Insert(0, new XamlConstantNode(node, bindingPriorityType, (int)BindingPriority.Template));
}
}

2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs

@ -27,7 +27,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public IBinding ProvideValue(IServiceProvider serviceProvider)
{
if (serviceProvider.IsInControlTemplate())
_priority = BindingPriority.TemplatedParent;
_priority = BindingPriority.Template;
var provideTarget = serviceProvider.GetService<IProvideValueTarget>();

2
src/Markup/Avalonia.Markup/Data/TemplateBinding.cs

@ -51,7 +51,7 @@ namespace Avalonia.Data
return new InstancedBinding(
this,
Mode == BindingMode.Default ? BindingMode.OneWay : Mode,
BindingPriority.TemplatedParent);
BindingPriority.Template);
}
else
{

3
tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Shapes;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
@ -435,7 +436,7 @@ namespace Avalonia.Base.UnitTests.Animation
}
};
style.TryAttach(control, control);
StyleHelpers.TryAttach(style, control);
// Which means that the transition state hasn't been initialized with the new
// Transitions when the Opacity change notification gets raised here.

2
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs

@ -251,7 +251,7 @@ namespace Avalonia.Base.UnitTests
{
Class1 target = new Class1();
target.SetValue(Class1.FooProperty, "one", BindingPriority.TemplatedParent);
target.SetValue(Class1.FooProperty, "one", BindingPriority.Template);
Assert.Equal("one", target.GetValue(Class1.FooProperty));
target.SetValue(Class1.FooProperty, "two", BindingPriority.Style);
Assert.Equal("one", target.GetValue(Class1.FooProperty));

2
tests/Avalonia.Base.UnitTests/PropertyStore/ValueStoreTests_Frames.cs

@ -117,7 +117,7 @@ namespace Avalonia.Base.UnitTests.PropertyStore
private static StyleInstance InstanceStyle(Style style, StyledElement target)
{
var result = new StyleInstance(style, null);
var result = new StyleInstance(style, null, FrameType.Style);
foreach (var setter in style.Setters)
result.Add(setter.Instance(result, target));

3
tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs

@ -6,6 +6,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Media;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
@ -503,7 +504,7 @@ namespace Avalonia.Base.UnitTests.Styling
private void Apply(Style style, Control control)
{
style.TryAttach(control, null);
StyleHelpers.TryAttach(style, control);
}
private void Apply(Setter setter, Control control)

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

@ -5,6 +5,7 @@ using Avalonia.Base.UnitTests.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
@ -27,7 +28,7 @@ namespace Avalonia.Base.UnitTests.Styling
var target = new Class1();
style.TryAttach(target, null);
StyleHelpers.TryAttach(style, target);
Assert.Equal("Foo", target.Foo);
}
@ -45,7 +46,7 @@ namespace Avalonia.Base.UnitTests.Styling
var target = new Class1();
style.TryAttach(target, null);
StyleHelpers.TryAttach(style, target);
Assert.Equal("foodefault", target.Foo);
target.Classes.Add("foo");
Assert.Equal("Foo", target.Foo);
@ -66,7 +67,7 @@ namespace Avalonia.Base.UnitTests.Styling
var target = new Class1();
style.TryAttach(target, target);
StyleHelpers.TryAttach(style, target);
Assert.Equal("Foo", target.Foo);
}
@ -92,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Styling
var target = new Class1();
var other = new Class1();
style.TryAttach(target, other);
StyleHelpers.TryAttach(style, target, host: other);
Assert.Equal("foodefault", target.Foo);
}
@ -113,7 +114,7 @@ namespace Avalonia.Base.UnitTests.Styling
Foo = "Original",
};
style.TryAttach(target, null);
StyleHelpers.TryAttach(style, target);
Assert.Equal("Original", target.Foo);
}
@ -577,7 +578,7 @@ namespace Avalonia.Base.UnitTests.Styling
Child = border = new Border(),
};
style.TryAttach(border, null);
StyleHelpers.TryAttach(style, border);
Assert.Equal(new Thickness(4), border.BorderThickness);
root.Child = null;
@ -617,15 +618,15 @@ namespace Avalonia.Base.UnitTests.Styling
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Border>())
{
new Style(x => x.OfType<Border>())
Setters =
{
Setters =
{
new Setter(Border.BorderThicknessProperty, new Thickness(4)),
}
new Setter(Border.BorderThicknessProperty, new Thickness(4)),
}
},
}
},
Child = border,
};
@ -635,9 +636,9 @@ namespace Avalonia.Base.UnitTests.Styling
root.Styles.Add(new Style(x => x.OfType<Border>())
{
Setters =
{
new Setter(Border.BorderThicknessProperty, new Thickness(6)),
}
{
new Setter(Border.BorderThicknessProperty, new Thickness(6)),
}
});
root.Measure(Size.Infinity);
@ -651,18 +652,18 @@ namespace Avalonia.Base.UnitTests.Styling
var root = new TestRoot
{
Styles =
{
new Styles
{
new Styles
new Style(x => x.OfType<Border>())
{
new Style(x => x.OfType<Border>())
Setters =
{
Setters =
{
new Setter(Border.BorderThicknessProperty, new Thickness(4)),
}
new Setter(Border.BorderThicknessProperty, new Thickness(4)),
}
}
},
}
},
Child = border,
};
@ -749,7 +750,34 @@ namespace Avalonia.Base.UnitTests.Styling
}
[Fact]
public void DetachStyles_Should_Detach_Activator()
public void Adding_Style_With_No_Setters_Or_Animations_Should_Not_Invalidate_Styles()
{
var border = new Border();
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Border>())
{
Setters =
{
new Setter(Border.BorderThicknessProperty, new Thickness(4)),
}
}
},
Child = border,
};
root.Measure(Size.Infinity);
Assert.Equal(new Thickness(4), border.BorderThickness);
root.Styles.Add(new Style(x => x.OfType<Border>()));
Assert.Equal(new Thickness(4), border.BorderThickness);
}
[Fact]
public void Invalidating_Styles_Should_Detach_Activator()
{
Style style = new Style(x => x.OfType<Class1>().Class("foo"))
{
@ -761,11 +789,11 @@ namespace Avalonia.Base.UnitTests.Styling
var target = new Class1();
style.TryAttach(target, null);
StyleHelpers.TryAttach(style, target);
Assert.Equal(1, target.Classes.ListenerCount);
((IStyleable)target).DetachStyles();
target.InvalidateStyles(recurse: false);
Assert.Equal(0, target.Classes.ListenerCount);
}
@ -874,7 +902,7 @@ namespace Avalonia.Base.UnitTests.Styling
var clock = new TestClock();
var target = new Class1 { Clock = clock };
style.TryAttach(target, null);
StyleHelpers.TryAttach(style, target);
Assert.Equal(0.0, target.Double);

218
tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs

@ -23,7 +23,7 @@ public class StyledElementTests_Theming
Assert.Null(target.Template);
var root = CreateRoot(target);
CreateRoot(target);
Assert.NotNull(target.Template);
var border = Assert.IsType<Border>(target.VisualChild);
@ -43,7 +43,7 @@ public class StyledElementTests_Theming
Assert.Null(target.Template);
var root = CreateRoot(target);
CreateRoot(target);
Assert.NotNull(target.Template);
var border = Assert.IsType<Border>(target.VisualChild);
@ -57,7 +57,7 @@ public class StyledElementTests_Theming
public void Theme_Is_Detached_When_Theme_Property_Cleared()
{
var target = CreateTarget();
var root = CreateRoot(target);
CreateRoot(target);
Assert.NotNull(target.Template);
@ -66,7 +66,47 @@ public class StyledElementTests_Theming
}
[Fact]
public void Theme_Is_Detached_From_Template_Controls_When_Theme_Property_Cleared()
public void Setting_Explicit_Theme_Detaches_Default_Theme()
{
var target = new ThemedControl();
var root = new TestRoot
{
Resources = { { typeof(ThemedControl), CreateTheme() } },
Child = target,
};
root.LayoutManager.ExecuteInitialLayoutPass();
Assert.Equal("theme", target.Tag);
target.Theme = new ControlTheme(typeof(ThemedControl))
{
Setters =
{
new Setter(ThemedControl.BackgroundProperty, Brushes.Yellow),
}
};
root.LayoutManager.ExecuteLayoutPass();
Assert.Null(target.Tag);
Assert.Equal(Brushes.Yellow, target.Background);
}
[Fact]
public void Unrelated_Styles_Are_Not_Detached_When_Theme_Property_Cleared()
{
var target = CreateTarget();
CreateRoot(target, createAdditionalStyles: true);
Assert.Equal("style", target.Tag);
target.Theme = null;
Assert.Equal("style", target.Tag);
}
[Fact]
public void TemplatedParent_Theme_Is_Detached_From_Template_Controls_When_Theme_Property_Cleared()
{
var theme = new ControlTheme
{
@ -93,10 +133,115 @@ public class StyledElementTests_Theming
target.Theme = null;
Assert.IsType<Canvas>(target.VisualChild);
Assert.Same(canvas, target.VisualChild);
Assert.Null(canvas.Background);
}
[Fact]
public void Primary_Theme_Is_Not_Detached_From_Template_Controls_When_Theme_Property_Cleared()
{
var templatedParentTheme = new ControlTheme
{
TargetType = typeof(ThemedControl),
Children =
{
new Style(x => x.Nesting().Template().OfType<Button>())
{
Setters =
{
new Setter(Panel.BackgroundProperty, Brushes.Red),
}
},
}
};
var childTheme = new ControlTheme
{
TargetType = typeof(Button),
Setters =
{
new Setter(TemplatedControl.ForegroundProperty, Brushes.Green),
}
};
var target = CreateTarget(templatedParentTheme);
target.Template = new FuncControlTemplate<ThemedControl>((o, n) => new Button
{
Theme = childTheme,
});
var root = CreateRoot(target, createAdditionalStyles: true);
var templateChild = Assert.IsType<Button>(target.VisualChild);
Assert.Equal(Brushes.Red, templateChild.Background);
Assert.Equal(Brushes.Green, templateChild.Foreground);
target.Theme = null;
Assert.Null(templateChild.Background);
Assert.Equal(Brushes.Green, templateChild.Foreground);
}
[Fact]
public void TemplatedParent_Theme_Is_Not_Detached_From_Template_Controls_When_Primary_Theme_Property_Cleared()
{
var templatedParentTheme = new ControlTheme
{
TargetType = typeof(ThemedControl),
Children =
{
new Style(x => x.Nesting().Template().OfType<Button>())
{
Setters =
{
new Setter(Panel.BackgroundProperty, Brushes.Red),
}
},
}
};
var childTheme = new ControlTheme
{
TargetType = typeof(Button),
Setters =
{
new Setter(Button.TagProperty, "childTheme"),
}
};
var target = CreateTarget(templatedParentTheme);
target.Template = new FuncControlTemplate<ThemedControl>((o, n) => new Button
{
Theme = childTheme,
});
var root = CreateRoot(target, createAdditionalStyles: true);
var templateChild = Assert.IsType<Button>(target.VisualChild);
Assert.Equal(Brushes.Red, templateChild.Background);
Assert.Equal("childTheme", templateChild.Tag);
templateChild.Theme = null;
Assert.Equal(Brushes.Red, templateChild.Background);
Assert.Null(templateChild.Tag);
}
[Fact]
public void Unrelated_Styles_Are_Not_Detached_From_Template_Controls_When_Theme_Property_Cleared()
{
var target = CreateTarget();
var root = CreateRoot(target, createAdditionalStyles: true);
var canvas = Assert.IsType<Border>(target.VisualChild);
Assert.Equal("style", canvas.Tag);
target.Theme = null;
Assert.Same(canvas, target.VisualChild);
Assert.Equal("style", canvas.Tag);
}
[Fact]
public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes()
{
@ -122,7 +267,7 @@ public class StyledElementTests_Theming
Assert.Null(target.Template);
var root = CreateRoot(target);
CreateRoot(target);
Assert.NotNull(target.Template);
Assert.Equal(Brushes.Blue, target.BorderBrush);
@ -135,6 +280,29 @@ public class StyledElementTests_Theming
Assert.Equal(Brushes.Cyan, border.BorderBrush);
}
[Fact]
public void Theme_Has_Lower_Priority_Than_Style()
{
var target = CreateTarget();
CreateRoot(target, createAdditionalStyles: true);
Assert.Equal("style", target.Tag);
}
[Fact]
public void Theme_Has_Lower_Priority_Than_Style_After_Change()
{
var target = CreateTarget();
var theme = target.Theme;
CreateRoot(target, createAdditionalStyles: true);
target.Theme = null;
target.Theme = theme;
target.ApplyStyling();
Assert.Equal("style", target.Tag);
}
private static ThemedControl CreateTarget(ControlTheme? theme = null)
{
return new ThemedControl
@ -143,9 +311,32 @@ public class StyledElementTests_Theming
};
}
private static TestRoot CreateRoot(IControl child)
private static TestRoot CreateRoot(
IControl child,
bool createAdditionalStyles = false)
{
var result = new TestRoot(child);
var result = new TestRoot();
if (createAdditionalStyles)
{
result.Styles.Add(new Style(x => x.OfType<ThemedControl>())
{
Setters =
{
new Setter(Control.TagProperty, "style"),
}
});
result.Styles.Add(new Style(x => x.OfType<Border>())
{
Setters =
{
new Setter(Control.TagProperty, "style"),
}
});
}
result.Child = child;
result.LayoutManager.ExecuteInitialLayoutPass();
return result;
}
@ -157,7 +348,7 @@ public class StyledElementTests_Theming
public void Implicit_Theme_Is_Applied_When_Attached_To_Logical_Tree()
{
var target = CreateTarget();
var root = CreateRoot(target);
CreateRoot(target);
Assert.NotNull(target.Template);
var border = Assert.IsType<Border>(target.VisualChild);
@ -231,7 +422,7 @@ public class StyledElementTests_Theming
Assert.Null(target.Theme);
Assert.Null(target.Template);
var root = CreateRoot(target);
CreateRoot(target);
Assert.NotNull(target.Theme);
Assert.NotNull(target.Template);
@ -348,6 +539,7 @@ public class StyledElementTests_Theming
TargetType = typeof(ThemedControl),
Setters =
{
new Setter(Control.TagProperty, "theme"),
new Setter(TemplatedControl.TemplateProperty, template),
new Setter(TemplatedControl.CornerRadiusProperty, new CornerRadius(5)),
},
@ -355,7 +547,11 @@ public class StyledElementTests_Theming
{
new Style(x => x.Nesting().Template().OfType<Border>())
{
Setters = { new Setter(Border.BackgroundProperty, Brushes.Red) }
Setters =
{
new Setter(Border.BackgroundProperty, Brushes.Red),
new Setter(Control.TagProperty, "theme"),
}
},
new Style(x => x.Nesting().Class("foo").Template().OfType<Border>())
{

2
tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs

@ -43,7 +43,7 @@ namespace Avalonia.Benchmarks.Base
for (var i = 0; i < 100; ++i)
{
obj.SetValue(StyledClass.IntValueProperty, obj.IntValue + 1, BindingPriority.TemplatedParent);
obj.SetValue(StyledClass.IntValueProperty, obj.IntValue + 1, BindingPriority.Template);
}
}

149
tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs

@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Styling
{
[MemoryDiagnoser]
public class ControlTheme_Change : IDisposable
{
private readonly IDisposable _app;
private readonly TestRoot _root;
private readonly TextBox _control;
private readonly ControlTheme _theme1;
private readonly ControlTheme _theme2;
public ControlTheme_Change()
{
_app = UnitTestApplication.Start(
TestServices.StyledWindow.With(
renderInterface: new NullRenderingPlatform(),
threadingInterface: new NullThreadingPlatform()));
// Simulate an application with a lot of styles by creating a tree of nested panels,
// each with a bunch of styles applied.
var (rootPanel, leafPanel) = CreateNestedPanels(10);
// We're benchmarking how long it takes to switch control theme on a TextBox in this
// situation.
var baseTheme = (ControlTheme)Application.Current.FindResource(typeof(TextBox)) ??
throw new Exception("Base TextBox theme not found.");
_theme1 = new ControlTheme(typeof(TextBox))
{
BasedOn = baseTheme,
Setters = { new Setter(TextBox.BackgroundProperty, Brushes.Red) },
};
_theme2 = new ControlTheme(typeof(TextBox))
{
BasedOn = baseTheme,
Setters = { new Setter(TextBox.BackgroundProperty, Brushes.Green) },
};
_control = new TextBox { Theme = _theme1 };
leafPanel.Children.Add(_control);
_root = new TestRoot(true, rootPanel)
{
Renderer = new NullRenderer(),
};
_root.LayoutManager.ExecuteInitialLayoutPass();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void Change_ControlTheme()
{
if (_control.Background != Brushes.Red)
throw new Exception("Invalid benchmark state");
_control.Theme = _theme2;
_root.LayoutManager.ExecuteLayoutPass();
if (_control.Background != Brushes.Green)
throw new Exception("Invalid benchmark state");
_control.Theme = _theme1;
_root.LayoutManager.ExecuteLayoutPass();
if (_control.Background != Brushes.Red)
throw new Exception("Invalid benchmark state");
}
public void Dispose()
{
_app.Dispose();
}
private static (Panel, Panel) CreateNestedPanels(int count)
{
var root = new Panel();
var last = root;
for (var i = 0; i < count; ++i)
{
var panel = new Panel();
panel.Styles.AddRange(CreateStyles());
last.Children.Add(panel);
last = panel;
}
return (root, last);
}
private static IEnumerable<IStyle> CreateStyles()
{
var types = new[]
{
typeof(Border),
typeof(Button),
typeof(ButtonSpinner),
typeof(Carousel),
typeof(CheckBox),
typeof(ComboBox),
typeof(ContentControl),
typeof(Expander),
typeof(ItemsControl),
typeof(Label),
typeof(ListBox),
typeof(ProgressBar),
typeof(RadioButton),
typeof(RepeatButton),
typeof(ScrollViewer),
typeof(Slider),
typeof(Spinner),
typeof(SplitView),
typeof(TextBox),
typeof(ToggleSwitch),
typeof(TreeView),
typeof(Viewbox),
typeof(Window),
};
foreach (var type in types)
{
yield return new Style(x => x.OfType(type))
{
Setters = { new Setter(Control.TagProperty, type.Name) }
};
yield return new Style(x => x.OfType(type).Class("foo"))
{
Setters = { new Setter(Control.TagProperty, type.Name + " foo") }
};
yield return new Style(x => x.OfType(type).Class("bar"))
{
Setters = { new Setter(Control.TagProperty, type.Name + " bar") }
};
}
}
}
}

5
tests/Avalonia.Benchmarks/Styling/Style_Activation.cs

@ -1,6 +1,8 @@
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
#nullable enable
@ -26,8 +28,7 @@ namespace Avalonia.Benchmarks.Styling
{
Setters = { new Setter(TestClass.StringProperty, "foo") }
};
style.TryAttach(_target, null);
StyleHelpers.TryAttach(style, _target);
}
[Benchmark]

4
tests/Avalonia.Benchmarks/Styling/Style_Apply.cs

@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
#nullable enable
@ -56,7 +58,7 @@ namespace Avalonia.Benchmarks.Styling
target.GetValueStore().BeginStyling();
foreach (var style in _styles)
style.TryAttach(target, null);
StyleHelpers.TryAttach(style, target);
target.GetValueStore().EndStyling();
}

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

@ -45,7 +45,7 @@ namespace Avalonia.Benchmarks.Styling
if ((string)_control.Tag != "TextBox")
throw new Exception("Invalid benchmark state");
((IStyleable)_control).DetachStyles();
_control.InvalidateStyles(true);
if (_control.Tag is not null)
throw new Exception("Invalid benchmark state");

10
tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs

@ -1,7 +1,9 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
#nullable enable
@ -35,7 +37,7 @@ namespace Avalonia.Benchmarks.Styling
target.GetValueStore().BeginStyling();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
StyleHelpers.TryAttach(_style, target);
target.GetValueStore().EndStyling();
}
@ -48,7 +50,7 @@ namespace Avalonia.Benchmarks.Styling
target.GetValueStore().BeginStyling();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
StyleHelpers.TryAttach(_style, target);
target.GetValueStore().EndStyling();
@ -64,7 +66,7 @@ namespace Avalonia.Benchmarks.Styling
target.GetValueStore().BeginStyling();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
StyleHelpers.TryAttach(_style, target);
target.GetValueStore().EndStyling();
@ -75,7 +77,7 @@ namespace Avalonia.Benchmarks.Styling
{
public static readonly StyledProperty<string?> StringProperty =
AvaloniaProperty.Register<TestClass, string?>("String");
public void DetachStyles() => InvalidateStyles();
public void DetachStyles() => InvalidateStyles(recurse: true);
}
private class TestClass2 : Control

4
tests/Avalonia.Benchmarks/Styling/Style_NonActive.cs

@ -1,6 +1,8 @@
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
#nullable enable
@ -27,7 +29,7 @@ namespace Avalonia.Benchmarks.Styling
Setters = { new Setter(TestClass.StringProperty, "foo") }
};
style.TryAttach(_target, null);
StyleHelpers.TryAttach(style, _target);
_target.SetValue(TestClass.StringProperty, "foo");
_target.SetValue(TestClass.Struct1Property, new(1));
_target.SetValue(TestClass.Struct2Property, new(1));

4
tests/Avalonia.Controls.UnitTests/ContentControlTests.cs

@ -292,13 +292,13 @@ namespace Avalonia.Controls.UnitTests
// as they are in Avalonia.Markup.Xaml.
DelayedBinding.Add(presenter, ContentPresenter.ContentProperty, new Binding("Content")
{
Priority = BindingPriority.TemplatedParent,
Priority = BindingPriority.Template,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
});
DelayedBinding.Add(presenter, ContentPresenter.ContentTemplateProperty, new Binding("ContentTemplate")
{
Priority = BindingPriority.TemplatedParent,
Priority = BindingPriority.Template,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
});

4
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@ -895,14 +895,14 @@ namespace Avalonia.Controls.UnitTests
{
Path = nameof(TextPresenter.Text),
Mode = BindingMode.TwoWay,
Priority = BindingPriority.TemplatedParent,
Priority = BindingPriority.Template,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
},
[!!TextPresenter.CaretIndexProperty] = new Binding
{
Path = nameof(TextPresenter.CaretIndex),
Mode = BindingMode.TwoWay,
Priority = BindingPriority.TemplatedParent,
Priority = BindingPriority.Template,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
}
}.RegisterInNameScope(scope));

4
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -1061,14 +1061,14 @@ namespace Avalonia.Controls.UnitTests
{
Path = nameof(TextPresenter.Text),
Mode = BindingMode.TwoWay,
Priority = BindingPriority.TemplatedParent,
Priority = BindingPriority.Template,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
},
[!!TextPresenter.CaretIndexProperty] = new Binding
{
Path = nameof(TextPresenter.CaretIndex),
Mode = BindingMode.TwoWay,
Priority = BindingPriority.TemplatedParent,
Priority = BindingPriority.Template,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
}
}.RegisterInNameScope(scope));

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

@ -100,7 +100,7 @@ namespace Avalonia.Controls.UnitTests
{
Path = "Text",
Mode = BindingMode.TwoWay,
Priority = BindingPriority.TemplatedParent,
Priority = BindingPriority.Template,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
},
}.RegisterInNameScope(scope));

2
tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs

@ -54,7 +54,7 @@ namespace Avalonia.Markup.UnitTests.Data
DataContext = new Class1(),
};
var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.TemplatedParent };
var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.Template };
var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true);
var subject = (BindingExpression)instanced.Subject;
object result = null;

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

@ -2,6 +2,7 @@ using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit;
@ -54,7 +55,7 @@ namespace Avalonia.Markup.Xaml.UnitTests
}
};
style.TryAttach(control, control);
StyleHelpers.TryAttach(style, control);
Assert.Equal("foo", control.Text);
control.Text = "bar";

12
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs

@ -77,7 +77,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(Brushes.Red, presenter.Background);
var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty);
Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority);
Assert.Equal(BindingPriority.Template, diagnostic.Priority);
}
}
@ -111,7 +111,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(Brushes.Red, presenter.Background);
var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty);
Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority);
Assert.Equal(BindingPriority.Template, diagnostic.Priority);
}
}
@ -142,7 +142,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(Dock.Top, DockPanel.GetDock(presenter));
var diagnostic = presenter.GetDiagnostic(DockPanel.DockProperty);
Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority);
Assert.Equal(BindingPriority.Template, diagnostic.Priority);
}
}
@ -176,7 +176,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(Brushes.Red, presenter.Background);
var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty);
Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority);
Assert.Equal(BindingPriority.Template, diagnostic.Priority);
}
}
@ -210,7 +210,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(Brushes.Red, presenter.Background);
var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty);
Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority);
Assert.Equal(BindingPriority.Template, diagnostic.Priority);
}
}
@ -241,7 +241,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("Foo", presenter.Content);
var diagnostic = presenter.GetDiagnostic(ContentPresenter.ContentProperty);
Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority);
Assert.Equal(BindingPriority.Template, diagnostic.Priority);
}
}

14
tests/Avalonia.UnitTests/StyleHelpers.cs

@ -0,0 +1,14 @@
using Avalonia.Styling;
#nullable enable
namespace Avalonia.UnitTests
{
public static class StyleHelpers
{
public static SelectorMatchResult TryAttach(Style style, StyledElement element, object? host = null)
{
return style.TryAttach(element, host ?? element, PropertyStore.FrameType.Style);
}
}
}
Loading…
Cancel
Save