View Issue Details

IDProjectCategoryView StatusLast Update
0002498NoesisGUIUnity3Dpublic2023-10-16 20:38
Reporterjphyzic Assigned Tosfernandez  
PrioritynormalSeverityminorReproducibilityalways
Status resolvedResolutionfixed 
Product Version3.1.4 
Target Version3.2.0Fixed in Version3.2.0 
Summary0002498: Behaviors gets detached when associated object is temporarily removed from visual tree
DescriptionAssume that we have a UI with several controls with attached behaviors and user wishes to rearrange them from code behind. To do that he should temporarily remove controls from their parents and add them to a new place. Problem is that when a control gets removed from visual tree all behaviors attached to it gets detached and are not reattached when the control is added back.

See more: https://www.noesisengine.com/forums/viewtopic.php?p=15217
Steps To Reproduce1. Create a userControl with the following markup:
<UserControl
    ... namespaces skipped
    mc:Ignorable="d">
    <StackPanel Orientation="Vertical">
        <ItemsControl x:Name="itemsControl">
            <Border Width="100" Height="100" Background="Red">
                <i:Interaction.Behaviors>
                    <attach:TestBehaviour />
                </i:Interaction.Behaviors>
            </Border>
        </ItemsControl>
        <Button Click="Button_Click">Sort</Button>
    </StackPanel>
</UserControl>

TestBehaviour implementation:
public class TestBehaviour : Behavior<FrameworkElement>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        System.Diagnostics.Debug.WriteLine($"Attached to {AssociatedObject}");
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        System.Diagnostics.Debug.WriteLine($"Detached");
    }
}

2. Create a button handler in code behind:
private void Button_Click(object sender, RoutedEventArgs e)
{
    var itemsControl = FindName("itemsControl") as ItemsControl;
    var item = itemsControl.Items[0];
    itemsControl.Items.RemoveAt(0);
    itemsControl.Items.Insert(0, item);
}

On app startup the TestBehaviour gets attached. Clicking on a button causes the Border element to be temporarily removed from ItemsControl and added back. It causes the TestBehaviour to be detached.
TagsNo tags attached.
PlatformAny

Relationships

related to 0002730 assignedsfernandez AssociatedObject is null while detaching Behaviours 

Activities

jphyzic

jphyzic

2023-02-03 10:24

reporter   ~0008257

I've managed to find a workaround. Not sure if it's reliable, but better than nothing:

public static class NoesisBehaviorsFixes
    {
        /// <summary>
        /// Fixes detaching behaviours when manipulating UI items.
        /// </summary>
        /// <remarks>
        /// When a visual is removed from visual tree and this visual has an attached behaviours in it
        /// (or its children) these behaviours gets a notification that they are removed from visual tree
        /// and detach themselves, but adding them back to tree does not make them reattach.
        /// This method remembers all objects that were detached and not reattached until returned object
        /// is disposed and detaches them on dispose.
        /// </remarks>
        /// <returns></returns>
        public static IDisposable GuardDetachingBehaviors()
        {
            return new AttachmentTracker();
        }

        private class AttachmentTracker : IDisposable
        {
            private readonly HashSet<AttachableObject> objectsTodetach = new HashSet<AttachableObject>();
            private readonly PropertyMetadata metadata;
            private readonly PropertyChangedCallback originalChangeCallback;
            private bool isDisposed;

            public AttachmentTracker()
            {
                var type = typeof(AttachableObject);
                var field = type.GetField("AttachmentProperty", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
                var attachmentProperty = field.GetValue(null) as DependencyProperty;
                if (attachmentProperty == null)
                {
                    isDisposed = true;
                    return;
                }

                metadata = attachmentProperty.GetMetadata(typeof(AttachableObject));
                originalChangeCallback = metadata.PropertyChangedCallback;
                metadata.PropertyChangedCallback = OnAttachmentChanged;
            }

            public void Dispose()
            {
                if (!isDisposed)
                {
                    DetachObjects();
                    metadata.PropertyChangedCallback = originalChangeCallback;
                    isDisposed = true;
                }
            }

            private void OnAttachmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                if ((Visibility)e.NewValue == (Visibility)(-1))
                {
                    if (d is AttachableObject obj)
                        objectsTodetach.Add(obj);
                }
                else
                {
                    if (d is AttachableObject obj)
                        objectsTodetach.Remove(obj);
                }
            }

            private void DetachObjects()
            {
                foreach (var item in objectsTodetach)
                    item.Detach();
            }
        }
    }

Usage: any manipulation with UI elements that involves detaching them from visual tree should be guarded:

using (NoesisBehavioursFixes.GuardDetachingBehaviors())
{
    ... manipulate UI elements
}
sfernandez

sfernandez

2023-02-14 11:09

manager   ~0008281

This is the patch:

Index: AttachableObject.cs
===================================================================
--- AttachableObject.cs	(revision 12128)
+++ AttachableObject.cs	(revision 12129)
@@ -53,14 +53,10 @@
                         associatedObject.GetType(), GetType()));
                 }
 
+                associatedObject.Destroyed += OnAssociatedObjectDestroyed;
                 _associatedObject = GetPtr(associatedObject);
                 _view = GetPtr(View.Find(this));
 
-                BindingOperations.SetBinding(this, AttachmentProperty, new Binding("Visibility")
-                {
-                    RelativeSource = new RelativeSource { AncestorType = typeof(UIElement) }
-                });
-
                 InitObject();
 
                 OnAttached();
@@ -69,7 +65,7 @@
 
         public void Detach()
         {
-            if (AssociatedObject != null)
+            if (_associatedObject != IntPtr.Zero)
             {
                 OnDetaching();
 
@@ -100,19 +96,9 @@
             get { return (View)GetProxy(_view); }
         }
 
-        // We use a binding to get notified when the associated object is disconnected from the tree
-        // and is going to be destroyed, so we can properly detach from it
-        private static readonly DependencyProperty AttachmentProperty = DependencyProperty.Register(
-            ".Attachment", typeof(Visibility), typeof(AttachableObject),
-            new PropertyMetadata(Detached, OnAttachmentChanged));
-
-        private static void OnAttachmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        private void OnAssociatedObjectDestroyed(IntPtr d)
         {
-            if ((Visibility)e.NewValue == Detached)
-            {
-                AttachableObject attachable = (AttachableObject)d;
-                attachable.Detach();
-            }
+            Detach();
         }
 
         private const Visibility Detached = (Visibility)(-1);

Issue History

Date Modified Username Field Change
2023-01-31 07:23 jphyzic New Issue
2023-02-03 10:24 jphyzic Note Added: 0008257
2023-02-03 19:24 sfernandez Assigned To => sfernandez
2023-02-03 19:24 sfernandez Status new => assigned
2023-02-03 19:24 sfernandez Target Version => 3.2.0
2023-02-14 11:09 sfernandez Status assigned => resolved
2023-02-14 11:09 sfernandez Resolution open => fixed
2023-02-14 11:09 sfernandez Fixed in Version => 3.2.0
2023-02-14 11:09 sfernandez Note Added: 0008281
2023-10-16 20:38 jsantos Relationship added related to 0002730