diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 1a2db9fc3d..48e72db126 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -703,7 +703,7 @@ namespace Avalonia /// The default value. private object GetDefaultValue(AvaloniaProperty property) { - if (property.Inherits && _inheritanceParent is AvaloniaObject aobj) + if (property.Inherits && InheritanceParent is AvaloniaObject aobj) return aobj.GetValueInternal(property); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 89721e2e05..2bcd540264 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -29,53 +29,23 @@ namespace 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 + public class Control : InputElement, IControl, INamed, ISupportInitialize, IVisualBrushInitialize, IRequiresTemplateInStyle { - /// - /// 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. /// @@ -88,20 +58,8 @@ namespace Avalonia.Controls 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 IResourceDictionary _resources; - private Styles _styles; - private bool _styled; - private Subject _styleDetach = new Subject(); - private bool _dataContextUpdating; /// /// Initializes static members of the class. @@ -112,127 +70,6 @@ namespace Avalonia.Controls PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled"); PseudoClass(IsFocusedProperty, ":focus"); PseudoClass(IsPointerOverProperty, ":pointerover"); - DataContextProperty.Changed.AddClassHandler(x => x.OnDataContextChangedCore); - } - - /// - /// Initializes a new instance of the class. - /// - public Control() - { - _nameScope = this as INameScope; - _isAttachedToLogicalTree = this is IStyleRoot; - } - - /// - /// 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; - - /// - /// Occurs when a resource in this control or a parent control has changed. - /// - public event EventHandler ResourcesChanged; - - /// - /// 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 (String.IsNullOrWhiteSpace(value)) - { - throw new InvalidOperationException("Cannot set Name to null or 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); } } /// @@ -253,55 +90,6 @@ namespace Avalonia.Controls /// public DataTemplates DataTemplates => _dataTemplates ?? (_dataTemplates = new DataTemplates()); - /// - /// 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 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 - { - Contract.Requires(value != null); - - if (_styles != value) - { - if (_styles != null) - { - (_styles as ISetStyleParent)?.SetParent(null); - _styles.ResourcesChanged -= ThisResourcesChanged; - } - - _styles = value; - - if (value is ISetStyleParent setParent && setParent.ResourceParent == null) - { - setParent.SetParent(this); - } - - _styles.ResourcesChanged += ThisResourcesChanged; - } - } - } - - /// - /// Gets the control's logical parent. - /// - public IControl Parent => _parent; - /// /// Gets or sets a context menu to the control. /// @@ -311,34 +99,6 @@ namespace Avalonia.Controls set { SetValue(ContextMenuProperty, value); } } - /// - /// Gets or sets the control's resource dictionary. - /// - public IResourceDictionary Resources - { - get => _resources ?? (Resources = new ResourceDictionary()); - set - { - Contract.Requires(value != null); - - var hadResources = false; - - if (_resources != null) - { - hadResources = _resources.Count > 0; - _resources.ResourcesChanged -= ThisResourcesChanged; - } - - _resources = value; - _resources.ResourcesChanged += ThisResourcesChanged; - - if (hadResources || _resources.Count > 0) - { - ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); - } - } - } - /// /// Gets or sets a user-defined object attached to the control. /// @@ -348,226 +108,11 @@ namespace Avalonia.Controls 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 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; - } - } + public new IControl Parent => (IControl)base.Parent; /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; - /// - /// 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 => _isAttachedToLogicalTree; - - /// - /// Gets the control's logical parent. - /// - ILogical ILogical.LogicalParent => Parent; - - /// - /// Gets the control's logical children. - /// - IAvaloniaReadOnlyList ILogical.LogicalChildren => LogicalChildren; - - /// - bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; - - /// - IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; - - /// - 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; - - /// - 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 && _isAttachedToLogicalTree) - { - InitializeStylesIfNeeded(); - - InitializeIfNeeded(); - } - } - - private void InitializeStylesIfNeeded(bool force = false) - { - if (_initCount == 0 && (!_styled || force)) - { - RegisterWithNameScope(); - ApplyStyling(); - _styled = true; - } - } - - private void InitializeIfNeeded() - { - if (_initCount == 0 && !IsInitialized) - { - IsInitialized = true; - Initialized?.Invoke(this, EventArgs.Empty); - } - } - - /// - void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - this.OnAttachedToLogicalTreeCore(e); - } - - /// - void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - this.OnDetachedFromLogicalTreeCore(e); - } - - /// - void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } - - /// - bool IResourceProvider.TryGetResource(string key, out object value) - { - value = null; - return (_resources?.TryGetResource(key, out value) ?? false) || - (_styles?.TryGetResource(key, out value) ?? false); - } - - /// - /// 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) ?? this as IStyleRoot; - - 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 (old != null) - { - old.ResourcesChanged -= ThisResourcesChanged; - } - if (_parent != null) - { - _parent.ResourcesChanged += ThisResourcesChanged; - } - ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); - - if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true || this is IStyleRoot) - { - 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() { @@ -600,52 +145,6 @@ namespace Avalonia.Controls } } - /// - /// 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); - - 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. /// @@ -655,22 +154,6 @@ namespace Avalonia.Controls return this; } - /// - /// Called when the control is added to a rooted logical tree. - /// - /// The event args. - protected virtual void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - } - - /// - /// Called when the control is removed from a rooted logical tree. - /// - /// The event args. - protected virtual void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - } - /// protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { @@ -685,29 +168,6 @@ namespace Avalonia.Controls base.OnDetachedFromVisualTreeCore(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() - { - } - /// protected override void OnGotFocus(GotFocusEventArgs e) { @@ -757,211 +217,5 @@ namespace Avalonia.Controls _focusAdorner = null; } } - - private static void DataContextNotifying(IAvaloniaObject o, bool notifying) - { - if (o is Control control) - { - DataContextNotifying(control, notifying); - } - } - - private static void DataContextNotifying(Control control, bool notifying) - { - if (notifying) - { - if (!control._dataContextUpdating) - { - control._dataContextUpdating = true; - control.OnDataContextBeginUpdate(); - - foreach (var child in control.LogicalChildren) - { - if (child is Control c && - c.InheritanceParent == control && - !c.IsSet(DataContextProperty)) - { - DataContextNotifying(c, notifying); - } - } - } - } - else - { - if (control._dataContextUpdating) - { - control.OnDataContextEndUpdate(); - control._dataContextUpdating = false; - } - } - } - - private static IStyleRoot FindStyleRoot(IStyleHost e) - { - while (e != null) - { - if (e is IRenderRoot root) - { - return root as IStyleRoot; - } - - 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; - - InitializeStylesIfNeeded(true); - - OnAttachedToLogicalTree(e); - AttachedToLogicalTree?.Invoke(this, 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); - DetachedFromLogicalTree?.Invoke(this, 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 OnDataContextChangedCore(AvaloniaPropertyChangedEventArgs e) - { - OnDataContextChanged(EventArgs.Empty); - } - - 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); - } - } - } - - private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e) - { - ((ILogical)this).NotifyResourcesChanged(e); - } } } diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index 35c9d6a9c5..a59fb86fb7 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -55,13 +55,13 @@ namespace Avalonia.Controls public void Init() { _hotkeySub = _control.GetObservable(HotKeyProperty).Subscribe(OnHotkeyChanged); - _parentSub = AncestorFinder.Create(_control, typeof (TopLevel)).Subscribe(OnParentChanged); + _parentSub = AncestorFinder.Create(_control).Subscribe(OnParentChanged); } - private void OnParentChanged(IControl control) + private void OnParentChanged(TopLevel control) { Unregister(); - _root = (TopLevel) control; + _root = control; Register(); } diff --git a/src/Avalonia.Controls/IControl.cs b/src/Avalonia.Controls/IControl.cs index 36e09b2ea1..e7f2903249 100644 --- a/src/Avalonia.Controls/IControl.cs +++ b/src/Avalonia.Controls/IControl.cs @@ -16,37 +16,11 @@ namespace Avalonia.Controls /// public interface IControl : IVisual, IDataTemplateHost, - ILogical, ILayoutable, IInputElement, INamed, - IResourceNode, - IStyleable, - IStyleHost + IStyledElement { - /// - /// Occurs when the control has finished initialization. - /// - event EventHandler Initialized; - - /// - /// Gets or sets the control's styling classes. - /// - new Classes Classes { get; set; } - - /// - /// Gets or sets the control's data context. - /// - object DataContext { get; set; } - - /// - /// Gets a value that indicates whether the element has finished initialization. - /// - bool IsInitialized { get; } - - /// - /// Gets the control's logical parent. - /// - IControl Parent { get; } + new IControl Parent { get; } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Utils/AncestorFinder.cs b/src/Avalonia.Controls/Utils/AncestorFinder.cs index 7e6523261a..6ee284e05d 100644 --- a/src/Avalonia.Controls/Utils/AncestorFinder.cs +++ b/src/Avalonia.Controls/Utils/AncestorFinder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive; using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Reactive.Subjects; using System.Reflection; using System.Text; @@ -14,15 +15,15 @@ namespace Avalonia.Controls.Utils { class FinderNode : IDisposable { - private readonly IControl _control; + private readonly IStyledElement _control; private readonly TypeInfo _ancestorType; - public IObservable Observable => _subject; - private readonly Subject _subject = new Subject(); + public IObservable Observable => _subject; + private readonly Subject _subject = new Subject(); private FinderNode _child; private IDisposable _disposable; - public FinderNode(IControl control, TypeInfo ancestorType) + public FinderNode(IStyledElement control, TypeInfo ancestorType) { _control = control; _ancestorType = ancestorType; @@ -33,7 +34,7 @@ namespace Avalonia.Controls.Utils _disposable = _control.GetObservable(Control.ParentProperty).Subscribe(OnValueChanged); } - private void OnValueChanged(IControl next) + private void OnValueChanged(IStyledElement next) { if (next == null || _ancestorType.IsAssignableFrom(next.GetType().GetTypeInfo())) _subject.OnNext(next); @@ -46,7 +47,7 @@ namespace Avalonia.Controls.Utils } } - private void OnChildValueChanged(IControl control) => _subject.OnNext(control); + private void OnChildValueChanged(IStyledElement control) => _subject.OnNext(control); public void Dispose() @@ -55,10 +56,15 @@ namespace Avalonia.Controls.Utils } } + public static IObservable Create(IStyledElement control) + where T : IStyledElement + { + return Create(control, typeof(T)).Cast(); + } - public static IObservable Create(IControl control, Type ancestorType) + public static IObservable Create(IStyledElement control, Type ancestorType) { - return new AnonymousObservable(observer => + return new AnonymousObservable(observer => { var finder = new FinderNode(control, ancestorType.GetTypeInfo()); var subscription = finder.Observable.Subscribe(observer); @@ -70,8 +76,6 @@ namespace Avalonia.Controls.Utils finder.Dispose(); }); }); - - } } } diff --git a/src/Avalonia.Controls/Classes.cs b/src/Avalonia.Styling/Controls/Classes.cs similarity index 95% rename from src/Avalonia.Controls/Classes.cs rename to src/Avalonia.Styling/Controls/Classes.cs index 48ce69f0c9..85935b030b 100644 --- a/src/Avalonia.Controls/Classes.cs +++ b/src/Avalonia.Styling/Controls/Classes.cs @@ -9,7 +9,7 @@ using Avalonia.Collections; namespace Avalonia.Controls { /// - /// Holds a collection of style classes for an . + /// Holds a collection of style classes for an . /// /// /// Similar to CSS, each control may have any number of styling classes applied. @@ -54,7 +54,7 @@ namespace Avalonia.Controls /// The class name. /// /// Only standard classes may be added via this method. To add pseudoclasses (classes - /// beginning with a ':' character) use the protected + /// beginning with a ':' character) use the protected /// property. /// public override void Add(string name) @@ -73,7 +73,7 @@ namespace Avalonia.Controls /// The class names. /// /// Only standard classes may be added via this method. To add pseudoclasses (classes - /// beginning with a ':' character) use the protected + /// beginning with a ':' character) use the protected /// property. /// public override void AddRange(IEnumerable names) @@ -114,7 +114,7 @@ namespace Avalonia.Controls /// The class name. /// /// Only standard classes may be added via this method. To add pseudoclasses (classes - /// beginning with a ':' character) use the protected + /// beginning with a ':' character) use the protected /// property. /// public override void Insert(int index, string name) @@ -134,7 +134,7 @@ namespace Avalonia.Controls /// The class names. /// /// Only standard classes may be added via this method. To add pseudoclasses (classes - /// beginning with a ':' character) use the protected + /// beginning with a ':' character) use the protected /// property. /// public override void InsertRange(int index, IEnumerable names) @@ -160,7 +160,7 @@ namespace Avalonia.Controls /// The class name. /// /// Only standard classes may be removed via this method. To remove pseudoclasses (classes - /// beginning with a ':' character) use the protected + /// beginning with a ':' character) use the protected /// property. /// public override bool Remove(string name) @@ -175,7 +175,7 @@ namespace Avalonia.Controls /// The class name. /// /// Only standard classes may be removed via this method. To remove pseudoclasses (classes - /// beginning with a ':' character) use the protected + /// beginning with a ':' character) use the protected /// property. /// public override void RemoveAll(IEnumerable names) @@ -201,7 +201,7 @@ namespace Avalonia.Controls /// The index of the class in the collection. /// /// Only standard classes may be removed via this method. To remove pseudoclasses (classes - /// beginning with a ':' character) use the protected + /// beginning with a ':' character) use the protected /// property. /// public override void RemoveAt(int index) diff --git a/src/Avalonia.Controls/IPseudoClasses.cs b/src/Avalonia.Styling/Controls/IPseudoClasses.cs similarity index 100% rename from src/Avalonia.Controls/IPseudoClasses.cs rename to src/Avalonia.Styling/Controls/IPseudoClasses.cs diff --git a/src/Avalonia.Controls/ISetInheritanceParent.cs b/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs similarity index 88% rename from src/Avalonia.Controls/ISetInheritanceParent.cs rename to src/Avalonia.Styling/Controls/ISetInheritanceParent.cs index 788ab77246..1a778fa0f9 100644 --- a/src/Avalonia.Controls/ISetInheritanceParent.cs +++ b/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs @@ -4,7 +4,7 @@ namespace Avalonia.Controls { /// - /// Defines an interface through which a 's inheritance parent can be set. + /// Defines an interface through which a 's inheritance parent can be set. /// /// /// You should not usually need to use this interface - it is for advanced scenarios only. diff --git a/src/Avalonia.Controls/ISetLogicalParent.cs b/src/Avalonia.Styling/Controls/ISetLogicalParent.cs similarity index 85% rename from src/Avalonia.Controls/ISetLogicalParent.cs rename to src/Avalonia.Styling/Controls/ISetLogicalParent.cs index 4422033634..5fb450c896 100644 --- a/src/Avalonia.Controls/ISetLogicalParent.cs +++ b/src/Avalonia.Styling/Controls/ISetLogicalParent.cs @@ -6,7 +6,7 @@ using Avalonia.LogicalTree; namespace Avalonia.Controls { /// - /// Defines an interface through which a 's logical parent can be set. + /// Defines an interface through which a 's logical parent can be set. /// /// /// You should not usually need to use this interface - it is for advanced scenarios only. diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs index 8b5bd81d3c..e3a29af541 100644 --- a/src/Avalonia.Styling/Controls/NameScope.cs +++ b/src/Avalonia.Styling/Controls/NameScope.cs @@ -16,7 +16,7 @@ namespace Avalonia.Controls /// Defines the NameScope attached property. /// public static readonly AttachedProperty NameScopeProperty = - AvaloniaProperty.RegisterAttached("NameScope"); + AvaloniaProperty.RegisterAttached("NameScope"); private readonly Dictionary _inner = new Dictionary(); @@ -31,53 +31,53 @@ namespace Avalonia.Controls public event EventHandler Unregistered; /// - /// Finds the containing name scope for a visual. + /// Finds the containing name scope for a styled element. /// - /// The visual. + /// The styled element. /// The containing name scope. - public static INameScope FindNameScope(Visual visual) + public static INameScope FindNameScope(StyledElement styled) { - Contract.Requires(visual != null); + Contract.Requires(styled != null); INameScope result; - while (visual != null) + while (styled != null) { - result = visual as INameScope ?? GetNameScope(visual); + result = styled as INameScope ?? GetNameScope(styled); if (result != null) { return result; } - visual = (visual as ILogical)?.LogicalParent as Visual; + styled = (styled as ILogical)?.LogicalParent as StyledElement; } return null; } /// - /// Gets the value of the attached on a visual. + /// Gets the value of the attached on a styled element. /// - /// The visual. + /// The styled element. /// The value of the NameScope attached property. - public static INameScope GetNameScope(Visual visual) + public static INameScope GetNameScope(StyledElement styled) { - Contract.Requires(visual != null); + Contract.Requires(styled != null); - return visual.GetValue(NameScopeProperty); + return styled.GetValue(NameScopeProperty); } /// - /// Sets the value of the attached on a visual. + /// Sets the value of the attached on a styled element. /// - /// The visual. + /// The styled element. /// The value to set. - public static void SetNameScope(Visual visual, INameScope value) + public static void SetNameScope(StyledElement styled, INameScope value) { - Contract.Requires(visual != null); + Contract.Requires(styled != null); - visual.SetValue(NameScopeProperty, value); + styled.SetValue(NameScopeProperty, value); } /// diff --git a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs index 49be842ffc..491e4d71a7 100644 --- a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs +++ b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.LogicalTree; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -73,7 +72,7 @@ namespace Avalonia.Controls Contract.Requires(control != null); return control.GetSelfAndLogicalAncestors() - .OfType() + .OfType() .Select(x => (x as INameScope) ?? NameScope.GetNameScope(x)) .FirstOrDefault(x => x != null); } diff --git a/src/Avalonia.Visuals/INamed.cs b/src/Avalonia.Styling/INamed.cs similarity index 100% rename from src/Avalonia.Visuals/INamed.cs rename to src/Avalonia.Styling/INamed.cs diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Styling/IStyledElement.cs new file mode 100644 index 0000000000..8369717233 --- /dev/null +++ b/src/Avalonia.Styling/IStyledElement.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls; +using Avalonia.LogicalTree; +using Avalonia.Styling; + +namespace Avalonia +{ + public interface IStyledElement : + IStyleable, + IStyleHost, + ILogical, + IResourceProvider, + IResourceNode + { + /// + /// Occurs when the control has finished initialization. + /// + event EventHandler Initialized; + + /// + /// Gets a value that indicates whether the element has finished initialization. + /// + bool IsInitialized { get; } + + /// + /// Gets or sets the control's styling classes. + /// + new Classes Classes { get; set; } + + /// + /// Gets or sets the control's data context. + /// + object DataContext { get; set; } + + /// + /// Gets the control's logical parent. + /// + IStyledElement Parent { get; } + } +} diff --git a/src/Avalonia.Styling/Controls/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs similarity index 68% rename from src/Avalonia.Styling/Controls/ControlLocator.cs rename to src/Avalonia.Styling/LogicalTree/ControlLocator.cs index e03ec722ad..2858d11d9d 100644 --- a/src/Avalonia.Styling/Controls/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -6,26 +6,9 @@ using System.Linq; using System.Reactive.Linq; using System.Reflection; using Avalonia.Controls; -using Avalonia.LogicalTree; -using Avalonia.VisualTree; -namespace Avalonia.Controls +namespace Avalonia.LogicalTree { - /// - /// The type of tree via which to track a control. - /// - public enum TreeType - { - /// - /// The visual tree. - /// - Visual, - /// - /// The logical tree. - /// - Logical, - } - /// /// Locates controls relative to other controls. /// @@ -94,40 +77,6 @@ namespace Avalonia.Controls }); } - public static IObservable Track(IVisual relativeTo, int ancestorLevel, Type ancestorType = null) - { - return TrackAttachmentToTree(relativeTo).Select(isAttachedToTree => - { - if (isAttachedToTree) - { - return relativeTo.GetVisualAncestors() - .Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) - .ElementAtOrDefault(ancestorLevel); - } - else - { - return null; - } - }); - } - - private static IObservable TrackAttachmentToTree(IVisual relativeTo) - { - var attached = Observable.FromEventPattern( - x => relativeTo.AttachedToVisualTree += x, - x => relativeTo.AttachedToVisualTree -= x) - .Select(x => true) - .StartWith(relativeTo.IsAttachedToVisualTree); - - var detached = Observable.FromEventPattern( - x => relativeTo.DetachedFromVisualTree += x, - x => relativeTo.DetachedFromVisualTree -= x) - .Select(x => false); - - var attachmentStatus = attached.Merge(detached); - return attachmentStatus; - } - private static IObservable TrackAttachmentToTree(ILogical relativeTo) { var attached = Observable.FromEventPattern( diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs new file mode 100644 index 0000000000..bd0d0b8c1f --- /dev/null +++ b/src/Avalonia.Styling/StyledElement.cs @@ -0,0 +1,783 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +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.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, 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 StyledProperty TemplatedParentProperty = + AvaloniaProperty.Register(nameof(TemplatedParent), inherits: true); + + private int _initCount; + private string _name; + private readonly Classes _classes = new Classes(); + private bool _isAttachedToLogicalTree; + private IAvaloniaList _logicalChildren; + private INameScope _nameScope; + private IResourceDictionary _resources; + private Styles _styles; + private bool _styled; + private Subject _styleDetach = new Subject(); + private bool _dataContextUpdating; + + /// + /// Initializes static members of the class. + /// + static StyledElement() + { + DataContextProperty.Changed.AddClassHandler(x => x.OnDataContextChangedCore); + } + + /// + /// Initializes a new instance of the class. + /// + public StyledElement() + { + _nameScope = this as INameScope; + _isAttachedToLogicalTree = this is IStyleRoot; + } + + /// + /// 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 + { + return _name; + } + + set + { + if (String.IsNullOrWhiteSpace(value)) + { + throw new InvalidOperationException("Cannot set Name to null or empty string."); + } + + 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 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 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 + { + get { return _styles ?? (Styles = new Styles()); } + set + { + Contract.Requires(value != null); + + if (_styles != value) + { + if (_styles != null) + { + (_styles as ISetStyleParent)?.SetParent(null); + _styles.ResourcesChanged -= ThisResourcesChanged; + } + + _styles = value; + + if (value is ISetStyleParent setParent && setParent.ResourceParent == null) + { + setParent.SetParent(this); + } + + _styles.ResourcesChanged += ThisResourcesChanged; + } + } + } + + /// + /// Gets or sets the styled element's resource dictionary. + /// + public IResourceDictionary Resources + { + get => _resources ?? (Resources = new ResourceDictionary()); + set + { + Contract.Requires(value != null); + + var hadResources = false; + + if (_resources != null) + { + hadResources = _resources.Count > 0; + _resources.ResourcesChanged -= ThisResourcesChanged; + } + + _resources = value; + _resources.ResourcesChanged += ThisResourcesChanged; + + if (hadResources || _resources.Count > 0) + { + ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + } + } + } + + /// + /// Gets the styled element whose lookless template this styled element is part of. + /// + public ITemplatedControl TemplatedParent + { + get { return GetValue(TemplatedParentProperty); } + internal set { SetValue(TemplatedParentProperty, value); } + } + + /// + /// Gets the styled element's logical children. + /// + protected IAvaloniaList LogicalChildren + { + get + { + if (_logicalChildren == null) + { + var list = new AvaloniaList + { + ResetBehavior = ResetBehavior.Remove, + 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; + + /// + /// Gets a value indicating whether the element is attached to a rooted logical tree. + /// + bool ILogical.IsAttachedToLogicalTree => _isAttachedToLogicalTree; + + /// + /// 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 IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; + + /// + IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; + + /// + 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(); + + /// + IObservable IStyleable.StyleDetach => _styleDetach; + + /// + 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 && _isAttachedToLogicalTree) + { + InitializeStylesIfNeeded(); + + InitializeIfNeeded(); + } + } + + private void InitializeStylesIfNeeded(bool force = false) + { + if (_initCount == 0 && (!_styled || force)) + { + RegisterWithNameScope(); + ApplyStyling(); + _styled = true; + } + } + + protected void InitializeIfNeeded() + { + if (_initCount == 0 && !IsInitialized) + { + IsInitialized = true; + Initialized?.Invoke(this, EventArgs.Empty); + } + } + + /// + void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + this.OnAttachedToLogicalTreeCore(e); + } + + /// + void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + this.OnDetachedFromLogicalTreeCore(e); + } + + /// + void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + + /// + bool IResourceProvider.TryGetResource(string 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 (_isAttachedToLogicalTree) + { + var oldRoot = FindStyleRoot(old) ?? this as IStyleRoot; + + 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 = (IStyledElement)parent; + + if (old != null) + { + old.ResourcesChanged -= ThisResourcesChanged; + } + if (Parent != null) + { + Parent.ResourcesChanged += ThisResourcesChanged; + } + ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + + if (Parent is IStyleRoot || Parent?.IsAttachedToLogicalTree == true || this is IStyleRoot) + { + 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 styled element's inheritance parent. + /// + /// The parent. + void ISetInheritanceParent.SetParent(IAvaloniaObject parent) + { + InheritanceParent = parent; + } + + /// + /// 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); + + if (string.IsNullOrWhiteSpace(className)) + { + throw new ArgumentException("Cannot supply an empty className."); + } + + property.Changed.Merge(property.Initialized) + .Where(e => e.Sender is StyledElement) + .Subscribe(e => + { + if (selector((T)e.NewValue)) + { + ((StyledElement)e.Sender).PseudoClasses.Add(className); + } + else + { + ((StyledElement)e.Sender).PseudoClasses.Remove(className); + } + }); + } + + /// + /// 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() + { + } + + 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(); + + foreach (var child in element.LogicalChildren) + { + if (child is StyledElement s && + s.InheritanceParent == element && + !s.IsSet(DataContextProperty)) + { + DataContextNotifying(s, updateStarted); + } + } + } + } + else + { + if (element._dataContextUpdating) + { + element.OnDataContextEndUpdate(); + element._dataContextUpdating = false; + } + } + } + + private static IStyleRoot FindStyleRoot(IStyleHost e) + { + while (e != null) + { + if (e is IStyleRoot root) + { + 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) ?? ((StyledElement)Parent)?._nameScope; + } + + if (Name != null) + { + _nameScope?.Register(Name, this); + + var visualParent = Parent as StyledElement; + + 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; + + InitializeStylesIfNeeded(true); + + OnAttachedToLogicalTree(e); + AttachedToLogicalTree?.Invoke(this, 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); + DetachedFromLogicalTree?.Invoke(this, 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 OnDataContextChangedCore(AvaloniaPropertyChangedEventArgs e) + { + OnDataContextChanged(EventArgs.Empty); + } + + 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); + } + } + } + + private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e) + { + ((ILogical)this).NotifyResourcesChanged(e); + } + } +} diff --git a/src/Avalonia.Styling/Styling/IRequiresTemplateInStyle.cs b/src/Avalonia.Styling/Styling/IRequiresTemplateInStyle.cs new file mode 100644 index 0000000000..819d783061 --- /dev/null +++ b/src/Avalonia.Styling/Styling/IRequiresTemplateInStyle.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Styling +{ + /// + /// This is an interface for advanced scenarios to assist users in correct style development. + /// You as a user will not need to use this interface directly. + /// + public interface IRequiresTemplateInStyle + { + } +} diff --git a/src/Avalonia.Styling/Styling/IStyleable.cs b/src/Avalonia.Styling/Styling/IStyleable.cs index e03231e4ac..98c8501681 100644 --- a/src/Avalonia.Styling/Styling/IStyleable.cs +++ b/src/Avalonia.Styling/Styling/IStyleable.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Collections; +using Avalonia.LogicalTree; namespace Avalonia.Styling { diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index e4d0ce5fab..337e01c0b5 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -65,7 +65,7 @@ namespace Avalonia.Styling set { - if (value is IStyleable) + if (value is IRequiresTemplateInStyle) { throw new ArgumentException( "Cannot assign a control to Style.Value. Wrap the control in a