diff --git a/docfx/articles/interactions-custom/core/detached-from-logical-tree-trigger.md b/docfx/articles/interactions-custom/core/detached-from-logical-tree-trigger.md index d5940dd86..3bb8b34f7 100644 --- a/docfx/articles/interactions-custom/core/detached-from-logical-tree-trigger.md +++ b/docfx/articles/interactions-custom/core/detached-from-logical-tree-trigger.md @@ -1,3 +1,5 @@ # DetachedFromLogicalTreeTrigger Executes when the associated object is detached from the logical tree. + +The trigger executes before its behavior/action logical scope is released, so bound actions retain their data context for the detach callback. diff --git a/docfx/articles/interactions-custom/core/detached-from-visual-tree-trigger.md b/docfx/articles/interactions-custom/core/detached-from-visual-tree-trigger.md index 50ae4a8d5..6a70091b9 100644 --- a/docfx/articles/interactions-custom/core/detached-from-visual-tree-trigger.md +++ b/docfx/articles/interactions-custom/core/detached-from-visual-tree-trigger.md @@ -1,3 +1,5 @@ # DetachedFromVisualTreeTrigger Executes when the associated object is detached from the visual tree. + +The trigger executes before its behavior/action logical scope is released. Bound actions therefore retain their data context for the detach callback, including when content leaves the visual tree because a `TabControl` selection changes. diff --git a/src/Xaml.Behaviors.Interactivity/StyledElement/StyledElementBehavior.cs b/src/Xaml.Behaviors.Interactivity/StyledElement/StyledElementBehavior.cs index 128b1cf0b..7c241afb7 100644 --- a/src/Xaml.Behaviors.Interactivity/StyledElement/StyledElementBehavior.cs +++ b/src/Xaml.Behaviors.Interactivity/StyledElement/StyledElementBehavior.cs @@ -118,12 +118,17 @@ void IBehaviorEventsHandler.AttachedToVisualTreeEventHandler() void IBehaviorEventsHandler.DetachedFromVisualTreeEventHandler() { - if (AssociatedObject is not TopLevel) + try { - DetachBehaviorFromLogicalTree(); + OnDetachedFromVisualTree(); + } + finally + { + if (AssociatedObject is not TopLevel) + { + DetachBehaviorFromLogicalTree(); + } } - - OnDetachedFromVisualTree(); } void IBehaviorEventsHandler.AttachedToLogicalTreeEventHandler() @@ -135,12 +140,17 @@ void IBehaviorEventsHandler.AttachedToLogicalTreeEventHandler() void IBehaviorEventsHandler.DetachedFromLogicalTreeEventHandler() { - if (AssociatedObject is not TopLevel) + try { - DetachBehaviorFromLogicalTree(); + OnDetachedFromLogicalTree(); + } + finally + { + if (AssociatedObject is not TopLevel) + { + DetachBehaviorFromLogicalTree(); + } } - - OnDetachedFromLogicalTree(); } void IBehaviorEventsHandler.LoadedEventHandler() => OnLoaded(); diff --git a/tests/Xaml.Behaviors.Interactions.UnitTests/Custom/DetachedFromVisualTreeTrigger001.axaml b/tests/Xaml.Behaviors.Interactions.UnitTests/Custom/DetachedFromVisualTreeTrigger001.axaml new file mode 100644 index 000000000..de1733dc5 --- /dev/null +++ b/tests/Xaml.Behaviors.Interactions.UnitTests/Custom/DetachedFromVisualTreeTrigger001.axaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Xaml.Behaviors.Interactions.UnitTests/Custom/DetachedFromVisualTreeTrigger001.axaml.cs b/tests/Xaml.Behaviors.Interactions.UnitTests/Custom/DetachedFromVisualTreeTrigger001.axaml.cs new file mode 100644 index 000000000..2123b04ca --- /dev/null +++ b/tests/Xaml.Behaviors.Interactions.UnitTests/Custom/DetachedFromVisualTreeTrigger001.axaml.cs @@ -0,0 +1,25 @@ +using System.Windows.Input; +using Avalonia.Controls; +using Avalonia.Xaml.Interactions.UnitTests.Core; + +namespace Avalonia.Xaml.Interactions.UnitTests.Custom; + +public partial class DetachedFromVisualTreeTrigger001 : Window +{ + public DetachedFromVisualTreeTrigger001() + { + InitializeComponent(); + } +} + +public sealed class DetachedTriggerBindingSource +{ + public DetachedTriggerBindingSource() + { + DetachedCommand = new Command(_ => DetachedCount++); + } + + public ICommand DetachedCommand { get; } + + public int DetachedCount { get; private set; } +} diff --git a/tests/Xaml.Behaviors.Interactions.UnitTests/Custom/DetachedFromVisualTreeTriggerTests.cs b/tests/Xaml.Behaviors.Interactions.UnitTests/Custom/DetachedFromVisualTreeTriggerTests.cs new file mode 100644 index 000000000..c8162048b --- /dev/null +++ b/tests/Xaml.Behaviors.Interactions.UnitTests/Custom/DetachedFromVisualTreeTriggerTests.cs @@ -0,0 +1,25 @@ +using Avalonia.Headless; +using Avalonia.Headless.XUnit; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Xaml.Interactions.UnitTests.Custom; + +public class DetachedFromVisualTreeTriggerTests +{ + [AvaloniaFact] + public void SwitchingTabs_ExecutesBoundDetachedCommand() + { + var window = new DetachedFromVisualTreeTrigger001(); + var source = Assert.IsType(window.DataContext); + + window.Show(); + window.CaptureRenderedFrame(); + + window.TargetTabs.SelectedIndex = 1; + Dispatcher.UIThread.RunJobs(); + window.CaptureRenderedFrame(); + + Assert.Equal(1, source.DetachedCount); + } +} diff --git a/tests/Xaml.Behaviors.Interactivity.UnitTests/StyledElementBehaviorTests.cs b/tests/Xaml.Behaviors.Interactivity.UnitTests/StyledElementBehaviorTests.cs index 13e55a78b..25076cb40 100644 --- a/tests/Xaml.Behaviors.Interactivity.UnitTests/StyledElementBehaviorTests.cs +++ b/tests/Xaml.Behaviors.Interactivity.UnitTests/StyledElementBehaviorTests.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Controls; using Avalonia.Headless.XUnit; using Avalonia.Threading; @@ -33,6 +34,29 @@ public void Detach_ClearsLogicalParentAndTemplatedParent() window.Close(); } + [AvaloniaFact] + public void DetachVisualTree_ClearsLogicalScopeWhenCallbackThrows() + { + var behavior = new ThrowingDetachBehavior(); + var button = new Button(); + var templatedParent = new ContentControl(); + var window = new Window { Content = button }; + + TemplatedParentHelper.SetTemplatedParent(button, templatedParent); + Interaction.GetBehaviors(button).Add(behavior); + window.Show(); + + Assert.Throws(() => + ((IBehaviorEventsHandler)behavior).DetachedFromVisualTreeEventHandler()); + + Assert.Null(behavior.Parent); + Assert.Null(behavior.TemplatedParent); + + behavior.Detach(); + Interaction.SetBehaviors(button, null); + window.Close(); + } + [AvaloniaFact] public void TopLevelClose_PreservesLogicalParentUntilDeferredDetach() { @@ -57,4 +81,12 @@ public void TopLevelClose_PreservesLogicalParentUntilDeferredDetach() } private sealed class TestStyledElementBehavior : StyledElementBehavior; + + private sealed class ThrowingDetachBehavior : StyledElementBehavior + { + protected override void OnDetachedFromVisualTree() + { + throw new InvalidOperationException("Expected test exception."); + } + } }