diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 9200490668..22e06a40b1 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -127,7 +127,7 @@ namespace Avalonia.Controls.Generators Index = new TreeContainerIndex(); _treeView = treeViewOwner; } - else if (Owner.IsAttachedToLogicalTree) + else { var treeView = Owner.GetSelfAndLogicalAncestors().OfType().FirstOrDefault(); @@ -138,12 +138,6 @@ namespace Avalonia.Controls.Generators _treeView = treeView; } } - else - { - Clear(); - Index = null; - _treeView = null; - } } class WrapperTreeDataTemplate : ITreeDataTemplate diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs index b7229eb121..00652f21ba 100644 --- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -8,7 +8,7 @@ namespace Avalonia.Controls.Primitives { private const int AdornerZIndex = int.MaxValue - 100; private const int OverlayZIndex = int.MaxValue - 99; - private IStyleHost _styleRoot; + private IStyleRoot _styleRoot; private readonly List _layers = new List(); @@ -53,7 +53,7 @@ namespace Avalonia.Controls.Primitives layer.ZIndex = zindex; VisualChildren.Add(layer); if (((ILogical)this).IsAttachedToLogicalTree) - ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_styleRoot)); + ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_styleRoot, layer, this)); InvalidateArrange(); } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5a8711a21c..799e6c81d6 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -266,7 +266,7 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { - var logicalArgs = new LogicalTreeAttachmentEventArgs(this); + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); var visualArgs = new VisualTreeAttachmentEventArgs(this, this); diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs index 25d3b6887f..bcd962bc31 100644 --- a/src/Avalonia.Input/DragDropDevice.cs +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -19,7 +19,7 @@ namespace Avalonia.Input return null; } - private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, InputModifiers modifiers) + private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers) { if (target == null) return DragDropEffects.None; @@ -38,13 +38,13 @@ namespace Avalonia.Input return args.DragEffects; } - private DragDropEffects DragEnter(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects DragEnter(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { _lastTarget = GetTarget(inputRoot, point); return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers); } - private DragDropEffects DragOver(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects DragOver(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { var target = GetTarget(inputRoot, point); @@ -77,7 +77,7 @@ namespace Avalonia.Input } } - private DragDropEffects Drop(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects Drop(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { try { @@ -100,16 +100,16 @@ namespace Avalonia.Input switch (e.Type) { case RawDragEventType.DragEnter: - e.Effects = DragEnter(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = DragEnter(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; case RawDragEventType.DragOver: - e.Effects = DragOver(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = DragOver(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; case RawDragEventType.DragLeave: DragLeave(e.Root); break; case RawDragEventType.Drop: - e.Effects = Drop(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = Drop(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; } } diff --git a/src/Avalonia.Input/DragEventArgs.cs b/src/Avalonia.Input/DragEventArgs.cs index dc0b76b225..08e9eb7d33 100644 --- a/src/Avalonia.Input/DragEventArgs.cs +++ b/src/Avalonia.Input/DragEventArgs.cs @@ -13,8 +13,11 @@ namespace Avalonia.Input public IDataObject Data { get; private set; } + [Obsolete("Use KeyModifiers")] public InputModifiers Modifiers { get; private set; } + public KeyModifiers KeyModifiers { get; private set; } + public Point GetPosition(IVisual relativeTo) { var point = new Point(0, 0); @@ -32,13 +35,25 @@ namespace Avalonia.Input return point; } + [Obsolete("Use constructor taking KeyModifiers")] public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, InputModifiers modifiers) : base(routedEvent) { - this.Data = data; - this._target = target; - this._targetLocation = targetLocation; - this.Modifiers = modifiers; + Data = data; + _target = target; + _targetLocation = targetLocation; + Modifiers = modifiers; + KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xF); + } + + public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) + : base(routedEvent) + { + Data = data; + _target = target; + _targetLocation = targetLocation; + Modifiers = (InputModifiers)keyModifiers; + KeyModifiers = keyModifiers; } } diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs index 5722e17593..4193cdafc5 100644 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ b/src/Avalonia.Input/Raw/RawDragEvent.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Input.Raw +using System; + +namespace Avalonia.Input.Raw { public class RawDragEvent : RawInputEventArgs { @@ -6,7 +8,9 @@ public IDataObject Data { get; } public DragDropEffects Effects { get; set; } public RawDragEventType Type { get; } + [Obsolete("Use KeyModifiers")] public InputModifiers Modifiers { get; } + public KeyModifiers KeyModifiers { get; } public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, IInputRoot root, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) @@ -17,6 +21,7 @@ Data = data; Effects = effects; Modifiers = (InputModifiers)modifiers; + KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); } } } diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 65c2a75b1c..1a2bdeef1e 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -19,8 +19,9 @@ - - + + + diff --git a/src/Avalonia.Native/CallbackBase.cs b/src/Avalonia.Native/CallbackBase.cs index 67c383f6ae..1356dd58ff 100644 --- a/src/Avalonia.Native/CallbackBase.cs +++ b/src/Avalonia.Native/CallbackBase.cs @@ -2,11 +2,13 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Runtime.ExceptionServices; using SharpGen.Runtime; +using Avalonia.Platform; namespace Avalonia.Native { - public class CallbackBase : SharpGen.Runtime.IUnknown + public class CallbackBase : SharpGen.Runtime.IUnknown, IExceptionCallback { private uint _refCount; private bool _disposed; @@ -76,5 +78,15 @@ namespace Avalonia.Native { } + + public void RaiseException(Exception e) + { + if (AvaloniaLocator.Current.GetService() is PlatformThreadingInterface threadingInterface) + { + threadingInterface.TerminateNativeApp(); + + threadingInterface.DispatchException(ExceptionDispatchInfo.Capture(e)); + } + } } } diff --git a/src/Avalonia.Native/PlatformThreadingInterface.cs b/src/Avalonia.Native/PlatformThreadingInterface.cs index 353124a9d1..5ddaf83ed4 100644 --- a/src/Avalonia.Native/PlatformThreadingInterface.cs +++ b/src/Avalonia.Native/PlatformThreadingInterface.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Runtime.ExceptionServices; using System.Threading; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -43,6 +44,8 @@ namespace Avalonia.Native } readonly IAvnPlatformThreadingInterface _native; + private ExceptionDispatchInfo _exceptionDispatchInfo; + private CancellationTokenSource _exceptionCancellationSource; public PlatformThreadingInterface(IAvnPlatformThreadingInterface native) { @@ -57,32 +60,48 @@ namespace Avalonia.Native public void RunLoop(CancellationToken cancellationToken) { - if (cancellationToken.CanBeCanceled == false) - _native.RunLoop(null); - else + var l = new object(); + _exceptionCancellationSource = new CancellationTokenSource(); + + var compositeCancellation = CancellationTokenSource + .CreateLinkedTokenSource(cancellationToken, _exceptionCancellationSource.Token).Token; + + var cancellation = _native.CreateLoopCancellation(); + compositeCancellation.Register(() => { - var l = new object(); - var cancellation = _native.CreateLoopCancellation(); - cancellationToken.Register(() => + lock (l) { - lock (l) - { - cancellation?.Cancel(); - } - }); - try - { - _native.RunLoop(cancellation); + cancellation?.Cancel(); } - finally + }); + + try + { + _native.RunLoop(cancellation); + } + finally + { + lock (l) { - lock(l) - { - cancellation?.Dispose(); - cancellation = null; - } + cancellation?.Dispose(); + cancellation = null; } } + + if (_exceptionDispatchInfo != null) + { + _exceptionDispatchInfo.Throw(); + } + } + + public void DispatchException (ExceptionDispatchInfo exceptionInfo) + { + _exceptionDispatchInfo = exceptionInfo; + } + + public void TerminateNativeApp() + { + _exceptionCancellationSource?.Cancel(); } public void Signal(DispatcherPriority priority) diff --git a/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs b/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs index 1b0eb2b61b..dc1b801a5c 100644 --- a/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs +++ b/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs @@ -16,16 +16,44 @@ namespace Avalonia.LogicalTree /// Initializes a new instance of the class. /// /// The root of the logical tree. - public LogicalTreeAttachmentEventArgs(IStyleHost root) + /// The control being attached/detached. + /// The . + public LogicalTreeAttachmentEventArgs( + IStyleRoot root, + ILogical source, + ILogical parent) { Contract.Requires(root != null); + Contract.Requires(source != null); Root = root; + Source = source; + Parent = parent; } /// /// Gets the root of the logical tree that the control is being attached to or detached from. /// - public IStyleHost Root { get; } + public IStyleRoot Root { get; } + + /// + /// Gets the control that was attached or detached from the logical tree. + /// + /// + /// Logical tree attachment events travel down the attached logical tree from the point of + /// attachment/detachment, so this control may be different from the control that the + /// event is being raised on. + /// + public ILogical Source { get; } + + /// + /// Gets the control that is being attached to or detached from. + /// + /// + /// For logical tree attachment, holds the new logical parent of . For + /// detachment, holds the old logical parent of . If the detachment event + /// was caused by a top-level control being closed, then this property will be null. + /// + public ILogical Parent { get; } } } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 6aa75effe6..83e37d826e 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -59,7 +59,7 @@ namespace Avalonia private int _initCount; private string _name; private readonly Classes _classes = new Classes(); - private bool _isAttachedToLogicalTree; + private IStyleRoot _styleRoot; private IAvaloniaList _logicalChildren; private IResourceDictionary _resources; private Styles _styles; @@ -81,7 +81,7 @@ namespace Avalonia /// public StyledElement() { - _isAttachedToLogicalTree = this is IStyleRoot; + _styleRoot = this as IStyleRoot; } /// @@ -307,7 +307,7 @@ namespace Avalonia /// /// Gets a value indicating whether the element is attached to a rooted logical tree. /// - bool ILogical.IsAttachedToLogicalTree => _isAttachedToLogicalTree; + bool ILogical.IsAttachedToLogicalTree => _styleRoot != null; /// /// Gets the styled element's logical parent. @@ -367,7 +367,7 @@ namespace Avalonia throw new InvalidOperationException("BeginInit was not called."); } - if (--_initCount == 0 && _isAttachedToLogicalTree) + if (--_initCount == 0 && _styleRoot != null) { InitializeStylesIfNeeded(); @@ -435,19 +435,6 @@ namespace Avalonia 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; @@ -455,6 +442,12 @@ namespace Avalonia Parent = (IStyledElement)parent; + if (_styleRoot != null) + { + var e = new LogicalTreeAttachmentEventArgs(_styleRoot, this, old); + OnDetachedFromLogicalTreeCore(e); + } + if (old != null) { old.ResourcesChanged -= ThisResourcesChanged; @@ -474,7 +467,7 @@ namespace Avalonia throw new AvaloniaInternalException("Parent is attached to logical tree but cannot find root."); } - var e = new LogicalTreeAttachmentEventArgs(newRoot); + var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent); OnAttachedToLogicalTreeCore(e); } @@ -708,15 +701,21 @@ namespace Avalonia private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { + if (this.GetLogicalParent() == null && !(this is IStyleRoot)) + { + 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 (!_isAttachedToLogicalTree) + if (_styleRoot == null) { - _isAttachedToLogicalTree = true; + _styleRoot = e.Root; InitializeStylesIfNeeded(true); @@ -732,9 +731,9 @@ namespace Avalonia private void OnDetachedFromLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { - if (_isAttachedToLogicalTree) + if (_styleRoot != null) { - _isAttachedToLogicalTree = false; + _styleRoot = null; _styleDetach.OnNext(this); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); diff --git a/src/Avalonia.X11/X11Framebuffer.cs b/src/Avalonia.X11/X11Framebuffer.cs index 00288f300d..94f930e9ec 100644 --- a/src/Avalonia.X11/X11Framebuffer.cs +++ b/src/Avalonia.X11/X11Framebuffer.cs @@ -14,6 +14,10 @@ namespace Avalonia.X11 public X11Framebuffer(IntPtr display, IntPtr xid, int depth, int width, int height, double factor) { + // HACK! Please fix renderer, should never ask for 0x0 bitmap. + width = Math.Max(1, width); + height = Math.Max(1, height); + _display = display; _xid = xid; _depth = depth; diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 6ff5b96f12..919abae243 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -102,7 +102,7 @@ namespace Avalonia.X11 valueMask |= SetWindowValuemask.ColorMap; } - int defaultWidth = 300, defaultHeight = 200; + int defaultWidth = 0, defaultHeight = 0; if (!_popup && Screen != null) { @@ -117,6 +117,10 @@ namespace Avalonia.X11 } } + // check if the calculated size is zero then compensate to hardcoded resolution + defaultWidth = Math.Max(defaultWidth, 300); + defaultHeight = Math.Max(defaultHeight, 200); + _handle = XCreateWindow(_x11.Display, _x11.RootWindow, 10, 10, defaultWidth, defaultHeight, 0, depth, (int)CreateWindowArgs.InputOutput, @@ -133,7 +137,7 @@ namespace Avalonia.X11 _renderHandle = _handle; Handle = new PlatformHandle(_handle, "XID"); - _realSize = new PixelSize(300, 200); + _realSize = new PixelSize(defaultWidth, defaultHeight); platform.Windows[_handle] = OnEvent; XEventMask ignoredMask = XEventMask.SubstructureRedirectMask | XEventMask.ResizeRedirectMask diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index 8a5a725594..a23c31ca64 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -75,7 +75,7 @@ namespace Avalonia.Shared.PlatformSupport lock (_btlock) Backtraces.Remove(_backtrace); #endif - _plat.Free(_address, Size); + _plat?.Free(_address, Size); GC.RemoveMemoryPressure(Size); IsDisposed = true; _address = IntPtr.Zero; diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 72c81659c3..91592b1ff7 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -166,6 +166,31 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Impl_Close_Should_Raise_DetachedFromLogicalTree_Event() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + impl.SetupAllProperties(); + + var target = new TestTopLevel(impl.Object); + var raised = 0; + + target.DetachedFromLogicalTree += (s, e) => + { + Assert.Same(target, e.Root); + Assert.Same(target, e.Source); + Assert.Null(e.Parent); + ++raised; + }; + + impl.Object.Closed(); + + Assert.Equal(1, raised); + } + } + [Fact] public void Impl_Input_Should_Pass_Input_To_InputManager() { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index ed8a39d063..a2892be5c2 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -971,6 +971,40 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Removing_TreeView_From_Root_Should_Preserve_TreeViewItems() + { + // Issue #3328 + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var root = new TestRoot(); + root.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + Assert.Equal(5, target.ItemContainerGenerator.Index.Containers.Count()); + + root.Child = null; + + Assert.Equal(5, target.ItemContainerGenerator.Index.Containers.Count()); + Assert.Equal(1, target.Presenter.Panel.Children.Count); + + var rootNode = Assert.IsType(target.Presenter.Panel.Children[0]); + Assert.Equal(3, rootNode.ItemContainerGenerator.Containers.Count()); + Assert.Equal(3, rootNode.Presenter.Panel.Children.Count); + + var child2Node = Assert.IsType(rootNode.Presenter.Panel.Children[1]); + Assert.Equal(1, child2Node.ItemContainerGenerator.Containers.Count()); + Assert.Equal(1, child2Node.Presenter.Panel.Children.Count); + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); diff --git a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs index c40e6c735e..ae519ed674 100644 --- a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs @@ -76,6 +76,29 @@ namespace Avalonia.Styling.UnitTests Assert.Null(target.InheritanceParent); } + [Fact] + public void Adding_Element_With_Null_Parent_To_Logical_Tree_Should_Throw() + { + var target = new Border(); + var visualParent = new Panel(); + var logicalParent = new Panel(); + var root = new TestRoot(); + + // Set the logical parent... + ((ISetLogicalParent)target).SetParent(logicalParent); + + // ...so that when it's added to `visualParent`, the parent won't be set again. + visualParent.Children.Add(target); + + // Clear the logical parent. It's now a logical child of `visualParent` but doesn't have + // a logical parent itself. + ((ISetLogicalParent)target).SetParent(null); + + // In this case, attaching the control to a logical tree should throw. + logicalParent.Children.Add(visualParent); + Assert.Throws(() => root.Child = logicalParent); + } + [Fact] public void AttachedToLogicalParent_Should_Be_Called_When_Added_To_Tree() { @@ -144,6 +167,50 @@ namespace Avalonia.Styling.UnitTests Assert.True(raised); } + [Fact] + public void AttachedToLogicalParent_Should_Have_Source_Set() + { + var root = new TestRoot(); + var canvas = new Canvas(); + var border = new Border { Child = canvas }; + var raised = 0; + + void Attached(object sender, LogicalTreeAttachmentEventArgs e) + { + Assert.Same(border, e.Source); + ++raised; + } + + border.AttachedToLogicalTree += Attached; + canvas.AttachedToLogicalTree += Attached; + + root.Child = border; + + Assert.Equal(2, raised); + } + + [Fact] + public void AttachedToLogicalParent_Should_Have_Parent_Set() + { + var root = new TestRoot(); + var canvas = new Canvas(); + var border = new Border { Child = canvas }; + var raised = 0; + + void Attached(object sender, LogicalTreeAttachmentEventArgs e) + { + Assert.Same(root, e.Parent); + ++raised; + } + + border.AttachedToLogicalTree += Attached; + canvas.AttachedToLogicalTree += Attached; + + root.Child = border; + + Assert.Equal(2, raised); + } + [Fact] public void DetachedFromLogicalParent_Should_Be_Called_When_Removed_From_Tree() { @@ -190,6 +257,25 @@ namespace Avalonia.Styling.UnitTests Assert.True(raised); } + [Fact] + public void Parent_Should_Be_Null_When_DetachedFromLogicalParent_Called() + { + var target = new TestControl(); + var root = new TestRoot(target); + var called = 0; + + target.DetachedFromLogicalTree += (s, e) => + { + Assert.Null(target.Parent); + Assert.Null(target.InheritanceParent); + ++called; + }; + + root.Child = null; + + Assert.Equal(1, called); + } + [Fact] public void Adding_Tree_To_IStyleRoot_Should_Style_Controls() {