using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.Text; using Avalonia.Animation; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.PropertyStore; using Avalonia.Styling; namespace Avalonia { /// /// Extends an with the following features: /// /// - An inherited . /// - Implements to allow styling to work on the styled element. /// - Implements to form part of a logical tree. /// - A collection of class strings for custom styling. /// public class StyledElement : Animatable, IDataContextProvider, ILogical, IThemeVariantHost, IResourceHost2, IStyleHost, ISetLogicalParent, ISetInheritanceParent, ISupportInitialize, INamed, IAvaloniaListItemValidator, #pragma warning disable CS0618 // Type or member is obsolete IStyleable #pragma warning restore CS0618 // Type or member is obsolete { /// /// Defines the property. /// public static readonly StyledProperty DataContextProperty = AvaloniaProperty.Register( nameof(DataContext), defaultValue: null, inherits: true, defaultBindingMode: BindingMode.OneWay, validate: null, coerce: null, enableDataValidation: false, notifying: DataContextNotifying); /// /// Defines the property. /// public static readonly DirectProperty NameProperty = AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); /// /// Defines the property. /// public static readonly DirectProperty ParentProperty = AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); /// /// Defines the property. /// public static readonly DirectProperty TemplatedParentProperty = AvaloniaProperty.RegisterDirect( nameof(TemplatedParent), o => o.TemplatedParent); /// /// Defines the property. /// public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; private Classes? _classes; private ILogicalRoot? _logicalRoot; private AvaloniaList? _logicalChildren; private IResourceDictionary? _resources; private Styles? _styles; private bool _stylesApplied; private bool _themeApplied; private bool _templatedParentThemeApplied; private AvaloniaObject? _templatedParent; private bool _dataContextUpdating; private ControlTheme? _implicitTheme; private EventHandler? _resourcesChanged2; private ResourcesChangedToken _lastResourcesChangedToken; /// /// Initializes static members of the class. /// static StyledElement() { DataContextProperty.Changed.AddClassHandler((x,e) => x.OnDataContextChangedCore(e)); } /// /// Initializes a new instance of the class. /// public StyledElement() { _logicalRoot = this as ILogicalRoot; } /// /// Raised when the styled element is attached to a rooted logical tree. /// public event EventHandler? AttachedToLogicalTree; /// /// Raised when the styled element is detached from a rooted logical tree. /// public event EventHandler? DetachedFromLogicalTree; /// /// Occurs when the property changes. /// /// /// This event will be raised when the property has changed and /// all subscribers to that change have been notified. /// public event EventHandler? DataContextChanged; /// /// Occurs when the styled element has finished initialization. /// /// /// The Initialized event indicates that all property values on the styled element have been set. /// When loading the styled element from markup, it occurs when /// is called *and* the styled element /// is attached to a rooted logical tree. When the styled element is created by code and /// is not used, it is called when the styled element is attached /// to the visual tree. /// public event EventHandler? Initialized; /// /// Occurs when a resource in this styled element or a parent styled element has changed. /// public event EventHandler? ResourcesChanged; event EventHandler? IResourceHost2.ResourcesChanged2 { add => _resourcesChanged2 += value; remove => _resourcesChanged2 -= value; } /// public event EventHandler? ActualThemeVariantChanged; /// /// Gets or sets the name of the styled element. /// /// /// 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. /// public string? Name { get => _name; set { if (_stylesApplied) { throw new InvalidOperationException("Cannot set Name : styled element already styled."); } _name = value; } } /// /// Gets or sets the styled element's classes. /// /// /// /// Classes can be used to apply user-defined styling to styled elements, or to allow styled elements /// that share a common purpose to be easily selected. /// /// public Classes Classes => _classes ??= new(); /// /// Gets or sets the control's data context. /// /// /// The data context is an inherited property that specifies the default object that will /// be used for data binding. /// public object? DataContext { get { return GetValue(DataContextProperty); } set { SetValue(DataContextProperty, value); } } /// /// Gets a value that indicates whether the element has finished initialization. /// /// /// For more information about when IsInitialized is set, see the /// event. /// public bool IsInitialized { get; private set; } /// /// Gets the styles for the styled element. /// /// /// Styles for the entire application are added to the Application.Styles collection, but /// each styled element may in addition define its own styles which are applied to the styled element /// itself and its children. /// public Styles Styles => _styles ??= new Styles(this); /// /// Gets the type by which the element is styled. /// /// /// Usually controls are styled by their own type, but there are instances where you want /// an element to be styled by its base type, e.g. creating SpecialButton that /// derives from Button and adds extra functionality but is still styled as a regular /// Button. To change the style for a control class, override the /// property /// public Type StyleKey => StyleKeyOverride; /// /// Gets or sets the styled element's resource dictionary. /// public IResourceDictionary Resources { get => _resources ??= new ResourceDictionary(this); set { value = value ?? throw new ArgumentNullException(nameof(value)); _resources?.RemoveOwner(this); _resources = value; _resources.AddOwner(this); } } /// /// Gets the styled element whose lookless template this styled element is part of. /// public AvaloniaObject? TemplatedParent { get => _templatedParent; internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); } /// /// Gets or sets the theme to be applied to the element. /// public ControlTheme? Theme { get => GetValue(ThemeProperty); set => SetValue(ThemeProperty, value); } /// /// Gets the styled element's logical children. /// protected internal IAvaloniaList LogicalChildren { get { if (_logicalChildren == null) { var list = new AvaloniaList { ResetBehavior = ResetBehavior.Remove, Validator = this }; list.CollectionChanged += LogicalChildrenCollectionChanged; _logicalChildren = list; } return _logicalChildren; } } /// /// Gets the collection in a form that allows adding and removing /// pseudoclasses. /// protected IPseudoClasses PseudoClasses => Classes; /// /// Gets the type by which the element is styled. /// /// /// Usually controls are styled by their own type, but there are instances where you want /// an element to be styled by its base type, e.g. creating SpecialButton that /// derives from Button and adds extra functionality but is still styled as a regular /// Button. Override this property to change the style for a control class, returning the /// type that you wish the elements to be styled as. /// protected virtual Type StyleKeyOverride => GetType(); /// /// Gets a value indicating whether the element is attached to a rooted logical tree. /// bool ILogical.IsAttachedToLogicalTree => _logicalRoot != null; /// /// Gets the styled element's logical parent. /// public StyledElement? Parent { get; private set; } /// [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1030:StyledProperty accessors should not have side effects", Justification = "False positive?")] public ThemeVariant ActualThemeVariant => GetValue(ThemeVariant.ActualThemeVariantProperty); /// /// Gets the styled element's logical parent. /// ILogical? ILogical.LogicalParent => Parent; /// /// Gets the styled element's logical children. /// IAvaloniaReadOnlyList ILogical.LogicalChildren => LogicalChildren; /// bool IResourceNode.HasResources => (_resources?.HasResources ?? false) || (((IResourceNode?)_styles)?.HasResources ?? false); /// IAvaloniaReadOnlyList IStyleable.Classes => Classes; /// bool IStyleHost.IsStylesInitialized => _styles != null; /// IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; /// public virtual void BeginInit() { ++_initCount; } /// public virtual void EndInit() { if (_initCount == 0) { throw new InvalidOperationException("BeginInit was not called."); } if (--_initCount == 0 && _logicalRoot is not null) { ApplyStyling(); InitializeIfNeeded(); } } /// /// Applies styling to the control if the control is initialized and styling is not /// already applied. /// /// /// The styling system will automatically apply styling when required, so it should not /// usually be necessary to call this method manually. /// /// /// A value indicating whether styling is now applied to the control. /// public bool ApplyStyling() { if (_initCount == 0 && (!_stylesApplied || !_themeApplied || !_templatedParentThemeApplied)) { GetValueStore().BeginStyling(); try { if (!_themeApplied) { ApplyControlTheme(); _themeApplied = true; } if (!_templatedParentThemeApplied) { ApplyTemplatedParentControlTheme(); _templatedParentThemeApplied = true; } if (!_stylesApplied) { ApplyStyles(this); _stylesApplied = true; } } finally { GetValueStore().EndStyling(); } } return _stylesApplied; } protected void InitializeIfNeeded() { if (_initCount == 0 && !IsInitialized) { IsInitialized = true; OnInitialized(); Initialized?.Invoke(this, EventArgs.Empty); } } /// void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { OnAttachedToLogicalTreeCore(e); } /// void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { OnDetachedFromLogicalTreeCore(e); } /// void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(ResourcesChangedToken.Create()); /// void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(ResourcesChangedToken.Create()); void IResourceHost2.NotifyHostedResourcesChanged(ResourcesChangedToken token) => NotifyResourcesChanged(token); /// public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { value = null; return (_resources?.TryGetResource(key, theme, out value) ?? false) || (_styles?.TryGetResource(key, theme, out value) ?? false); } /// /// Sets the styled element's logical parent. /// /// The parent. void ISetLogicalParent.SetParent(ILogical? parent) { var old = Parent; if (parent != old) { if (old != null && parent != null) { throw new InvalidOperationException("The Control already has a parent."); } if (InheritanceParent == null || parent == null) { InheritanceParent = parent as AvaloniaObject; } Parent = (StyledElement?)parent; if (_logicalRoot != null) { var e = new LogicalTreeAttachmentEventArgs(_logicalRoot, this, old!); OnDetachedFromLogicalTreeCore(e); } var newRoot = FindLogicalRoot(this); if (newRoot is object) { var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent!); OnAttachedToLogicalTreeCore(e); } else if (parent is null) { // If we were attached to the logical tree, we piggyback on the tree traversal // there to raise resources changed notifications. If we're being removed from // the logical tree, then traverse the tree raising notifications now. // // We don't raise resources changed notifications if we're being attached to a // non-rooted control beacuse it's unlikely that dynamic resources need to be // correct until the control is added to the tree, and it causes a *lot* of // notifications. NotifyResourcesChanged(ResourcesChangedToken.Create()); } RaisePropertyChanged(ParentProperty, old, Parent); } } /// /// Sets the styled element's inheritance parent. /// /// The parent. void ISetInheritanceParent.SetParent(AvaloniaObject? parent) { InheritanceParent = parent; } void IStyleHost.StylesAdded(IReadOnlyList styles) { if (HasSettersOrAnimations(styles)) InvalidateStyles(recurse: true); } void IStyleHost.StylesRemoved(IReadOnlyList styles) { if (FlattenStyles(styles) is { } allStyles) DetachStyles(allStyles); } protected virtual void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: SetLogicalParent(e.NewItems!); break; case NotifyCollectionChangedAction.Remove: ClearLogicalParent(e.OldItems!); break; case NotifyCollectionChangedAction.Replace: ClearLogicalParent(e.OldItems!); SetLogicalParent(e.NewItems!); break; case NotifyCollectionChangedAction.Reset: throw new NotSupportedException("Reset should not be signaled on LogicalChildren collection"); } } /// /// Notifies child controls that a change has been made to resources that apply to them. /// /// The change token. internal virtual void NotifyChildResourcesChanged(ResourcesChangedToken token) { if (_logicalChildren is object) { var count = _logicalChildren.Count; if (count > 0) { for (var i = 0; i < count; ++i) { _logicalChildren[i].NotifyResourcesChanged(token); } } } } /// /// Called when the styled element is added to a rooted logical tree. /// /// The event args. protected virtual void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { } /// /// Called when the styled element is removed from a rooted logical tree. /// /// The event args. protected virtual void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { } /// /// Called when the property changes. /// /// The event args. protected virtual void OnDataContextChanged(EventArgs e) { DataContextChanged?.Invoke(this, EventArgs.Empty); } /// /// Called when the begins updating. /// protected virtual void OnDataContextBeginUpdate() { } /// /// Called when the finishes updating. /// protected virtual void OnDataContextEndUpdate() { } /// /// Called when the control finishes initialization. /// protected virtual void OnInitialized() { } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ThemeProperty) { OnControlThemeChanged(); } else if (change.Property == ThemeVariant.RequestedThemeVariantProperty) { if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) SetValue(ThemeVariant.ActualThemeVariantProperty, themeVariant); else ClearValue(ThemeVariant.ActualThemeVariantProperty); } else if (change.Property == ThemeVariant.ActualThemeVariantProperty) { ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); } } private protected virtual void OnControlThemeChanged() { var values = GetValueStore(); values.BeginStyling(); try { values.RemoveFrames(FrameType.Theme); } finally { values.EndStyling(); _themeApplied = false; } } internal virtual void OnTemplatedParentControlThemeChanged() { var values = GetValueStore(); values.BeginStyling(); try { values.RemoveFrames(FrameType.TemplatedParentTheme); } finally { values.EndStyling(); _templatedParentThemeApplied = false; } } internal ControlTheme? GetEffectiveTheme() { var theme = Theme; // Explicitly set Theme property takes precedence. if (theme is not null) return theme; // If the Theme property is not set, try to find a ControlTheme resource with our StyleKey. if (_implicitTheme is null) { var key = GetStyleKey(this); if (this.TryFindResource(key, out var value) && value is ControlTheme t) _implicitTheme = t; else _implicitTheme = s_invalidTheme; } if (_implicitTheme != s_invalidTheme) return _implicitTheme; 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); } } /// /// Internal getter for so that we only need to suppress the obsolete /// warning in one place. /// /// The element /// /// is obsolete and will be removed in a future version, but for backwards /// compatibility we need to support code which overrides . /// internal static Type GetStyleKey(StyledElement e) { #pragma warning disable CS0618 // Type or member is obsolete return ((IStyleable)e).StyleKey; #pragma warning restore CS0618 // Type or member is obsolete } private static void DataContextNotifying(AvaloniaObject o, bool updateStarted) { if (o is StyledElement element) { DataContextNotifying(element, updateStarted); } } private static void DataContextNotifying(StyledElement element, bool updateStarted) { if (updateStarted) { if (!element._dataContextUpdating) { element._dataContextUpdating = true; element.OnDataContextBeginUpdate(); var logicalChildren = element.LogicalChildren; var logicalChildrenCount = logicalChildren.Count; for (var i = 0; i < logicalChildrenCount; i++) { if (element.LogicalChildren[i] is StyledElement s && s.InheritanceParent == element && !s.IsSet(DataContextProperty)) { DataContextNotifying(s, updateStarted); } } } } else { if (element._dataContextUpdating) { element.OnDataContextEndUpdate(); element._dataContextUpdating = false; } } } private static ILogicalRoot? FindLogicalRoot(IStyleHost? e) { while (e != null) { if (e is ILogicalRoot root) { return root; } e = e.StylingParent; } return null; } void IAvaloniaListItemValidator.Validate(ILogical item) { if (item is null) { throw new ArgumentException($"Cannot add null to {nameof(LogicalChildren)}."); } } private void ApplyControlTheme() { if (GetEffectiveTheme() is { } theme) ApplyControlTheme(theme, FrameType.Theme); } private void ApplyTemplatedParentControlTheme() { if ((TemplatedParent as StyledElement)?.GetEffectiveTheme() is { } parentTheme) { ApplyControlTheme(parentTheme, FrameType.TemplatedParentTheme); } } private void ApplyControlTheme(ControlTheme theme, FrameType type) { Debug.Assert(type is FrameType.Theme or FrameType.TemplatedParentTheme); if (theme.BasedOn is ControlTheme basedOn) ApplyControlTheme(basedOn, type); theme.TryAttach(this, type); if (theme.HasChildren) { var children = theme.Children; for (var i = 0; i < children.Count; i++) { ApplyStyle(children[i], null, type); } } } private void ApplyStyles(IStyleHost host) { var parent = host.StylingParent; if (parent != null) ApplyStyles(parent); if (host.IsStylesInitialized) { var styles = host.Styles; for (var i = 0; i < styles.Count; ++i) { ApplyStyle(styles[i], host, FrameType.Style); } } } private void ApplyStyle(IStyle style, IStyleHost? host, FrameType type) { if (style is Style s) s.TryAttach(this, host, type); var children = style.Children; for (var i = 0; i < children.Count; i++) { ApplyStyle(children[i], host, type); } } private void ReevaluateImplicitTheme() { // We only need to check if the theme has changed when Theme isn't set (i.e. when we // have an implicit theme). if (Theme is not null) return; // Refetch the implicit theme. var oldImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; _implicitTheme = null; GetEffectiveTheme(); var newImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; // If the implicit theme has changed, detach the existing theme. if (newImplicitTheme != oldImplicitTheme) { OnControlThemeChanged(); _themeApplied = false; } } private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { if (this.GetLogicalParent() == null && !(this is ILogicalRoot)) { throw new InvalidOperationException( $"AttachedToLogicalTreeCore called for '{GetType().Name}' but control has no logical parent."); } // This method can be called when a control is already attached to the logical tree // in the following scenario: // - ListBox gets assigned Items containing ListBoxItem // - ListBox makes ListBoxItem a logical child // - ListBox template gets applied; making its Panel get attached to logical tree // - That AttachedToLogicalTree signal travels down to the ListBoxItem if (_logicalRoot == null) { _logicalRoot = e.Root; ReevaluateImplicitTheme(); ApplyStyling(); NotifyResourcesChanged(ResourcesChangedToken.Create(), propagate: false); OnAttachedToLogicalTree(e); AttachedToLogicalTree?.Invoke(this, e); } var logicalChildren = LogicalChildren; var logicalChildrenCount = logicalChildren.Count; for (var i = 0; i < logicalChildrenCount; i++) { if (logicalChildren[i] is StyledElement child && child._logicalRoot != e.Root) // child may already have been attached within an event handler { child.OnAttachedToLogicalTreeCore(e); } } } private void OnDetachedFromLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { if (_logicalRoot != null) { _logicalRoot = null; InvalidateStyles(recurse: false); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); var logicalChildren = LogicalChildren; var logicalChildrenCount = logicalChildren.Count; for (var i = 0; i < logicalChildrenCount; i++) { if (logicalChildren[i] is StyledElement child) { child.OnDetachedFromLogicalTreeCore(e); } } #if DEBUG if (((INotifyCollectionChangedDebug)Classes).GetCollectionChangedSubscribers()?.Length > 0) { Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log( this, "{Type} detached from logical tree but still has class listeners", GetType()); } #endif } } private void OnDataContextChangedCore(AvaloniaPropertyChangedEventArgs e) { OnDataContextChanged(EventArgs.Empty); } private void SetLogicalParent(IList children) { var count = children.Count; for (var i = 0; i < count; i++) { var logical = (ILogical) children[i]!; if (logical.LogicalParent is null) { ((ISetLogicalParent)logical).SetParent(this); } } } private void ClearLogicalParent(IList children) { var count = children.Count; for (var i = 0; i < count; i++) { var logical = (ILogical) children[i]!; if (logical.LogicalParent == this) { ((ISetLogicalParent)logical).SetParent(null); } } } private void DetachStyles(IReadOnlyList