Browse Source

Added ControlLocator.

To track named controls relative to another control.
Steven Kirk 10 years ago
parent
commit
554383b61d

+ 60 - 0
src/Markup/Perspex.Markup/Data/ControlLocator.cs

@@ -0,0 +1,60 @@
+// Copyright (c) The Perspex 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.Linq;
+using System.Reactive.Linq;
+using Perspex.Controls;
+
+namespace Perspex.Markup.Data
+{
+    /// <summary>
+    /// Locates controls relative to other controls.
+    /// </summary>
+    public static class ControlLocator
+    {
+        /// <summary>
+        /// Tracks a named control relative to another control.
+        /// </summary>
+        /// <param name="relativeTo">
+        /// The control relative from which the other control should be found.
+        /// </param>
+        /// <param name="name">The name of the control to find.</param>
+        public static IObservable<IControl> Track(IControl relativeTo, string name)
+        {
+            var attached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
+                x => relativeTo.AttachedToVisualTree += x,
+                x => relativeTo.DetachedFromVisualTree += x)
+                .Select(x => x.EventArgs.NameScope)
+                .StartWith(relativeTo.FindNameScope());
+
+            var detached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
+                x => relativeTo.DetachedFromVisualTree += x,
+                x => relativeTo.DetachedFromVisualTree += x)
+                .Select(x => (INameScope)null);
+
+            return attached.Merge(detached).Select(nameScope =>
+            {
+                if (nameScope != null)
+                {
+                    var registered = Observable.FromEventPattern<NameScopeEventArgs>(
+                        x => nameScope.Registered += x,
+                        x => nameScope.Registered -= x)
+                        .Where(x => x.EventArgs.Name == name)
+                        .Select(x => x.EventArgs.Element)
+                        .OfType<IControl>();
+                    var unregistered = Observable.FromEventPattern<NameScopeEventArgs>(
+                        x => nameScope.Unregistered += x,
+                        x => nameScope.Unregistered -= x);
+                    return registered
+                        .StartWith(nameScope.Find<IControl>(name))
+                        .TakeUntil(unregistered);
+                }
+                else
+                {
+                    return Observable.Return<IControl>(null);
+                }
+            }).Switch();
+        }
+    }
+}

+ 21 - 0
src/Markup/Perspex.Markup/Perspex.Markup.csproj

@@ -42,6 +42,7 @@
     <Compile Include="Data\ExpressionNodeBuilder.cs" />
     <Compile Include="Data\ExpressionParseException.cs" />
     <Compile Include="Data\ExpressionSubject.cs" />
+    <Compile Include="Data\ControlLocator.cs" />
     <Compile Include="Data\Plugins\PerspexPropertyAccessorPlugin.cs" />
     <Compile Include="Data\Plugins\InpcPropertyAccessorPlugin.cs" />
     <Compile Include="Data\Plugins\IPropertyAccessor.cs" />
@@ -91,10 +92,30 @@
       <Project>{b09b78d8-9b26-48b0-9149-d64a2f120f3f}</Project>
       <Name>Perspex.Base</Name>
     </ProjectReference>
+    <ProjectReference Include="..\..\Perspex.Controls\Perspex.Controls.csproj">
+      <Project>{d2221c82-4a25-4583-9b43-d791e3f6820c}</Project>
+      <Name>Perspex.Controls</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Perspex.Input\Perspex.Input.csproj">
+      <Project>{62024b2d-53eb-4638-b26b-85eeaa54866e}</Project>
+      <Name>Perspex.Input</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Perspex.Interactivity\Perspex.Interactivity.csproj">
+      <Project>{6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b}</Project>
+      <Name>Perspex.Interactivity</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Perspex.Layout\Perspex.Layout.csproj">
+      <Project>{42472427-4774-4c81-8aff-9f27b8e31721}</Project>
+      <Name>Perspex.Layout</Name>
+    </ProjectReference>
     <ProjectReference Include="..\..\Perspex.SceneGraph\Perspex.SceneGraph.csproj">
       <Project>{eb582467-6abb-43a1-b052-e981ba910e3a}</Project>
       <Name>Perspex.SceneGraph</Name>
     </ProjectReference>
+    <ProjectReference Include="..\..\Perspex.Styling\Perspex.Styling.csproj">
+      <Project>{f1baa01a-f176-4c6a-b39d-5b40bb1b148f}</Project>
+      <Name>Perspex.Styling</Name>
+    </ProjectReference>
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 15 - 5
src/Perspex.Controls/ControlExtensions.cs

@@ -40,7 +40,7 @@ namespace Perspex.Controls
         }
 
         /// <summary>
