Browse Source

Introduced name scope chains

Nikita Tsukanov 6 years ago
parent
commit
3169f59da8

+ 2 - 1
build/Base.props

@@ -1,5 +1,6 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
     <PackageReference Include="System.ValueTuple" Version="4.5.0" />
+    <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.3" />
   </ItemGroup>
-</Project>
+</Project>

+ 54 - 0
src/Avalonia.Styling/Controls/ChildNameScope.cs

@@ -0,0 +1,54 @@
+using System.Threading.Tasks;
+
+namespace Avalonia.Controls
+{
+    public class ChildNameScope : INameScope
+    {
+        private readonly INameScope _parentScope;
+        private readonly NameScope _inner = new NameScope();
+
+        public ChildNameScope(INameScope parentScope)
+        {
+            _parentScope = parentScope;
+        }
+        
+        public void Register(string name, object element) => _inner.Register(name, element);
+
+        public ValueTask<object> FindAsync(string name)
+        {
+            var found = Find(name);
+            if (found != null)
+                return new ValueTask<object>(found);
+            // Not found and both current and parent scope are in completed stage
+            if(IsCompleted)
+                return new ValueTask<object>(null);
+            return DoFindAsync(name);
+        }
+
+        async ValueTask<object> DoFindAsync(string name)
+        {
+            if (!_inner.IsCompleted)
+            {
+                var found = await _inner.FindAsync(name);
+                if (found != null)
+                    return found;
+            }
+
+            return await _parentScope.FindAsync(name);
+        }
+
+        public object Find(string name)
+        {
+            var found = _inner.Find(name);
+            if (found != null)
+                return found;
+            if (_inner.IsCompleted)
+                return _parentScope.Find(name);
+            return null;
+        }
+
+        public void Complete() => _inner.Complete();
+
+        public bool IsCompleted => _inner.IsCompleted && _parentScope.IsCompleted;
+    }
+}

+ 18 - 14
src/Avalonia.Styling/Controls/INameScope.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.Threading.Tasks;
 
 namespace Avalonia.Controls
 {
@@ -10,16 +11,6 @@ namespace Avalonia.Controls
     /// </summary>
     public interface INameScope
     {
-        /// <summary>
-        /// Raised when an element is registered with the name scope.
-        /// </summary>
-        event EventHandler<NameScopeEventArgs> Registered;
-
-        /// <summary>
-        /// Raised when an element is unregistered with the name scope.
-        /// </summary>
-        event EventHandler<NameScopeEventArgs> Unregistered;
-
         /// <summary>
         /// Registers an element in the name scope.
         /// </summary>
@@ -28,16 +19,29 @@ namespace Avalonia.Controls
         void Register(string name, object element);
 
         /// <summary>
-        /// Finds a named element in the name scope.
+        /// Finds a named element in the name scope, waits for the scope to be completely populated before returning null
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <returns>The element, or null if the name was not found.</returns>
+        ValueTask<object> FindAsync(string name);
+        
+        /// <summary>
+        /// Finds a named element in the name scope, returns immediately, doesn't traverse the name scope stack
         /// </summary>
         /// <param name="name">The name.</param>
         /// <returns>The element, or null if the name was not found.</returns>
         object Find(string name);
 
         /// <summary>
-        /// Unregisters an element with the name scope.
+        /// Marks the name scope as completed, no further registrations will be allowed
         /// </summary>
-        /// <param name="name">The name.</param>
-        void Unregister(string name);
+        void Complete();
+        
+        /// <summary>
+        /// Returns whether further registrations are allowed on the scope
+        /// </summary>
+        bool IsCompleted { get; }
+
+
     }
 }

+ 35 - 62
src/Avalonia.Styling/Controls/NameScope.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Threading.Tasks;
 using Avalonia.LogicalTree;
 
 namespace Avalonia.Controls
@@ -18,44 +19,14 @@ namespace Avalonia.Controls
         public static readonly AttachedProperty<INameScope> NameScopeProperty =
             AvaloniaProperty.RegisterAttached<NameScope, StyledElement, INameScope>("NameScope");
 
+        /// <inheritdoc/>
+        public bool IsCompleted { get; private set; }
+        
         private readonly Dictionary<string, object> _inner = new Dictionary<string, object>();
 
