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.");
+ }
+ }
}