// Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using Avalonia.Collections; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls { /// /// Base class for Avalonia controls. /// /// /// The control class extends and adds the following features: /// /// - An inherited . /// - A property to allow user-defined data to be attached to the control. /// - A collection of class strings for custom styling. /// - Implements to allow styling to work on the control. /// - Implements to form part of a logical tree. /// public class Control : InputElement, IControl, INamed, ISetInheritanceParent, ISetLogicalParent, ISupportInitialize, IVisualBrushInitialize { /// /// Defines the property. /// public static readonly StyledProperty DataContextProperty = AvaloniaProperty.Register( nameof(DataContext), inherits: true, notifying: DataContextNotifying); /// /// Defines the property. /// public static readonly StyledProperty> FocusAdornerProperty = AvaloniaProperty.Register>(nameof(FocusAdorner)); /// /// 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 StyledProperty TagProperty = AvaloniaProperty.Register(nameof(Tag)); /// /// Defines the property. /// public static readonly StyledProperty TemplatedParentProperty = AvaloniaProperty.Register(nameof(TemplatedParent), inherits: true); /// /// Defines the property. /// public static readonly StyledProperty ContextMenuProperty = AvaloniaProperty.Register(nameof(ContextMenu)); /// /// Event raised when an element wishes to be scrolled into view. /// public static readonly RoutedEvent RequestBringIntoViewEvent = RoutedEvent.Register("RequestBringIntoView", RoutingStrategies.Bubble); private int _initCount; private string _name; private IControl _parent; private readonly Classes _classes = new Classes(); private DataTemplates _dataTemplates; private IControl _focusAdorner; private bool _isAttachedToLogicalTree; private IAvaloniaList _logicalChildren; private INameScope _nameScope; private Styles _styles; private bool _styled; private Subject _styleDetach = new Subject(); /// /// Initializes static members of the class. /// static Control() { AffectsMeasure(IsVisibleProperty); PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled"); PseudoClass(IsFocusedProperty, ":focus"); PseudoClass(IsPointerOverProperty, ":pointerover"); } /// /// Initializes a new instance of the class. /// public Control() { _nameScope = this as INameScope; } /// /// Raised when the control is attached to a rooted logical tree. /// public event EventHandler AttachedToLogicalTree; /// /// Raised when the control 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 control has finished initialization. /// /// /// The Initialized event indicates that all property values on the control have been set. /// When loading the control from markup, it occurs when /// is called *and* the control /// is attached to a rooted logical tree. When the control is created by code and /// is not used, it is called when the control is attached /// to the visual tree. /// public event EventHandler Initialized; /// /// Gets or sets the name of the control. /// /// /// An element's name is used to uniquely identify a control within the control's name /// scope. Once the element is added to a logical tree, its name cannot be changed. /// public string Name { get { return _name; } set { if (value.Trim() == string.Empty) { throw new InvalidOperationException("Cannot set Name to empty string."); } if (_styled) { throw new InvalidOperationException("Cannot set Name : control already styled."); } _name = value; } } /// /// Gets or sets the control's classes. /// /// /// /// Classes can be used to apply user-defined styling to controls, or to allow controls /// 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 addds 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 or sets the control's focus adorner. /// public ITemplate FocusAdorner { get { return GetValue(FocusAdornerProperty); } set { SetValue(FocusAdornerProperty, value); } } /// /// Gets or sets the data templates for the control. /// /// /// Each control may define data templates which are applied to the control itself and its /// children. /// public DataTemplates DataTemplates { get { return _dataTemplates ?? (_dataTemplates = new DataTemplates()); } set { _dataTemplates = 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 or sets the styles for the control. /// /// /// Styles for the entire application are added to the Application.Styles collection, but /// each control may in addition define its own styles which are applied to the control /// itself and its children. /// public Styles Styles { get { return _styles ?? (_styles = new Styles()); } set { _styles = value; } } /// /// Gets the control's logical parent. /// public IControl Parent => _parent; /// /// Gets or sets a context menu to the control. /// public ContextMenu ContextMenu { get { return GetValue(ContextMenuProperty); } set { SetValue(ContextMenuProperty, value); } } /// /// Gets or sets a user-defined object attached to the control. /// public object Tag { get { return GetValue(TagProperty); } set { SetValue(TagProperty, value); } } /// /// Gets the control whose lookless template this control is part of. /// public ITemplatedControl TemplatedParent { get { return GetValue(TemplatedParentProperty); } internal set { SetValue(TemplatedParentProperty, value); } } /// /// Gets a value indicating whether the element is attached to a rooted logical tree. /// bool ILogical.IsAttachedToLogicalTree => _isAttachedToLogicalTree; /// /// Gets the control's logical parent. /// ILogical ILogical.LogicalParent => Parent; /// /// Gets the control's logical children. /// IAvaloniaReadOnlyList ILogical.LogicalChildren => LogicalChildren; /// IAvaloniaReadOnlyList IStyleable.Classes => Classes; /// /// Gets the type by which the control is styled. /// /// /// Usually controls are styled by their own type, but there are instances where you want /// a control 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(); /// IObservable IStyleable.StyleDetach => _styleDetach; /// 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 && _isAttachedToLogicalTree) { if (!_styled) { RegisterWithNameScope(); ApplyStyling(); _styled = true; } if (!IsInitialized) { IsInitialized = true; Initialized?.Invoke(this, EventArgs.Empty); } } } /// void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { this.OnDetachedFromLogicalTreeCore(e); } /// /// Gets the control's logical children. /// protected IAvaloniaList LogicalChildren { get { if (_logicalChildren == null) { var list = new AvaloniaList(); list.ResetBehavior = ResetBehavior.Remove; list.Validate = ValidateLogicalChild; list.CollectionChanged += LogicalChildrenCollectionChanged; _logicalChildren = list; } return _logicalChildren; } } /// /// Gets the collection in a form that allows adding and removing /// pseudoclasses. /// protected IPseudoClasses PseudoClasses => Classes; /// /// Sets the control'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 (_isAttachedToLogicalTree) { var oldRoot = FindStyleRoot(old); if (oldRoot == null) { throw new AvaloniaInternalException("Was attached to logical tree but cannot find root."); } var e = new LogicalTreeAttachmentEventArgs(oldRoot); OnDetachedFromLogicalTreeCore(e); } if (InheritanceParent == null || parent == null) { InheritanceParent = parent as AvaloniaObject; } _parent = (IControl)parent; if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true) { var newRoot = FindStyleRoot(this); if (newRoot == null) { throw new AvaloniaInternalException("Parent is atttached to logical tree but cannot find root."); } var e = new LogicalTreeAttachmentEventArgs(newRoot); OnAttachedToLogicalTreeCore(e); } RaisePropertyChanged(ParentProperty, old, _parent, BindingPriority.LocalValue); } } /// /// Sets the control's inheritance parent. /// /// The parent. void ISetInheritanceParent.SetParent(IAvaloniaObject parent) { InheritanceParent = parent; } /// void IVisualBrushInitialize.EnsureInitialized() { if (VisualRoot == null) { if (!IsInitialized) { foreach (var i in this.GetSelfAndVisualDescendents()) { var c = i as IControl; if (c?.IsInitialized == false) { var init = c as ISupportInitialize; if (init != null) { init.BeginInit(); init.EndInit(); } } } } if (!IsArrangeValid) { Measure(Size.Infinity); Arrange(new Rect(DesiredSize)); } } } /// /// Adds a pseudo-class to be set when a property is true. /// /// The property. /// The pseudo-class. protected static void PseudoClass(AvaloniaProperty property, string className) { PseudoClass(property, x => x, className); } /// /// Adds a pseudo-class to be set when a property equals a certain value. /// /// The type of the property. /// The property. /// Returns a boolean value based on the property value. /// The pseudo-class. protected static void PseudoClass( AvaloniaProperty property, Func selector, string className) { Contract.Requires(property != null); Contract.Requires(selector != null); Contract.Requires(className != null); Contract.Requires(property != null); if (string.IsNullOrWhiteSpace(className)) { throw new ArgumentException("Cannot supply an empty className."); } property.Changed.Merge(property.Initialized) .Where(e => e.Sender is Control) .Subscribe(e => { if (selector((T)e.NewValue)) { ((Control)e.Sender).PseudoClasses.Add(className); } else { ((Control)e.Sender).PseudoClasses.Remove(className); } }); } /// /// Gets the element that recieves the focus adorner. /// /// The control that recieves the focus adorner. protected virtual IControl GetTemplateFocusTarget() { return this; } /// /// Called when the control is added to a rooted logical tree. /// /// The event args. protected virtual void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { AttachedToLogicalTree?.Invoke(this, e); } /// /// Called when the control is removed from a rooted logical tree. /// /// The event args. protected virtual void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { DetachedFromLogicalTree?.Invoke(this, e); } /// protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTreeCore(e); if (!IsInitialized) { IsInitialized = true; Initialized?.Invoke(this, EventArgs.Empty); } } /// protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTreeCore(e); } /// /// Called before the property changes. /// protected virtual void OnDataContextChanging() { } /// /// Called after the property changes. /// protected virtual void OnDataContextChanged() { DataContextChanged?.Invoke(this, EventArgs.Empty); } /// protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); if (IsFocused && (e.NavigationMethod == NavigationMethod.Tab || e.NavigationMethod == NavigationMethod.Directional)) { var adornerLayer = AdornerLayer.GetAdornerLayer(this); if (adornerLayer != null) { if (_focusAdorner == null) { var template = GetValue(FocusAdornerProperty); if (template != null) { _focusAdorner = template.Build(); } } if (_focusAdorner != null) { var target = (Visual)GetTemplateFocusTarget(); if (target != null) { AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target); adornerLayer.Children.Add(_focusAdorner); } } } } } /// protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); if (_focusAdorner != null) { var adornerLayer = _focusAdorner.Parent as Panel; adornerLayer.Children.Remove(_focusAdorner); _focusAdorner = null; } } /// /// Called when the property begins and ends being notified. /// /// The object on which the DataContext is changing. /// Whether the notifcation is beginning or ending. private static void DataContextNotifying(IAvaloniaObject o, bool notifying) { var control = o as Control; if (control != null) { if (notifying) { control.OnDataContextChanging(); } else { control.OnDataContextChanged(); } } } private static IStyleRoot FindStyleRoot(IStyleHost e) { while (e != null) { var root = e as IStyleRoot; if (root != null && root.StylingParent == null) { return root; } e = e.StylingParent; } return null; } private void ApplyStyling() { AvaloniaLocator.Current.GetService()?.ApplyStyles(this); } private void RegisterWithNameScope() { if (_nameScope == null) { _nameScope = NameScope.GetNameScope(this) ?? ((Control)Parent)?._nameScope; } if (Name != null) { _nameScope?.Register(Name, this); var visualParent = Parent as Visual; if (this is INameScope && visualParent != null) { // If we have e.g. a named UserControl in a window then we want that control // to be findable by name from the Window, so register with both name scopes. // This differs from WPF's behavior in that XAML manually registers controls // with name scopes based on the XAML file in which the name attribute appears, // but we're trying to avoid XAML magic in Avalonia in order to made code- // created UIs easy. This will cause problems if a UserControl declares a name // in its XAML and that control is included multiple times in a parent control // (as the name will be duplicated), however at the moment I'm fine with saying // "don't do that". var parentNameScope = NameScope.FindNameScope(visualParent); parentNameScope?.Register(Name, this); } } } private static void ValidateLogicalChild(ILogical c) { if (c == null) { throw new ArgumentException("Cannot add null to LogicalChildren."); } } private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { // 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 (!_isAttachedToLogicalTree) { _isAttachedToLogicalTree = true; if (_initCount == 0) { RegisterWithNameScope(); ApplyStyling(); _styled = true; } OnAttachedToLogicalTree(e); } foreach (var child in LogicalChildren.OfType()) { child.OnAttachedToLogicalTreeCore(e); } } private void OnDetachedFromLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { if (_isAttachedToLogicalTree) { if (Name != null) { _nameScope?.Unregister(Name); } _isAttachedToLogicalTree = false; _styleDetach.OnNext(this); OnDetachedFromLogicalTree(e); foreach (var child in LogicalChildren.OfType()) { child.OnDetachedFromLogicalTreeCore(e); } #if DEBUG if (((INotifyCollectionChangedDebug)_classes).GetCollectionChangedSubscribers()?.Length > 0) { Logger.Warning( LogArea.Control, this, "{Type} detached from logical tree but still has class listeners", this.GetType()); } #endif } } private void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: SetLogicalParent(e.NewItems.Cast()); break; case NotifyCollectionChangedAction.Remove: ClearLogicalParent(e.OldItems.Cast()); break; case NotifyCollectionChangedAction.Replace: ClearLogicalParent(e.OldItems.Cast()); SetLogicalParent(e.NewItems.Cast()); break; case NotifyCollectionChangedAction.Reset: throw new NotSupportedException("Reset should not be signalled on LogicalChildren collection"); } } private void SetLogicalParent(IEnumerable children) { foreach (var i in children) { if (i.LogicalParent == null) { ((ISetLogicalParent)i).SetParent(this); } } } private void ClearLogicalParent(IEnumerable children) { foreach (var i in children) { if (i.LogicalParent == this) { ((ISetLogicalParent)i).SetParent(null); } } } } }