-        /// <summary>
-        /// Raised when an element is registered with the name scope.
-        /// </summary>
-        public event EventHandler<NameScopeEventArgs> Registered;
-
-        /// <summary>
-        /// Raised when an element is unregistered with the name scope.
-        /// </summary>
-        public event EventHandler<NameScopeEventArgs> Unregistered;
-
-        /// <summary>
-        /// Finds the containing name scope for a styled element.
-        /// </summary>
-        /// <param name="styled">The styled element.</param>
-        /// <returns>The containing name scope.</returns>
-        public static INameScope FindNameScope(StyledElement styled)
-        {
-            Contract.Requires<ArgumentNullException>(styled != null);
-
-            INameScope result;
-
-            while (styled != null)
-            {
-                result = styled as INameScope ?? GetNameScope(styled);
-
-                if (result != null)
-                {
-                    return result;
-                }
-
-                styled = (styled as ILogical)?.LogicalParent as StyledElement;
-            }
-
-            return null;
-        }
-
+        private readonly Dictionary<string, TaskCompletionSource<object>> _pendingSearches =
+            new Dictionary<string, TaskCompletionSource<object>>();
+        
         /// <summary>
         /// Gets the value of the attached <see cref="NameScopeProperty"/> on a styled element.
         /// </summary>
@@ -80,13 +51,11 @@ namespace Avalonia.Controls
             styled.SetValue(NameScopeProperty, value);
         }
 
-        /// <summary>
-        /// Registers an element with the name scope.
-        /// </summary>
-        /// <param name="name">The element name.</param>
-        /// <param name="element">The element.</param>
+        /// <inheritdoc />
         public void Register(string name, object element)
         {
+            if (IsCompleted)
+                throw new InvalidOperationException("NameScope is completed, no further registrations are allowed");
             Contract.Requires<ArgumentNullException>(name != null);
             Contract.Requires<ArgumentNullException>(element != null);
 
@@ -102,15 +71,26 @@ namespace Avalonia.Controls
             else
             {
                 _inner.Add(name, element);
-                Registered?.Invoke(this, new NameScopeEventArgs(name, element));
+                if(_pendingSearches.TryGetValue(name, out var tcs))
+                    tcs.SetResult(element);
             }
         }
-        
-        /// <summary>
-        /// Finds a named element in the name scope.
-        /// </summary>
-        /// <param name="name">The name.</param>
-        /// <returns>The element, or null if the name was not found.</returns>
+
+        public ValueTask<object> FindAsync(string name)
+        {
+            var found = Find(name);
+            if (found != null)
+                return new ValueTask<object>(found);
+            if (IsCompleted)
+                return new ValueTask<object>((object)null);
+            if (!_pendingSearches.TryGetValue(name, out var tcs))
+                // We are intentionally running continuations synchronously here
+                _pendingSearches[name] = tcs = new TaskCompletionSource<object>();
+            
+            return new ValueTask<object>(tcs.Task);
+        }
+
+        /// <inheritdoc />
         public object Find(string name)
         {
             Contract.Requires<ArgumentNullException>(name != null);
@@ -120,21 +100,14 @@ namespace Avalonia.Controls
             return result;
         }
 
-        /// <summary>
-        /// Unregisters an element with the name scope.
-        /// </summary>
-        /// <param name="name">The name.</param>
-        public void Unregister(string name)
+        public void Complete()
         {
-            Contract.Requires<ArgumentNullException>(name != null);
-
-            object element;
-
-            if (_inner.TryGetValue(name, out element))
-            {
-                _inner.Remove(name);
-                Unregistered?.Invoke(this, new NameScopeEventArgs(name, element));
-            }
+            IsCompleted = true;
+            foreach (var kp in _pendingSearches)
+                kp.Value.TrySetResult(null);
+            _pendingSearches.Clear();
         }
+
+        
     }
 }

+ 30 - 46
src/Avalonia.Styling/Controls/NameScopeLocator.cs

@@ -1,6 +1,9 @@
 using System;
 using System.Linq;
+using System.Reactive.Disposables;
+using System.Reactive.Threading.Tasks;
 using System.Reflection;
+using System.Threading.Tasks;
 using Avalonia.LogicalTree;
 using Avalonia.Reactive;
 
@@ -17,63 +20,44 @@ namespace Avalonia.Controls
         /// <param name="name">The name of the control to find.</param>
         public static IObservable<object> Track(INameScope scope, string name)
         {
-            return new ScopeTracker(scope, name);
+            return new NeverEndingValueTaskObservable<object>(scope.FindAsync(name));
         }
         