-        /// Finds the named control in the specified control.
+        /// Finds the named control in the scope of the specified control.
         /// </summary>
         /// <typeparam name="T">The type of the control to find.</typeparam>
         /// <param name="control">The control to look in.</param>
@@ -48,10 +48,7 @@ namespace Perspex.Controls
         /// <returns>The control or null if not found.</returns>
         public static T FindControl<T>(this IControl control, string name) where T : class, IControl
         {
-            var nameScope = control.GetSelfAndLogicalAncestors()
-                .OfType<Visual>()
-                .Select(x => (x as INameScope) ?? NameScope.GetNameScope(x))
-                .FirstOrDefault(x => x != null);
+            var nameScope = control.FindNameScope();
 
             if (nameScope == null)
             {
@@ -60,5 +57,18 @@ namespace Perspex.Controls
 
             return nameScope.Find<T>(name);
         }
+
+        /// <summary>
+        /// Finds the name scope for a control.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <returns>The control's name scope, or null if not found.</returns>
+        public static INameScope FindNameScope(this IControl control)
+        {
+            return control.GetSelfAndLogicalAncestors()
+                .OfType<Visual>()
+                .Select(x => (x as INameScope) ?? NameScope.GetNameScope(x))
+                .FirstOrDefault(x => x != null);
+        }
     }
 }

+ 123 - 0
tests/Perspex.Markup.UnitTests/Data/ControlLocatorTests.cs

@@ -0,0 +1,123 @@
+// Copyright (c) The Perspex 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 Perspex.Controls;
+using Perspex.Markup.Data;
+using Xunit;
+
+namespace Perspex.Markup.UnitTests.Data
+{
+    public class ControlLocatorTests
+    {
+        [Fact]
+        public async void Track_By_Name_Should_Find_Control_Added_Earlier()
+        {
+            TextBlock target;
+            TextBlock relativeTo;
+
+            var root = new TestRoot
+            {
+                Child = new StackPanel
+                {
+                    Children = new Controls.Controls
+                    {
+                        (target = new TextBlock { Name = "target" }),
+                        (relativeTo = new TextBlock { Name = "start" }),
+                    }
+                }
+            };
+            
+            var locator = ControlLocator.Track(relativeTo, "target");
+            var result = await locator.Take(1);
+
+            Assert.Same(target, result);
+            Assert.Equal(0, root.NameScopeRegisteredSubscribers);
+            Assert.Equal(0, root.NameScopeUnregisteredSubscribers);
+        }
+
+        [Fact]
+        public void Track_By_Name_Should_Find_Control_Added_Later()
+        {
+            StackPanel panel;
+            TextBlock relativeTo;
+
+            var root = new TestRoot
+            {
+                Child = (panel = new StackPanel
+                {
+                    Children = new Controls.Controls
+                    {
+                        (relativeTo = new TextBlock
+                        {
+                            Name = "start"
+                        }),
+                    }
+                })
+            };
+
+            var locator = ControlLocator.Track(relativeTo, "target");
+            var target = new TextBlock { Name = "target" };
+            var result = new List<IControl>();
+
+            using (locator.Subscribe(x => result.Add(x)))
+            {
+                panel.Children.Add(target);
+            }
+
+            Assert.Equal(new[] { null, target }, result);
+            Assert.Equal(0, root.NameScopeRegisteredSubscribers);
+            Assert.Equal(0, root.NameScopeUnregisteredSubscribers);
+        }
+
+        [Fact]
+        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 = new Controls.Controls
+                    {
+                        (relativeTo = new TextBlock
+                        {
+                            Name = "start"
+                        }),
+                        (target1 = new TextBlock { Name = "target" }),
+                    }
+                }
+            };
+
+            var root2 = new TestRoot
+            {
+                Child = new StackPanel
+                {
+                    Children = new Controls.Controls
+                    {
+                        (target2 = new TextBlock { Name = "target" }),
+                    }
+                }
+            };
+
+            var locator = ControlLocator.Track(relativeTo, "target");
+            var target = new TextBlock { Name = "target" };
+            var result = new List<IControl>();
+
+            using (locator.Subscribe(x => result.Add(x)))
+            {
+                ((StackPanel)root1.Child).Children.Remove(relativeTo);
+                ((StackPanel)root2.Child).Children.Add(relativeTo);
+            }
+
+            Assert.Equal(new[] { target1, null, target2 }, result);
+            Assert.Equal(0, root1.NameScopeRegisteredSubscribers);
+            Assert.Equal(0, root1.NameScopeUnregisteredSubscribers);
+        }
+    }
+}

+ 35 - 0
tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

