using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using Avalonia.Animation; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.LogicalTree; 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, IStyledElement, ISetLogicalParent, ISetInheritanceParent { /// /// Defines the property. /// public static readonly StyledProperty DataContextProperty = AvaloniaProperty.Register( nameof(DataContext), inherits: true, 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, (o ,v) => o.TemplatedParent = v); /// /// 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 readonly Classes _classes = new Classes(); private ILogicalRoot? _logicalRoot; private IAvaloniaList? _logicalChildren; private IResourceDictionary? _resources; private Styles? _styles; private bool _styled; private List? _appliedStyles; private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; private bool _hasPromotedTheme; private ControlTheme? _implicitTheme; /// /// 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; /// /// 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 (_styled) { 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. /// /// /// Even though this property can be set, the setter is only intended for use in object /// initializers. Assigning to this property does not change the underlying collection, /// it simply clears the existing collection and adds the contents of the assigned /// collection. /// /// public Classes Classes { get { return _classes; } set { if (_classes != value) { _classes.Replace(value); } } } /// /// 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 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 ITemplatedControl? 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 IAvaloniaList LogicalChildren { get { if (_logicalChildren == null) { var list = new AvaloniaList { ResetBehavior = ResetBehavior.Remove, Validate = logical => ValidateLogicalChild(logical) }; list.CollectionChanged += LogicalChildrenCollectionChanged; _logicalChildren = list; } return _logicalChildren; } } /// /// Gets the collection in a form that allows adding and removing /// pseudoclasses. /// protected IPseudoClasses PseudoClasses => Classes; /// /// 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 IStyledElement? Parent { get; private set; } /// /// 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; /// /// Gets the type by which the styled element is styled. /// /// /// Usually controls are styled by their own type, but there are instances where you want /// a styled 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. /// Type IStyleable.StyleKey => GetType(); /// 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 != null) { ApplyStyling(); InitializeIfNeeded(); } } /// /// Applies styling to the control if the control is initialized and styling is not /// already applied. /// /// /// A value indicating whether styling is now applied to the control. /// protected bool ApplyStyling() { if (_initCount == 0 && !_styled) { try { BeginBatchUpdate(); AvaloniaLocator.Current.GetService()?.ApplyStyles(this); } finally { _styled = true; EndBatchUpdate(); } if (_hasPromotedTheme) { _hasPromotedTheme = false; ClearValue(ThemeProperty); } } return _styled; } /// /// Detaches all styles from the element and queues a restyle. /// protected virtual void InvalidateStyles() => DetachStyles(); protected void InitializeIfNeeded() { if (_initCount == 0 && !IsInitialized) { IsInitialized = true; OnInitialized(); Initialized?.Invoke(this, EventArgs.Empty); } } internal StyleDiagnostics GetStyleDiagnosticsInternal() { IReadOnlyList? appliedStyles = _appliedStyles; if (appliedStyles is null) { appliedStyles = Array.Empty(); } return new StyleDiagnostics(appliedStyles); } /// void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { OnAttachedToLogicalTreeCore(e); } /// void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { OnDetachedFromLogicalTreeCore(e); } /// void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); /// void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); /// bool IResourceNode.TryGetResource(object key, out object? value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || (_styles?.TryGetResource(key, 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 = (IStyledElement?)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(); } #nullable disable RaisePropertyChanged( ParentProperty, new Optional(old), new BindingValue(Parent), BindingPriority.LocalValue); #nullable enable } } /// /// Sets the styled element's inheritance parent. /// /// The parent. void ISetInheritanceParent.SetParent(IAvaloniaObject? parent) { InheritanceParent = parent switch { AvaloniaObject ao => ao, null => null, _ => throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.") }; } ControlTheme? IStyleable.GetEffectiveTheme() { var theme = Theme; // Explitly 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 = ((IStyleable)this).StyleKey; 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; } void IStyleable.StyleApplied(IStyleInstance instance) { instance = instance ?? throw new ArgumentNullException(nameof(instance)); _appliedStyles ??= new List(); _appliedStyles.Add(instance); } void IStyleable.DetachStyles() => DetachStyles(); void IStyleable.DetachStyles(IReadOnlyList styles) => DetachStyles(styles); void IStyleable.InvalidateStyles() => InvalidateStyles(); void IStyleHost.StylesAdded(IReadOnlyList styles) { InvalidateStylesOnThisAndDescendents(); } void IStyleHost.StylesRemoved(IReadOnlyList styles) { var allStyles = RecurseStyles(styles); DetachStylesFromThisAndDescendents(allStyles); } protected virtual void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { 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 event args. protected virtual void NotifyChildResourcesChanged(ResourcesChangedEventArgs e) { if (_logicalChildren is object) { var count = _logicalChildren.Count; if (count > 0) { e ??= ResourcesChangedEventArgs.Empty; for (var i = 0; i < count; ++i) { _logicalChildren[i].NotifyResourcesChanged(e); } } } } /// /// 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) { // 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 = change.GetNewValue(); _hasPromotedTheme = true; } else if (_hasPromotedTheme && change.Priority == BindingPriority.LocalValue) { _hasPromotedTheme = false; } InvalidateStyles(); } } private static void DataContextNotifying(IAvaloniaObject 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; } private static void ValidateLogicalChild(ILogical c) { if (c == null) { throw new ArgumentException("Cannot add null to LogicalChildren."); } } 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; ApplyStyling(); NotifyResourcesChanged(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.OnAttachedToLogicalTreeCore(e); } } } private void OnDetachedFromLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { if (_logicalRoot != null) { _logicalRoot = null; _implicitTheme = null; DetachStyles(); 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() { if (_appliedStyles?.Count > 0) { BeginBatchUpdate(); try { foreach (var i in _appliedStyles) { i.Dispose(); } _appliedStyles.Clear(); } finally { EndBatchUpdate(); } } _styled = false; } private void DetachStyles(IReadOnlyList styles) { styles = styles ?? throw new ArgumentNullException(nameof(styles)); if (_appliedStyles is null) { return; } var count = styles.Count; for (var i = 0; i < count; ++i) { for (var j = _appliedStyles.Count - 1; j >= 0; --j) { var applied = _appliedStyles[j]; if (applied.Source == styles[i]) { _appliedStyles.RemoveAt(j); applied.Dispose(); } if (j > _appliedStyles.Count) j = _appliedStyles.Count; } } } private void InvalidateStylesOnThisAndDescendents() { InvalidateStyles(); if (_logicalChildren is object) { var childCount = _logicalChildren.Count; for (var i = 0; i < childCount; ++i) { (_logicalChildren[i] as StyledElement)?.InvalidateStylesOnThisAndDescendents(); } } } private void DetachStylesFromThisAndDescendents(IReadOnlyList styles) { DetachStyles(styles); if (_logicalChildren is object) { var childCount = _logicalChildren.Count; for (var i = 0; i < childCount; ++i) { (_logicalChildren[i] as StyledElement)?.DetachStylesFromThisAndDescendents(styles); } } } private void NotifyResourcesChanged( ResourcesChangedEventArgs? e = null, bool propagate = true) { if (ResourcesChanged is object) { e ??= ResourcesChangedEventArgs.Empty; ResourcesChanged(this, e); } if (propagate) { e ??= ResourcesChangedEventArgs.Empty; NotifyChildResourcesChanged(e); } } private static IReadOnlyList RecurseStyles(IReadOnlyList styles) { var count = styles.Count; List? result = null; for (var i = 0; i < count; ++i) { var style = styles[i]; if (style.Children.Count > 0) { if (result is null) { result = new List(styles); } RecurseStyles(style.Children, result); } } return result ?? styles; } private static void RecurseStyles(IReadOnlyList styles, List result) { var count = styles.Count; for (var i = 0; i < count; ++i) { var style = styles[i]; result.Add(style); RecurseStyles(style.Children, result); } } } }