-        private class ScopeTracker : LightweightObservableBase<object>
-        {
-            private readonly string _name;
-            INameScope _nameScope;
-            object _value;
-
-            public ScopeTracker(INameScope nameScope, string name)
-            {
-                _nameScope = nameScope;
-                _name = name;
-            }
-
-
-            protected override void Initialize()
-            {
-                _nameScope.Registered += Registered;
-                _nameScope.Unregistered += Unregistered;
-                _value = _nameScope.Find<ILogical>(_name);
-            }
-
-            protected override void Deinitialize()
-            {
-                if (_nameScope != null)
-                {
-                    _nameScope.Registered -= Registered;
-                    _nameScope.Unregistered -= Unregistered;
-                }
+        // This class is implemented in such weird way because for some reason
+        // our binding system doesn't expect OnCompleted to be ever called and
+        // seems to treat it as binding cancellation or something 
 
-                _value = null;
-            }
+        private class NeverEndingValueTaskObservable<T> : IObservable<T>
+        {
+            private T _value;
+            private Task<T> _task;
 
-            protected override void Subscribed(IObserver<object> observer, bool first)
+            public NeverEndingValueTaskObservable(ValueTask<T> task)
             {
-                observer.OnNext(_value);
+                if (task.IsCompleted)
+                    _value = task.Result;
+                else
+                    _task = task.AsTask();
             }
-
-            private void Registered(object sender, NameScopeEventArgs e)
+            
+            public IDisposable Subscribe(IObserver<T> observer)
             {
-                if (e.Name == _name)
+                if (_task?.IsCompleted == true)
                 {
-                    _value = e.Element;
-                    PublishNext(_value);
+                    _value = _task.Result;
+                    _task = null;
                 }
-            }
 
-            private void Unregistered(object sender, NameScopeEventArgs e)
-            {
-                if (e.Name == _name)
-                {
-                    _value = null;
-                    PublishNext(null);
-                }
+                if (_task != null)
+                    _task.ContinueWith(t =>
+                    {
+                        observer.OnNext(t.Result);
+                    }, TaskContinuationOptions.ExecuteSynchronously);
+                else
+                    observer.OnNext(_value);
+                
+                return Disposable.Empty;
             }
-
         }
     }
 }

+ 11 - 8
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs

@@ -53,17 +53,17 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
                     new[]
                     {
                         mnode.Manipulation,
-                        new AddNameScopeToRootObjectXamlIlNode(mnode, context.GetAvaloniaTypes())
+                        new HandleRootObjectScopeNode(mnode, context.GetAvaloniaTypes())
                     });
             }
             return node;
         }
 
-        class AddNameScopeToRootObjectXamlIlNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode
+        class HandleRootObjectScopeNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode
         {
             private readonly AvaloniaXamlIlWellKnownTypes _types;
 
-            public AddNameScopeToRootObjectXamlIlNode(IXamlIlLineInfo lineInfo,
+            public HandleRootObjectScopeNode(IXamlIlLineInfo lineInfo,
                 AvaloniaXamlIlWellKnownTypes types) : base(lineInfo)
             {
                 _types = types;
@@ -72,6 +72,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
             public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen)
             {
                 var next = codeGen.DefineLabel();
+                var scopeField = context.RuntimeContext.ContextType.Fields.First(f =>
+                    f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName);
                 using (var local = codeGen.LocalsPool.GetLocal(_types.StyledElement))
                 {
                     codeGen
@@ -81,11 +83,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
                         .Brfalse(next)
                         .Ldloc(local.Local)
                         .Ldloc(context.ContextLocal)
-                        .Ldfld(context.RuntimeContext.ContextType.Fields.First(f =>
-                            f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName))
-                        .EmitCall(_types.NameScopeSetNameScope)
-                        .MarkLabel(next);
-
+                        .Ldfld(scopeField)
+                        .EmitCall(_types.NameScopeSetNameScope, true)
+                        .MarkLabel(next)
+                        .Ldloc(context.ContextLocal)
+                        .Ldfld(scopeField)
+                        .EmitCall(_types.INameScopeComplete, true);
                 }
 
                 return XamlIlNodeEmitResult.Void(1);

+ 6 - 0
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@@ -25,6 +25,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
         public IXamlIlMethod NameScopeSetNameScope { get; }
         public IXamlIlType INameScope { get; }
         public IXamlIlMethod INameScopeRegister { get; }
+        public IXamlIlMethod INameScopeComplete { get; }
         
         public AvaloniaXamlIlWellKnownTypes(XamlIlAstTransformationContext ctx)
         {
@@ -52,6 +53,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
                 {
                     IsStatic = false, DeclaringOnly = true, IsExactMatch = true
                 });
+            INameScopeComplete = INameScope.GetMethod(
+                new FindMethodMethodSignature("Complete", XamlIlTypes.Void)
+                {
+                    IsStatic = false, DeclaringOnly = true, IsExactMatch = true
+                });
             NameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScope");
             NameScopeSetNameScope = NameScope.GetMethod(new FindMethodMethodSignature("SetNameScope",
                 XamlIlTypes.Void, StyledElement, INameScope) {IsStatic = true});

+ 4 - 2
src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs

@@ -21,8 +21,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
             var rootObject = provider.GetService<IRootObjectProvider>().RootObject;
             return sp =>
             {
-                var scope = new NameScope();
-                var obj =  builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope));
+                var parentScope = sp.GetService<INameScope>();
+                var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope();
+                var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope));
+                scope.Complete();
                 return new ControlTemplateResult((IControl)obj, scope);
             };
         }

