using System; 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; 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, IStyleHost, IStyleable, ISetLogicalParent, ISetInheritanceParent, ISupportInitialize { /// /// Defines the property. /// public static readonly StyledProperty DataContextProperty = AvaloniaProperty.Register( nameof(DataContext), defaultValue: null, inherits: true, defaultBindingMode: BindingMode.OneWay, validate: null, coerce: null, 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 _stylesApplied; private bool _themeApplied; private bool _templatedParentThemeApplied; private AvaloniaObject? _templatedParent; private bool _dataContextUpdating; 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; /// 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. /// /// /// 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 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, 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 StyledElement? Parent { get; private set; } /// 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; /// /// 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 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)) { 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); } } internal StyleDiagnostics GetStyleDiagnosticsInternal() { var styles = new List(); foreach (var frame in GetValueStore().Frames) { if (frame is IStyleInstance style) styles.Add(style); } return new StyleDiagnostics(styles); } /// 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); /// 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(); } 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 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) { 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 = ((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; } 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(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; } private static void ValidateLogicalChild(ILogical c) { if (c == null) { throw new ArgumentException("Cannot add null to 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) { foreach (var child in theme.Children) ApplyStyle(child, null, type); } } private void ApplyStyles(IStyleHost host) { var parent = host.StylingParent; if (parent != null) ApplyStyles(parent); if (host.IsStylesInitialized) { foreach (var style in host.Styles) ApplyStyle(style, host, FrameType.Style); } } private void ApplyStyle(IStyle style, IStyleHost? host, FrameType type) { if (style is Style s) s.TryAttach(this, host, type); foreach (var child in style.Children) ApplyStyle(child, 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(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; 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