@@ -34,6 +34,11 @@
     <WarningLevel>4</WarningLevel>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="Microsoft.Reactive.Testing, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\..\packages\Rx-Testing.2.2.5\lib\net45\Microsoft.Reactive.Testing.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
     <Reference Include="System" />
     <Reference Include="System.Core" />
     <Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
@@ -72,6 +77,7 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Data\ControlLocatorTests.cs" />
     <Compile Include="Data\ExpressionNodeBuilderTests.cs" />
     <Compile Include="Data\ExpressionNodeBuilderTests_Errors.cs" />
     <Compile Include="Data\ExpressionObserverTests_Indexer.cs" />
@@ -85,6 +91,7 @@
     <Compile Include="Data\NotifyingBase.cs" />
     <Compile Include="DefaultValueConverterTests.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="TestRoot.cs" />
     <Compile Include="UnitTestSynchronizationContext.cs" />
   </ItemGroup>
   <ItemGroup>
@@ -95,10 +102,38 @@
       <Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
       <Name>Perspex.Markup</Name>
     </ProjectReference>
+    <ProjectReference Include="..\..\src\Perspex.Animation\Perspex.Animation.csproj">
+      <Project>{d211e587-d8bc-45b9-95a4-f297c8fa5200}</Project>
+      <Name>Perspex.Animation</Name>
+    </ProjectReference>
     <ProjectReference Include="..\..\src\Perspex.Base\Perspex.Base.csproj">
       <Project>{b09b78d8-9b26-48b0-9149-d64a2f120f3f}</Project>
       <Name>Perspex.Base</Name>
     </ProjectReference>
+    <ProjectReference Include="..\..\src\Perspex.Controls\Perspex.Controls.csproj">
+      <Project>{d2221c82-4a25-4583-9b43-d791e3f6820c}</Project>
+      <Name>Perspex.Controls</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\src\Perspex.Input\Perspex.Input.csproj">
+      <Project>{62024b2d-53eb-4638-b26b-85eeaa54866e}</Project>
+      <Name>Perspex.Input</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\src\Perspex.Interactivity\Perspex.Interactivity.csproj">
+      <Project>{6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b}</Project>
+      <Name>Perspex.Interactivity</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\src\Perspex.Layout\Perspex.Layout.csproj">
+      <Project>{42472427-4774-4c81-8aff-9f27b8e31721}</Project>
+      <Name>Perspex.Layout</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\src\Perspex.SceneGraph\Perspex.SceneGraph.csproj">
+      <Project>{eb582467-6abb-43a1-b052-e981ba910e3a}</Project>
+      <Name>Perspex.SceneGraph</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\src\Perspex.Styling\Perspex.Styling.csproj">
+      <Project>{f1baa01a-f176-4c6a-b39d-5b40bb1b148f}</Project>
+      <Name>Perspex.Styling</Name>
+    </ProjectReference>
   </ItemGroup>
   <ItemGroup>
     <Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />

+ 61 - 0
tests/Perspex.Markup.UnitTests/TestRoot.cs

@@ -0,0 +1,61 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Perspex.Controls;
+using Perspex.Platform;
+using Perspex.Rendering;
+
+namespace Perspex.Markup.UnitTests
+{
+    public class TestRoot : Decorator, IRenderRoot, INameScope
+    {
+        private NameScope _nameScope = new NameScope();
+
+        event EventHandler<NameScopeEventArgs> INameScope.Registered
+        {
+            add { _nameScope.Registered += value; ++NameScopeRegisteredSubscribers; }
+            remove { _nameScope.Registered -= value; --NameScopeRegisteredSubscribers; }
+        }
+
+        public event EventHandler<NameScopeEventArgs> Unregistered
+        {
+            add { _nameScope.Unregistered += value; ++NameScopeUnregisteredSubscribers; }
+            remove { _nameScope.Unregistered -= value; --NameScopeUnregisteredSubscribers; }
+        }
+
+        public int NameScopeRegisteredSubscribers { get; private set; }
+
+        public int NameScopeUnregisteredSubscribers { get; private set; }
+
+        public IRenderTarget RenderTarget
+        {
+            get { throw new NotImplementedException(); }
+        }
+
+        public IRenderQueueManager RenderQueueManager
+        {
+            get { throw new NotImplementedException(); }
+        }
+
+        public Point TranslatePointToScreen(Point p)
+        {
+            throw new NotImplementedException();
+        }
+
+        public void Register(string name, object element)
+        {
+            _nameScope.Register(name, element);
+        }
+
+        public object Find(string name)
+        {
+            return _nameScope.Find(name);
+        }
+
+        public void Unregister(string name)
+        {
+            _nameScope.Unregister(name);
+        }
+    }
+}