+ 114 - 11
tests/Avalonia.Controls.UnitTests/NameScopeTests.cs

@@ -20,36 +20,139 @@ namespace Avalonia.Controls.UnitTests
         }
 
         [Fact]
-        public void Unregister_Unregisters_Element()
+        public void Cannot_Register_New_Element_With_Existing_Name()
+        {
+            var target = new NameScope();
+
+            target.Register("foo", new object());
+            Assert.Throws<ArgumentException>(() => target.Register("foo", new object()));
+        }
+
+        [Fact]
+        public void Can_Register_Same_Element_More_Than_Once()
         {
             var target = new NameScope();
             var element = new object();
 
             target.Register("foo", element);
-            target.Unregister("foo");
+            target.Register("foo", element);
 
-            Assert.Null(target.Find("foo"));
+            Assert.Same(element, target.Find("foo"));
         }
 
         [Fact]
-        public void Cannot_Register_New_Element_With_Existing_Name()
+        public void Cannot_Register_New_Element_For_Completed_Scope()
         {
             var target = new NameScope();
+            var element = new object();
 
-            target.Register("foo", new object());
-            Assert.Throws<ArgumentException>(() => target.Register("foo", new object()));
+            target.Register("foo", element);
+            target.Complete();
+            Assert.Throws<InvalidOperationException>(() => target.Register("bar", element));
         }
 
+        object _found = null;
+        async void FindAsync(INameScope scope, string name)
+        {
+            _found = await scope.FindAsync(name);
+        }
+        
         [Fact]
-        public void Can_Register_Same_Element_More_Than_Once()
+        public void FindAsync_Should_Find_Controls_Added_Earlier()
         {
-            var target = new NameScope();
+            var scope = new NameScope();
+            var element = new object();
+            scope.Register("foo", element);
+            FindAsync(scope, "foo");
+            Assert.Same(_found, element);
+        }
+        
+        [Fact]
+        public void FindAsync_Should_Find_Controls_Added_Later()
+        {
+            var scope = new NameScope();
             var element = new object();
+            
+            FindAsync(scope, "foo");
+            Assert.Null(_found);
+            scope.Register("foo", element);
+            Assert.Same(_found, element);
+        }
+        
+        [Fact]
+        public void FindAsync_Should_Return_Null_After_Scope_Completion()
+        {
+            var scope = new NameScope();
+            var element = new object();
+            bool finished = false;
+            async void Find(string name)
+            {
+                Assert.Null(await scope.FindAsync(name));
+                finished = true;
+            }
+            Find("foo");
+            Assert.False(finished);
+            scope.Register("bar", element);
+            Assert.False(finished);
+            scope.Complete();
+            Assert.True(finished);
+        }
 
-            target.Register("foo", element);
-            target.Register("foo", element);
+        [Fact]
+        public void Child_Scope_Should_Not_Find_Control_In_Parent_Scope_Unless_Completed()
+        {
+            var scope = new NameScope();
+            var childScope = new ChildNameScope(scope);
+            var element = new object();
+            scope.Register("foo", element);
+            Assert.Null(childScope.Find("foo"));
+            childScope.Complete();
+            Assert.Same(element, childScope.Find("foo"));
+        }
+        
+        [Fact]
+        public void Child_Scope_Should_Prefer_Own_Elements()
+        {
+            var scope = new NameScope();
+            var childScope = new ChildNameScope(scope);
+            var element = new object();
+            var childElement = new object();
+            scope.Register("foo", element);
+            childScope.Register("foo", childElement);
+            childScope.Complete();
+            Assert.Same(childElement, childScope.Find("foo"));
+        }
 
-            Assert.Same(element, target.Find("foo"));
+        [Fact]
+        public void Child_Scope_FindAsync_Should_Find_Elements_In_Parent_Scope_When_Child_Is_Completed()
+        {
+            var scope = new NameScope();
+            var childScope = new ChildNameScope(scope);
+            var element = new object();
+            scope.Register("foo", element);
+            FindAsync(childScope, "foo");
+            Assert.Null(_found);
+            childScope.Complete();
+            Assert.Same(element, _found);
+        }
+        
+        
+        [Fact]
+        public void Child_Scope_FindAsync_Should_Prefer_Own_Elements()
+        {
+            var scope = new NameScope();
+            var childScope = new ChildNameScope(scope);
+            var element = new object();
+            var childElement = new object();
+            FindAsync(childScope, "foo");
+            scope.Register("foo", element);
+            Assert.Null(_found);
+            childScope.Register("foo", childElement);
+            Assert.Same(childElement, childScope.Find("foo"));
+            childScope.Complete();
+            FindAsync(childScope, "foo");
+            Assert.Same(childElement, childScope.Find("foo"));
         }
+
     }
 }

+ 1 - 1
tests/Avalonia.LeakTests/ControlTests.cs

@@ -85,7 +85,7 @@ namespace Avalonia.LeakTests
 
                     // Clear the content and ensure the Canvas is removed.
                     window.Content = null;
-                    scope.Unregister("foo");
+
                     window.LayoutManager.ExecuteLayoutPass();
                     Assert.Null(window.Presenter.Child);
 

+ 0 - 221
tests/Avalonia.Styling.UnitTests/ControlLocatorTests.cs

@@ -1,221 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections.Generic;
-using System.Reactive.Linq;
-using System.Threading.Tasks;
-using Avalonia.Controls;
-using Avalonia.LogicalTree;
-using Avalonia.UnitTests;
-using Xunit;
-
-namespace Avalonia.Styling.UnitTests
-{
-    public class ControlLocatorTests
-    {
-        [Fact]
-        public async Task Track_By_Name_Should_Find_Control_Added_Earlier()
-        {
-            TextBlock target;
-            TextBlock relativeTo;
-
-            var root = new TestRoot
-            {
-                Child = new StackPanel
-                {
-                    Children =
-                    {
-                        (target = new TextBlock { Name = "target" }),
-                        (relativeTo = new TextBlock { Name = "start" }),
-                    }
-                }
-            };
-            var scope = Register(root, relativeTo);
-            Register(root, target);
-            
-            var locator = NameScopeLocator.Track(scope, "target");
-            var result = await locator.Take(1);
-
-            Assert.Same(target, result);
-            Assert.Equal(0, scope.RegisteredSubscribers);
-            Assert.Equal(0, scope.UnregisteredSubscribers);
-        }
-
-
-        
-        [Fact]
-        public void Track_By_Name_Should_Find_Control_Added_Later()
-        {
-            StackPanel panel;
-            TextBlock relativeTo;
-
-            var root = new TestRoot
-            {
-                Child = (panel = new StackPanel
-                {
-                    Children =
-                    {
-                        (relativeTo = new TextBlock
-                        {
-                            Name = "start"
-                        }),
-                    }
-                })
-            };
-            var scope = Register(root, relativeTo);
-
-            var locator = NameScopeLocator.Track(scope, "target");
-            var target = new TextBlock { Name = "target" };
-            var result = new List<ILogical>();
-
-            using (locator.Subscribe(x => result.Add((ILogical)x)))
-            {
-                panel.Children.Add(target);
-                Register(root, target);
-            }
-
-            Assert.Equal(new[] { null, target }, result);
-            Assert.Equal(0, scope.RegisteredSubscribers);
-            Assert.Equal(0, scope.UnregisteredSubscribers);
-        }
-
-        [Fact]
-        public void Track_By_Name_Should_Track_Removal_And_Readd()
-        {
-            StackPanel panel;
-            TextBlock target;
-            TextBlock relativeTo;
-
-            var root = new TestRoot
-            {
-                Child = panel = new StackPanel
-                {
-                    Children =
-                    {
-                        (target = new TextBlock { Name = "target" }),
-                        (relativeTo = new TextBlock { Name = "start" }),
-                    }
-                }
-            };
-            var scope = Register(root, target);
-            Register(root, relativeTo);
-            
-            var locator = NameScopeLocator.Track(scope, "target");
-            var result = new List<ILogical>();
-            locator.Subscribe(x => result.Add((IControl)x));
-
-            var other = new TextBlock { Name = "target" };
-            panel.Children.Remove(target);
-            scope.Unregister(target.Name);
-            panel.Children.Add(other);
-            Register(root, other);
-
-            Assert.Equal(new[] { target, null, other }, result);
-        }
-
-        [Fact(Skip = "I'm going to remove that logic anyway")]
-        public void Track_By_Name_Should_Find_Control_When_Tree_Changed()
-        {
-            TextBlock target1;
-            TextBlock target2;
-            TextBlock relativeTo;
-
-            var root1 = new TestRoot
-            {
-                Child = new StackPanel
-                {
-                    Children =
-                    {
-                        (relativeTo = new TextBlock
-                        {
-                            Name = "start"
-                        }),
-                        (target1 = new TextBlock { Name = "target" }),
-                    }
-                }
-            };
-            var scope1 = Register(root1, relativeTo);
-            Register(root1, relativeTo);
-            Register(root1, target1);
-
-            var root2 = new TestRoot
-            {
-                Child = new StackPanel
-                {
-                    Children =
-                    {
-                        (target2 = new TextBlock { Name = "target" }),
-                    }
-                }
-            };
-            var scope2 = Register(root2, target2);
-
-            var locator = NameScopeLocator.Track(scope1, "target");
-            var result = new List<ILogical>();
-
-            using (locator.Subscribe(x => result.Add((ILogical)x)))
-            {
-                ((StackPanel)root1.Child).Children.Remove(relativeTo);
-                scope1.Unregister(relativeTo.Name);
-                ((StackPanel)root2.Child).Children.Add(relativeTo);
-                Register(root2, relativeTo);
-            }
-
-            Assert.Equal(new[] { target1, null, target2 }, result);
-            Assert.Equal(0, scope1.RegisteredSubscribers);
-            Assert.Equal(0, scope1.UnregisteredSubscribers);
-            Assert.Equal(0, scope2.RegisteredSubscribers);
-            Assert.Equal(0, scope2.UnregisteredSubscribers);
-        }
-
-        TrackingNameScope Register(StyledElement anchor, StyledElement element)
-        {
-            var scope = (TrackingNameScope)NameScope.GetNameScope(anchor);
-            if (scope == null)
-                NameScope.SetNameScope(anchor, scope = new TrackingNameScope());
-            scope.Register(element.Name, element);
-            return scope;
-        }
-        
-        class TrackingNameScope : INameScope
-        {
-            public int RegisteredSubscribers { get; private set; }
-            public int UnregisteredSubscribers { get; private set; }
-            private NameScope _inner = new NameScope();
-            public event EventHandler<NameScopeEventArgs> Registered
-            {
-                add
-                {
-                    _inner.Registered += value;
-                    RegisteredSubscribers++;
-                }
-                remove
-                {
-                    _inner.Registered -= value;
-                    RegisteredSubscribers--;
-                }
-            }
-
-            public event EventHandler<NameScopeEventArgs> Unregistered
-            {
-                add
-                {
-                    _inner.Unregistered += value;
-                    UnregisteredSubscribers++;
-                }
-                remove
-                {
-                    _inner.Unregistered -= value;
-                    UnregisteredSubscribers--;
-                }
-            }
-
-            public void Register(string name, object element) => _inner.Register(name, element);
-
-            public object Find(string name) => _inner.Find(name);
-
-            public void Unregister(string name) => _inner.Unregister(name);
-        }
-    }
-}