From 16c2242757037578b6863db557e219e6d8f6d273 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 4 Nov 2015 20:05:45 +0100 Subject: [PATCH 01/18] Implemented NameScopes Some tests still failing as controls are only registered with name scope when attached to a rooted visual tree. Need to work around this by explicitly registering controls when applying template. --- src/Perspex.Controls/Button.cs | 12 +- src/Perspex.Controls/Carousel.cs | 6 - src/Perspex.Controls/ContentControl.cs | 4 +- src/Perspex.Controls/Control.cs | 36 +--- src/Perspex.Controls/DropDown.cs | 4 +- src/Perspex.Controls/GridSplitter.cs | 4 +- src/Perspex.Controls/ItemsControl.cs | 4 +- src/Perspex.Controls/Menu.cs | 22 +-- src/Perspex.Controls/MenuItem.cs | 6 +- src/Perspex.Controls/Primitives/AccessText.cs | 20 +-- src/Perspex.Controls/Primitives/Popup.cs | 20 +-- src/Perspex.Controls/Primitives/PopupRoot.cs | 4 +- .../Primitives/TemplatedControl.cs | 7 +- src/Perspex.Controls/ProgressBar.cs | 4 +- .../Templates/TemplateExtensions.cs | 21 --- src/Perspex.Controls/TextBox.cs | 4 +- src/Perspex.Controls/TopLevel.cs | 4 +- src/Perspex.Controls/TreeViewItem.cs | 4 +- src/Perspex.Input/InputElement.cs | 8 +- src/Perspex.SceneGraph/INameScope.cs | 31 ++++ .../Styling => Perspex.SceneGraph}/INamed.cs | 8 +- src/Perspex.SceneGraph/NameScope.cs | 80 +++++++++ src/Perspex.SceneGraph/NameScopeExtensions.cs | 68 +++++++ .../Perspex.SceneGraph.csproj | 5 + src/Perspex.SceneGraph/Visual.cs | 169 ++++++++++++++---- .../VisualTreeAttachmentEventArgs.cs | 36 ++++ src/Perspex.Styling/Perspex.Styling.csproj | 1 - .../Primitives/PopupTests.cs | 2 +- .../Primitives/ScrollBarTests.cs | 7 +- .../Primitives/TemplatedControlTests.cs | 23 +++ .../ScrollViewerTests.cs | 4 +- .../TestTemplatedControl.cs | 2 +- .../Perspex.SceneGraph.UnitTests/TestRoot.cs | 19 +- .../TestVisual.cs | 39 ++-- .../VisualTests.cs | 87 +++++++++ 35 files changed, 581 insertions(+), 194 deletions(-) create mode 100644 src/Perspex.SceneGraph/INameScope.cs rename src/{Perspex.Styling/Styling => Perspex.SceneGraph}/INamed.cs (58%) create mode 100644 src/Perspex.SceneGraph/NameScope.cs create mode 100644 src/Perspex.SceneGraph/NameScopeExtensions.cs create mode 100644 src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs diff --git a/src/Perspex.Controls/Button.cs b/src/Perspex.Controls/Button.cs index 29eb4c3628..a1ca0e11a6 100644 --- a/src/Perspex.Controls/Button.cs +++ b/src/Perspex.Controls/Button.cs @@ -147,13 +147,13 @@ namespace Perspex.Controls } /// - protected override void OnAttachedToVisualTree(IRenderRoot root) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnAttachedToVisualTree(root); + base.OnAttachedToVisualTree(e); if (IsDefault) { - var inputElement = root as IInputElement; + var inputElement = e.Root as IInputElement; if (inputElement != null) { @@ -198,13 +198,13 @@ namespace Perspex.Controls } /// - protected override void OnDetachedFromVisualTree(IRenderRoot oldRoot) + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnDetachedFromVisualTree(oldRoot); + base.OnDetachedFromVisualTree(e); if (IsDefault) { - var inputElement = oldRoot as IInputElement; + var inputElement = e.Root as IInputElement; if (inputElement != null) { diff --git a/src/Perspex.Controls/Carousel.cs b/src/Perspex.Controls/Carousel.cs index 1c12b634d0..97745e1452 100644 --- a/src/Perspex.Controls/Carousel.cs +++ b/src/Perspex.Controls/Carousel.cs @@ -55,11 +55,5 @@ namespace Perspex.Controls { // Ignore pointer presses. } - - /// - protected override void OnTemplateApplied() - { - base.OnTemplateApplied(); - } } } diff --git a/src/Perspex.Controls/ContentControl.cs b/src/Perspex.Controls/ContentControl.cs index 1640853391..c4c1c94e31 100644 --- a/src/Perspex.Controls/ContentControl.cs +++ b/src/Perspex.Controls/ContentControl.cs @@ -104,12 +104,12 @@ namespace Perspex.Controls } /// - protected override void OnTemplateApplied() + protected override void OnTemplateApplied(INameScope nameScope) { // We allow ContentControls without ContentPresenters in the template. This can be // useful for e.g. a simple ToggleButton that displays an image. There's no need to // have a ContentPresenter in the visual tree for that. - Presenter = this.FindTemplateChild("PART_ContentPresenter"); + Presenter = nameScope.Find("PART_ContentPresenter"); } } } diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs index c4d85e7908..b382e82675 100644 --- a/src/Perspex.Controls/Control.cs +++ b/src/Perspex.Controls/Control.cs @@ -19,7 +19,6 @@ namespace Perspex.Controls /// /// The control class extends and adds the following features: /// - /// - A . /// - An inherited . /// - A property to allow user-defined data to be attached to the control. /// - A collection of class strings for custom styling. @@ -71,7 +70,6 @@ namespace Perspex.Controls private readonly Classes _classes = new Classes(); private DataTemplates _dataTemplates; private IControl _focusAdorner; - private string _name; private IPerspexList _logicalChildren; private Styles _styles; @@ -165,36 +163,6 @@ namespace Perspex.Controls } } - /// - /// Gets or sets the name of the control. - /// - /// - /// A control's name is used to uniquely identify a control within the control's name - /// scope. Once a control is added to a visual tree, its name cannot be changed. - /// - public string Name - { - get - { - return _name; - } - - set - { - if (_name != null) - { - throw new InvalidOperationException("Name already set."); - } - - if (((IVisual)this).VisualParent != null) - { - throw new InvalidOperationException("Cannot set Name : control already added to tree."); - } - - _name = value; - } - } - /// /// Gets or sets the styles for the control. /// @@ -404,9 +372,9 @@ namespace Perspex.Controls } /// - protected override void OnAttachedToVisualTree(IRenderRoot root) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnAttachedToVisualTree(root); + base.OnAttachedToVisualTree(e); IStyler styler = PerspexLocator.Current.GetService(); diff --git a/src/Perspex.Controls/DropDown.cs b/src/Perspex.Controls/DropDown.cs index 4af34d689d..31a44ad7d0 100644 --- a/src/Perspex.Controls/DropDown.cs +++ b/src/Perspex.Controls/DropDown.cs @@ -125,14 +125,14 @@ namespace Perspex.Controls base.OnPointerPressed(e); } - protected override void OnTemplateApplied() + protected override void OnTemplateApplied(INameScope nameScope) { if (_popup != null) { _popup.Opened -= PopupOpened; } - _popup = this.GetTemplateChild("PART_Popup"); + _popup = nameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; } diff --git a/src/Perspex.Controls/GridSplitter.cs b/src/Perspex.Controls/GridSplitter.cs index 1fc78549eb..9a0ef0b988 100644 --- a/src/Perspex.Controls/GridSplitter.cs +++ b/src/Perspex.Controls/GridSplitter.cs @@ -34,9 +34,9 @@ namespace Perspex.Controls } } - protected override void OnAttachedToVisualTree(IRenderRoot root) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnAttachedToVisualTree(root); + base.OnAttachedToVisualTree(e); _grid = this.GetVisualParent(); } } diff --git a/src/Perspex.Controls/ItemsControl.cs b/src/Perspex.Controls/ItemsControl.cs index 9da2ae45ba..5c06dbc04e 100644 --- a/src/Perspex.Controls/ItemsControl.cs +++ b/src/Perspex.Controls/ItemsControl.cs @@ -147,9 +147,9 @@ namespace Perspex.Controls } /// - protected override void OnTemplateApplied() + protected override void OnTemplateApplied(INameScope nameScope) { - Presenter = this.FindTemplateChild("PART_ItemsPresenter"); + Presenter = nameScope.Find("PART_ItemsPresenter"); } /// diff --git a/src/Perspex.Controls/Menu.cs b/src/Perspex.Controls/Menu.cs index 3513843c37..9e9ee451e8 100644 --- a/src/Perspex.Controls/Menu.cs +++ b/src/Perspex.Controls/Menu.cs @@ -96,15 +96,12 @@ namespace Perspex.Controls IsOpen = true; } - /// - /// Called when the is attached to the visual tree. - /// - /// The root of the visual tree. - protected override void OnAttachedToVisualTree(IRenderRoot root) + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnAttachedToVisualTree(root); + base.OnAttachedToVisualTree(e); - var topLevel = root as TopLevel; + var topLevel = e.Root as TopLevel; topLevel.Deactivated += Deactivated; @@ -117,7 +114,7 @@ namespace Perspex.Controls pointerPress, Disposable.Create(() => topLevel.Deactivated -= Deactivated)); - var inputRoot = root as IInputRoot; + var inputRoot = e.Root as IInputRoot; if (inputRoot != null && inputRoot.AccessKeyHandler != null) { @@ -125,13 +122,10 @@ namespace Perspex.Controls } } - /// - /// Called when the is detached from the visual tree. - /// - /// The root of the visual tree being detached from. - protected override void OnDetachedFromVisualTree(IRenderRoot oldRoot) + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnDetachedFromVisualTree(oldRoot); + base.OnDetachedFromVisualTree(e); _subscription.Dispose(); } diff --git a/src/Perspex.Controls/MenuItem.cs b/src/Perspex.Controls/MenuItem.cs index dde7dc16b3..4d73240604 100644 --- a/src/Perspex.Controls/MenuItem.cs +++ b/src/Perspex.Controls/MenuItem.cs @@ -376,11 +376,11 @@ namespace Perspex.Controls /// /// Called when the MenuItem's template has been applied. /// - protected override void OnTemplateApplied() + protected override void OnTemplateApplied(INameScope nameScope) { - base.OnTemplateApplied(); + base.OnTemplateApplied(nameScope); - _popup = this.GetTemplateChild("PART_Popup"); + _popup = nameScope.Get("PART_Popup"); _popup.DependencyResolver = DependencyResolver.Instance; _popup.PopupRootCreated += PopupRootCreated; _popup.Opened += PopupOpened; diff --git a/src/Perspex.Controls/Primitives/AccessText.cs b/src/Perspex.Controls/Primitives/AccessText.cs index 0168974248..9ad2640534 100644 --- a/src/Perspex.Controls/Primitives/AccessText.cs +++ b/src/Perspex.Controls/Primitives/AccessText.cs @@ -108,14 +108,11 @@ namespace Perspex.Controls.Primitives return result.WithHeight(result.Height + 1); } - /// - /// Called when the control is attached to a visual tree. - /// - /// The root of the visual tree. - protected override void OnAttachedToVisualTree(IRenderRoot root) + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnAttachedToVisualTree(root); - _accessKeys = (root as IInputRoot)?.AccessKeyHandler; + base.OnAttachedToVisualTree(e); + _accessKeys = (e.Root as IInputRoot)?.AccessKeyHandler; if (_accessKeys != null && AccessKey != 0) { @@ -123,13 +120,10 @@ namespace Perspex.Controls.Primitives } } - /// - /// Called when the control is detached from a visual tree. - /// - /// The root of the visual tree. - protected override void OnDetachedFromVisualTree(IRenderRoot root) + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnDetachedFromVisualTree(root); + base.OnDetachedFromVisualTree(e); if (_accessKeys != null && AccessKey != 0) { diff --git a/src/Perspex.Controls/Primitives/Popup.cs b/src/Perspex.Controls/Primitives/Popup.cs index 6396b29843..138c5b586f 100644 --- a/src/Perspex.Controls/Primitives/Popup.cs +++ b/src/Perspex.Controls/Primitives/Popup.cs @@ -212,23 +212,17 @@ namespace Perspex.Controls.Primitives return new Size(); } - /// - /// Called when the control is added to the visual tree. - /// - /// THe root of the visual tree. - protected override void OnAttachedToVisualTree(IRenderRoot root) + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnAttachedToVisualTree(root); - _topLevel = root as TopLevel; + base.OnAttachedToVisualTree(e); + _topLevel = e.Root as TopLevel; } - /// - /// Called when the control is removed to the visual tree. - /// - /// THe root of the visual tree. - protected override void OnDetachedFromVisualTree(IRenderRoot root) + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnDetachedFromVisualTree(root); + base.OnDetachedFromVisualTree(e); _topLevel = null; } diff --git a/src/Perspex.Controls/Primitives/PopupRoot.cs b/src/Perspex.Controls/Primitives/PopupRoot.cs index 4e628f79f0..aeaa63dd0f 100644 --- a/src/Perspex.Controls/Primitives/PopupRoot.cs +++ b/src/Perspex.Controls/Primitives/PopupRoot.cs @@ -93,9 +93,9 @@ namespace Perspex.Controls.Primitives } /// - protected override void OnTemplateApplied() + protected override void OnTemplateApplied(INameScope nameScope) { - base.OnTemplateApplied(); + base.OnTemplateApplied(nameScope); if (Parent.TemplatedParent != null) { diff --git a/src/Perspex.Controls/Primitives/TemplatedControl.cs b/src/Perspex.Controls/Primitives/TemplatedControl.cs index bbe9836e03..868da9b670 100644 --- a/src/Perspex.Controls/Primitives/TemplatedControl.cs +++ b/src/Perspex.Controls/Primitives/TemplatedControl.cs @@ -195,6 +195,8 @@ namespace Perspex.Controls.Primitives _templateLog.Verbose("Creating control template"); var child = Template.Build(this); + var nameScope = new NameScope(); + NameScope.SetNameScope((Visual)child, nameScope); // We need to call SetTemplatedParentAndApplyChildTemplates twice - once // before the controls are added to the visual tree so that the logical @@ -207,7 +209,7 @@ namespace Perspex.Controls.Primitives AddVisualChild((Visual)child); SetTemplatedParentAndApplyChildTemplates(child); - OnTemplateApplied(); + OnTemplateApplied(nameScope); } _templateApplied = true; @@ -217,7 +219,8 @@ namespace Perspex.Controls.Primitives /// /// Called when the control's template is applied. /// - protected virtual void OnTemplateApplied() + /// The template name scope. + protected virtual void OnTemplateApplied(INameScope nameScope) { } diff --git a/src/Perspex.Controls/ProgressBar.cs b/src/Perspex.Controls/ProgressBar.cs index f9a8d867d0..4bc1fdb9d2 100644 --- a/src/Perspex.Controls/ProgressBar.cs +++ b/src/Perspex.Controls/ProgressBar.cs @@ -26,9 +26,9 @@ namespace Perspex.Controls } /// - protected override void OnTemplateApplied() + protected override void OnTemplateApplied(INameScope nameScope) { - _indicator = this.GetTemplateChild("PART_Indicator"); + _indicator = nameScope.Get("PART_Indicator"); UpdateIndicator(Bounds.Size); } diff --git a/src/Perspex.Controls/Templates/TemplateExtensions.cs b/src/Perspex.Controls/Templates/TemplateExtensions.cs index e4e61c722f..b8cd9e25c2 100644 --- a/src/Perspex.Controls/Templates/TemplateExtensions.cs +++ b/src/Perspex.Controls/Templates/TemplateExtensions.cs @@ -41,27 +41,6 @@ namespace Perspex.Controls.Templates return null; } - public static T FindTemplateChild(this ITemplatedControl control, string name) where T : INamed - { - return control.GetTemplateChildren().OfType().SingleOrDefault(x => x.Name == name); - } - - public static T GetTemplateChild(this ITemplatedControl control, string name) where T : INamed - { - var result = control.FindTemplateChild(name); - - if (result == null) - { - throw new InvalidOperationException(string.Format( - "Could not find template child '{0}' of type '{1}' in template for '{2}'.", - name, - typeof(T).FullName, - control.GetType().FullName)); - } - - return result; - } - public static IEnumerable GetTemplateChildren(this ITemplatedControl control) { var visual = control as IVisual; diff --git a/src/Perspex.Controls/TextBox.cs b/src/Perspex.Controls/TextBox.cs index affa38c023..36fd8c0cb9 100644 --- a/src/Perspex.Controls/TextBox.cs +++ b/src/Perspex.Controls/TextBox.cs @@ -136,9 +136,9 @@ namespace Perspex.Controls set { SetValue(TextWrappingProperty, value); } } - protected override void OnTemplateApplied() + protected override void OnTemplateApplied(INameScope nameScope) { - _presenter = this.GetTemplateChild("PART_TextPresenter"); + _presenter = nameScope.Get("PART_TextPresenter"); _presenter.Cursor = new Cursor(StandardCursorType.Ibeam); } diff --git a/src/Perspex.Controls/TopLevel.cs b/src/Perspex.Controls/TopLevel.cs index 21247721bd..6aacac85ff 100644 --- a/src/Perspex.Controls/TopLevel.cs +++ b/src/Perspex.Controls/TopLevel.cs @@ -287,9 +287,9 @@ namespace Perspex.Controls } /// - protected override void OnAttachedToVisualTree(IRenderRoot root) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnAttachedToVisualTree(root); + base.OnAttachedToVisualTree(e); throw new InvalidOperationException( $"Control '{GetType().Name}' is a top level control and cannot be added as a child."); diff --git a/src/Perspex.Controls/TreeViewItem.cs b/src/Perspex.Controls/TreeViewItem.cs index bd499e4f10..da263ca951 100644 --- a/src/Perspex.Controls/TreeViewItem.cs +++ b/src/Perspex.Controls/TreeViewItem.cs @@ -83,9 +83,9 @@ namespace Perspex.Controls } /// - protected override void OnAttachedToVisualTree(IRenderRoot root) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnAttachedToVisualTree(root); + base.OnAttachedToVisualTree(e); _treeView = this.GetVisualAncestors().OfType().FirstOrDefault(); } diff --git a/src/Perspex.Input/InputElement.cs b/src/Perspex.Input/InputElement.cs index 7e0907c9e8..5be37be76a 100644 --- a/src/Perspex.Input/InputElement.cs +++ b/src/Perspex.Input/InputElement.cs @@ -348,9 +348,9 @@ namespace Perspex.Input } /// - protected override void OnDetachedFromVisualTree(IRenderRoot oldRoot) + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnDetachedFromVisualTree(oldRoot); + base.OnDetachedFromVisualTree(e); if (IsFocused) { @@ -359,9 +359,9 @@ namespace Perspex.Input } /// - protected override void OnAttachedToVisualTree(IRenderRoot root) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnAttachedToVisualTree(root); + base.OnAttachedToVisualTree(e); UpdateIsEnabledCore(); } diff --git a/src/Perspex.SceneGraph/INameScope.cs b/src/Perspex.SceneGraph/INameScope.cs new file mode 100644 index 0000000000..13b197f1ad --- /dev/null +++ b/src/Perspex.SceneGraph/INameScope.cs @@ -0,0 +1,31 @@ +// 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. + +namespace Perspex +{ + /// + /// Defines a name scope. + /// + public interface INameScope + { + /// + /// Registers an element eith the name scope. + /// + /// The element name. + /// The element. + void Register(string name, object element); + + /// + /// Finds a named element in the name scope. + /// + /// The name. + /// The element, or null if the name was not found. + object Find(string name); + + /// + /// Unregisters an element with the name scope. + /// + /// The name. + void Unregister(string name); + } +} diff --git a/src/Perspex.Styling/Styling/INamed.cs b/src/Perspex.SceneGraph/INamed.cs similarity index 58% rename from src/Perspex.Styling/Styling/INamed.cs rename to src/Perspex.SceneGraph/INamed.cs index bd5b806d7e..5ceda5a265 100644 --- a/src/Perspex.Styling/Styling/INamed.cs +++ b/src/Perspex.SceneGraph/INamed.cs @@ -1,10 +1,16 @@ // 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. -namespace Perspex.Styling +namespace Perspex { + /// + /// Interface for named elements. + /// public interface INamed { + /// + /// Gets the element name. + /// string Name { get; } } } diff --git a/src/Perspex.SceneGraph/NameScope.cs b/src/Perspex.SceneGraph/NameScope.cs new file mode 100644 index 0000000000..403a51bf1f --- /dev/null +++ b/src/Perspex.SceneGraph/NameScope.cs @@ -0,0 +1,80 @@ +// 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; + +namespace Perspex +{ + /// + /// Implements a name scope. + /// + public class NameScope : INameScope + { + /// + /// Defines the NameScope attached property. + /// + public static readonly PerspexProperty NameScopeProperty = + PerspexProperty.RegisterAttached("NameScope"); + + private Dictionary _inner = new Dictionary(); + + /// + /// Gets the value of the attached on a visual. + /// + /// The visual. + /// The value of the NameScope attached property. + public static INameScope GetNameScope(Visual visual) + { + return visual.GetValue(NameScopeProperty); + } + + /// + /// Sets the value of the attached on a visual. + /// + /// The visual. + /// The value to set. + public static void SetNameScope(Visual visual, INameScope value) + { + visual.SetValue(NameScopeProperty, value); + } + + /// + /// Registers an element with the name scope. + /// + /// The element name. + /// The element. + public void Register(string name, object element) + { + Contract.Requires(name != null); + Contract.Requires(element != null); + + _inner.Add(name, element); + } + + /// + /// Finds a named element in the name scope. + /// + /// The name. + /// The element, or null if the name was not found. + public object Find(string name) + { + Contract.Requires(name != null); + + object result; + _inner.TryGetValue(name, out result); + return result; + } + + /// + /// Unregisters an element with the name scope. + /// + /// The name. + public void Unregister(string name) + { + Contract.Requires(name != null); + + _inner.Remove(name); + } + } +} diff --git a/src/Perspex.SceneGraph/NameScopeExtensions.cs b/src/Perspex.SceneGraph/NameScopeExtensions.cs new file mode 100644 index 0000000000..3e80f8e838 --- /dev/null +++ b/src/Perspex.SceneGraph/NameScopeExtensions.cs @@ -0,0 +1,68 @@ +// 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; + +namespace Perspex +{ + /// + /// Extension methods for . + /// + public static class NameScopeExtensions + { + /// + /// Finds a named element in an . + /// + /// The element type. + /// The name scope. + /// The name. + /// The named element or null if not found. + public static T Find(this INameScope nameScope, string name) + where T : class + { + Contract.Requires(nameScope != null); + Contract.Requires(name != null); + + var result = nameScope.Find(name); + + if (result != null && !(result is T)) + { + throw new InvalidOperationException( + $"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'."); + } + + return (T)result; + } + + /// + /// Gets a named element from an or throws if no element of the + /// requested name was found. + /// + /// The element type. + /// The name scope. + /// The name. + /// The named element. + public static T Get(this INameScope nameScope, string name) + where T : class + { + Contract.Requires(nameScope != null); + Contract.Requires(name != null); + + var result = nameScope.Find(name); + + if (result == null) + { + throw new KeyNotFoundException($"Could not find control '{name}'."); + } + + if (!(result is T)) + { + throw new InvalidOperationException( + $"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'."); + } + + return (T)result; + } + } +} diff --git a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj index d2d897142f..871e14750f 100644 --- a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj +++ b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj @@ -55,6 +55,8 @@ + + @@ -101,6 +103,8 @@ + + @@ -122,6 +126,7 @@ + diff --git a/src/Perspex.SceneGraph/Visual.cs b/src/Perspex.SceneGraph/Visual.cs index a63b8b0aad..53c6b8e66d 100644 --- a/src/Perspex.SceneGraph/Visual.cs +++ b/src/Perspex.SceneGraph/Visual.cs @@ -26,7 +26,7 @@ namespace Perspex /// To traverse the scene graph (aka Visual Tree), use the extension methods defined /// in . /// - public class Visual : Animatable, IVisual + public class Visual : Animatable, IVisual, INamed { /// /// Defines the property. @@ -76,6 +76,11 @@ namespace Perspex public static readonly PerspexProperty ZIndexProperty = PerspexProperty.Register(nameof(ZIndex)); + /// + /// The name of the visual, if any. + /// + private string _name; + /// /// Holds the children of the visual. /// @@ -128,6 +133,16 @@ namespace Perspex _visualChildren.CollectionChanged += VisualChildrenChanged; } + /// + /// Raised when the control is attached to a rooted visual tree. + /// + public event EventHandler AttachedToVisualTree; + + /// + /// Raised when the control is detached from a rooted visual tree. + /// + public event EventHandler DetachedFromVisualTree; + /// /// Gets the bounds of the scene graph node relative to its parent. /// @@ -163,6 +178,36 @@ namespace Perspex set { SetValue(IsVisibleProperty, value); } } + /// + /// Gets or sets the name of the visual. + /// + /// + /// An element's name is used to uniquely identify a control within the control's name + /// scope. Once the element is added to a visual tree, its name cannot be changed. + /// + public string Name + { + get + { + return _name; + } + + set + { + if (value.Trim() == string.Empty) + { + throw new InvalidOperationException("Cannot set Name to empty string."); + } + + if (_isAttachedToVisualTree) + { + throw new InvalidOperationException("Cannot set Name : control already added to tree."); + } + + _name = value; + } + } + /// /// Gets the opacity of the scene graph node. /// @@ -340,17 +385,19 @@ namespace Perspex /// /// Called when the control is added to a visual tree. /// - /// The root of the visual tree. - protected virtual void OnAttachedToVisualTree(IRenderRoot root) + /// The event args. + protected virtual void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { + AttachedToVisualTree?.Invoke(this, e); } /// /// Called when the control is removed from a visual tree. /// - /// The root of the visual tree. - protected virtual void OnDetachedFromVisualTree(IRenderRoot root) + /// The event args. + protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { + DetachedFromVisualTree?.Invoke(this, e); } /// @@ -367,6 +414,60 @@ namespace Perspex } } + /// + /// Gets the event args for an or + /// event. + /// + /// + /// A if the visual currently has a root; + /// otherwise null. + /// + private VisualTreeAttachmentEventArgs GetAttachmentEventArgs() + { + var e = (IVisual)this; + IRenderRoot root = null; + INameScope nameScope = null; + + while (e != null) + { + if (nameScope == null) + { + nameScope = e as INameScope ?? NameScope.GetNameScope((Visual)e); + } + + root = e as IRenderRoot; + + if (root != null) + { + return new VisualTreeAttachmentEventArgs(root, nameScope); + } + + e = e.VisualParent; + } + + return null; + } + + /// + /// Gets the for this element based on the + /// parent's args. + /// + /// The parent args. + /// The args for this element. + private VisualTreeAttachmentEventArgs GetAttachmentEventArgs(VisualTreeAttachmentEventArgs e) + { + var childNameScope = (this as INameScope) ?? NameScope.GetNameScope(this); + + if (childNameScope != null) + { + return new VisualTreeAttachmentEventArgs(e.Root, childNameScope); + } + else + { + return e; + } + } + /// /// Gets the root of the controls visual tree and the distance from the root. /// @@ -436,28 +537,23 @@ namespace Perspex { if (_visualParent != value) { - var old = _visualParent; - var oldRoot = this.GetVisualAncestors().OfType().FirstOrDefault(); - var newRoot = default(IRenderRoot); - - if (value != null) - { - newRoot = value.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - } + var oldArgs = GetAttachmentEventArgs(); _visualParent = value; - if (oldRoot != null) + if (oldArgs != null) { - NotifyDetachedFromVisualTree(oldRoot); + NotifyDetachedFromVisualTree(oldArgs); } - if (newRoot != null) + var newArgs = GetAttachmentEventArgs(); + + if (newArgs != null) { - NotifyAttachedToVisualTree(newRoot); + NotifyAttachedToVisualTree(newArgs); } - RaisePropertyChanged(VisualParentProperty, old, value, BindingPriority.LocalValue); + RaisePropertyChanged(VisualParentProperty, oldArgs, value, BindingPriority.LocalValue); } } @@ -491,43 +587,56 @@ namespace Perspex } /// - /// Calls the method for this control - /// and all of its visual descendents. + /// Calls the method + /// for this control and all of its visual descendents. /// - /// The root of the visual tree. - private void NotifyAttachedToVisualTree(IRenderRoot root) + /// The event args. + private void NotifyAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { _visualLogger.Verbose("Attached to visual tree"); _isAttachedToVisualTree = true; - OnAttachedToVisualTree(root); + + if (Name != null && e.NameScope != null) + { + e.NameScope.Register(Name, this); + } + + OnAttachedToVisualTree(e); if (_visualChildren != null) { foreach (Visual child in _visualChildren.OfType()) { - child.NotifyAttachedToVisualTree(root); + var ce = child.GetAttachmentEventArgs(e); + child.NotifyAttachedToVisualTree(ce); } } } /// - /// Calls the method for this control - /// and all of its visual descendents. + /// Calls the method + /// for this control and all of its visual descendents. /// - /// The root of the visual tree. - private void NotifyDetachedFromVisualTree(IRenderRoot root) + /// The event args. + private void NotifyDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { _visualLogger.Verbose("Detached from visual tree"); + if (Name != null && e.NameScope != null) + { + e.NameScope.Unregister(Name); + } + _isAttachedToVisualTree = false; - OnDetachedFromVisualTree(root); + OnDetachedFromVisualTree(e); if (_visualChildren != null) { foreach (Visual child in _visualChildren.OfType()) { - child.NotifyDetachedFromVisualTree(root); + var ce = child.GetAttachmentEventArgs(e); + child.NotifyDetachedFromVisualTree(ce); } } } diff --git a/src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs b/src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs new file mode 100644 index 0000000000..41f37e26fc --- /dev/null +++ b/src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs @@ -0,0 +1,36 @@ +// 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.Rendering; + +namespace Perspex +{ + /// + /// Holds the event arguments for the and + /// events. + /// + public class VisualTreeAttachmentEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The root visual. + /// The name scope. + public VisualTreeAttachmentEventArgs(IRenderRoot root, INameScope nameScope) + { + Root = root; + NameScope = nameScope; + } + + /// + /// Gets the root of the visual tree that the visual is being attached to or detached from. + /// + public IRenderRoot Root { get; } + + /// + /// Gets the element's name scope. + /// + public INameScope NameScope { get; } + } +} diff --git a/src/Perspex.Styling/Perspex.Styling.csproj b/src/Perspex.Styling/Perspex.Styling.csproj index c0776e27c2..a8e997821e 100644 --- a/src/Perspex.Styling/Perspex.Styling.csproj +++ b/src/Perspex.Styling/Perspex.Styling.csproj @@ -46,7 +46,6 @@ - diff --git a/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs index 28da8d75e3..18d7867b0f 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs @@ -214,7 +214,7 @@ namespace Perspex.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - var popup = target.GetTemplateChild("popup"); + var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup"); popup.Open(); var popupRoot = popup.PopupRoot; diff --git a/tests/Perspex.Controls.UnitTests/Primitives/ScrollBarTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/ScrollBarTests.cs index 6ba3adc899..eb19ac1493 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/ScrollBarTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/ScrollBarTests.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.Linq; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.Media; @@ -20,7 +21,7 @@ namespace Perspex.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - var track = target.GetTemplateChild("track"); + var track = (Track)target.GetTemplateChildren().First(x => x.Name == "track"); target.Value = 50; Assert.Equal(track.Value, 50); @@ -35,7 +36,7 @@ namespace Perspex.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - var track = target.GetTemplateChild("track"); + var track = (Track)target.GetTemplateChildren().First(x => x.Name == "track"); track.Value = 50; Assert.Equal(target.Value, 50); @@ -51,7 +52,7 @@ namespace Perspex.Controls.UnitTests.Primitives target.ApplyTemplate(); - var track = target.GetTemplateChild("track"); + var track = (Track)target.GetTemplateChildren().First(x => x.Name == "track"); target.Value = 25; track.Value = 50; diff --git a/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs index 0138fe99fa..1d39e8d443 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -50,6 +50,29 @@ namespace Perspex.Controls.UnitTests.Primitives Assert.Empty(target.GetLogicalChildren()); } + [Fact] + public void Templated_Child_Should_Be_NameScope() + { + var target = new TemplatedControl + { + Template = new FuncControlTemplate(_ => new Decorator + { + Child = new Panel + { + Children = new Controls + { + new TextBlock(), + new Border(), + } + } + }), + }; + + target.ApplyTemplate(); + + Assert.NotNull(NameScope.GetNameScope((Visual)target.GetVisualChildren().Single())); + } + [Fact] public void Templated_Children_Should_Have_TemplatedParent_Set() { diff --git a/tests/Perspex.Controls.UnitTests/ScrollViewerTests.cs b/tests/Perspex.Controls.UnitTests/ScrollViewerTests.cs index 596acd53d8..7e54fb0e84 100644 --- a/tests/Perspex.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Perspex.Controls.UnitTests/ScrollViewerTests.cs @@ -24,9 +24,7 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); - var presenter = target.GetTemplateChild("contentPresenter"); - - Assert.IsType(presenter.Child); + Assert.IsType(target.Presenter.Child); } [Fact] diff --git a/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs b/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs index 9456caa557..8cbf1dca3f 100644 --- a/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs +++ b/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs @@ -14,7 +14,7 @@ namespace Perspex.Controls.UnitTests base.AddVisualChild(visual); } - protected override void OnTemplateApplied() + protected override void OnTemplateApplied(INameScope nameScope) { OnTemplateAppliedCalled = true; } diff --git a/tests/Perspex.SceneGraph.UnitTests/TestRoot.cs b/tests/Perspex.SceneGraph.UnitTests/TestRoot.cs index 99a1fd0d4a..a1802b2cef 100644 --- a/tests/Perspex.SceneGraph.UnitTests/TestRoot.cs +++ b/tests/Perspex.SceneGraph.UnitTests/TestRoot.cs @@ -7,8 +7,10 @@ using Perspex.Rendering; namespace Perspex.SceneGraph.UnitTests { - public class TestRoot : TestVisual, IRenderRoot + public class TestRoot : TestVisual, IRenderRoot, INameScope { + private NameScope nameScope = new NameScope(); + public IRenderTarget RenderTarget { get { throw new NotImplementedException(); } @@ -23,5 +25,20 @@ namespace Perspex.SceneGraph.UnitTests { 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); + } } } diff --git a/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs b/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs index cb3bc4328d..88f47d7876 100644 --- a/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs +++ b/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs @@ -20,11 +20,28 @@ namespace Perspex.SceneGraph.UnitTests public class TestVisual : Visual { - public event EventHandler> AttachedToVisualTreeCalled; + public new PerspexObject InheritanceParent => base.InheritanceParent; - public event EventHandler> DetachedFromVisualTreeCalled; + public IVisual Child + { + get + { + return ((IVisual)this).VisualChildren.FirstOrDefault(); + } - public new PerspexObject InheritanceParent => base.InheritanceParent; + set + { + if (Child != null) + { + RemoveVisualChild(Child); + } + + if (value != null) + { + AddVisualChild(value); + } + } + } public void AddChild(Visual v) { @@ -45,21 +62,5 @@ namespace Perspex.SceneGraph.UnitTests { ClearVisualChildren(); } - - protected override void OnAttachedToVisualTree(IRenderRoot root) - { - if (AttachedToVisualTreeCalled != null) - { - AttachedToVisualTreeCalled(this, new ParamEventArgs(root)); - } - } - - protected override void OnDetachedFromVisualTree(IRenderRoot oldRoot) - { - if (DetachedFromVisualTreeCalled != null) - { - DetachedFromVisualTreeCalled(this, new ParamEventArgs(oldRoot)); - } - } } } diff --git a/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs b/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs index 31acf6f9dc..7756130c20 100644 --- a/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs +++ b/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Perspex.Controls; using Perspex.VisualTree; using Xunit; @@ -84,5 +85,91 @@ namespace Perspex.SceneGraph.UnitTests Assert.Equal(new Visual[] { null, null }, result); } + + [Fact] + public void Adding_Children_Should_Fire_OnAttachedToVisualTree() + { + var child2 = new TestVisual(); + var child1 = new TestVisual { Child = child2 }; + var root = new TestRoot(); + var called1 = false; + var called2 = false; + + child1.AttachedToVisualTree += (s, e) => called1 = true; + child2.AttachedToVisualTree += (s, e) => called2 = true; + + root.Child = child1; + + Assert.True(called1); + Assert.True(called2); + } + + [Fact] + public void Removing_Children_Should_Fire_OnDetachedFromVisualTree() + { + var child2 = new TestVisual(); + var child1 = new TestVisual { Child = child2 }; + var root = new TestRoot(); + var called1 = false; + var called2 = false; + + root.Child = child1; + child1.DetachedFromVisualTree += (s, e) => called1 = true; + child2.DetachedFromVisualTree += (s, e) => called2 = true; + root.Child = null; + + Assert.True(called1); + Assert.True(called2); + } + + [Fact] + public void Controls_Should_Add_Themselves_To_Root_NameScope() + { + var child2 = new TestVisual { Name = "bar" }; + var child1 = new TestVisual { Name = "foo", Child = child2 }; + var root = new TestRoot { Child = child1 }; + + Assert.Same(child1, root.Find("foo")); + Assert.Same(child2, root.Find("bar")); + } + + [Fact] + public void Controls_Should_Add_Themselves_To_NameScopes_In_Attached_Property() + { + var child2 = new TestVisual { Name = "bar", [NameScope.NameScopeProperty] = new NameScope() }; + var child1 = new TestVisual { Name = "foo", Child = child2}; + var root = new TestRoot { Child = child1 }; + + Assert.Same(child1, root.Find("foo")); + Assert.Null(root.Find("bar")); + Assert.Same(child2, NameScope.GetNameScope(child2).Find("bar")); + } + + [Fact] + public void Controls_Should_Remove_Themselves_From_Root_NameScope() + { + var child2 = new TestVisual { Name = "bar" }; + var child1 = new TestVisual { Name = "foo", Child = child2 }; + var root = new TestRoot { Child = child1 }; + + root.Child = null; + + Assert.Null(root.Find("foo")); + Assert.Null(root.Find("bar")); + } + + [Fact] + public void Controls_Should_Remove_Themselves_From_NameScopes_In_Attached_Property() + { + var child2 = new TestVisual { Name = "bar" }; + var child1 = new TestVisual { Name = "foo", Child = child2,[NameScope.NameScopeProperty] = new NameScope() }; + var root = new TestRoot { Child = child1 }; + + root.Child = null; + + Assert.Null(root.Find("foo")); + Assert.Null(root.Find("bar")); + Assert.Null(NameScope.GetNameScope(child1).Find("bar")); + } } } From 3307ef8712c808f794549549a3c3fd79dab35143 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 5 Nov 2015 18:42:23 +0100 Subject: [PATCH 02/18] Register templated controls with name scope. Usually controls are only registered with their name scope when they get added to a rooted visual tree, but this causes problems for tests, and would mean the templated control wouldn't work if the client called ApplyTemplate themselves before the control is added to the visual tree. Work around this by explicitly registering template controls with the name scope when the template is applied. To allow this, we need to allow controls to be registered with a name scope multiple times. --- .../Primitives/TemplatedControl.cs | 19 +++++-- src/Perspex.SceneGraph/NameScope.cs | 14 ++++- src/Perspex.Themes.Default/ScrollViewer.paml | 3 +- .../ScrollViewerTests.cs | 2 +- .../TabControlTests.cs | 32 +++++------ .../NameScopeTests.cs | 55 +++++++++++++++++++ .../Perspex.SceneGraph.UnitTests.csproj | 1 + 7 files changed, 102 insertions(+), 24 deletions(-) create mode 100644 tests/Perspex.SceneGraph.UnitTests/NameScopeTests.cs diff --git a/src/Perspex.Controls/Primitives/TemplatedControl.cs b/src/Perspex.Controls/Primitives/TemplatedControl.cs index 868da9b670..170bfa6fa8 100644 --- a/src/Perspex.Controls/Primitives/TemplatedControl.cs +++ b/src/Perspex.Controls/Primitives/TemplatedControl.cs @@ -202,12 +202,12 @@ namespace Perspex.Controls.Primitives // before the controls are added to the visual tree so that the logical // tree can be set up before styling is applied. ((ISetLogicalParent)child).SetParent(this); - SetTemplatedParentAndApplyChildTemplates(child); + SetupTemplateControls(child, nameScope); // And again after the controls are added to the visual tree, and have their // styling and thus Template property set. AddVisualChild((Visual)child); - SetTemplatedParentAndApplyChildTemplates(child); + SetupTemplateControls(child, nameScope); OnTemplateApplied(nameScope); } @@ -226,14 +226,23 @@ namespace Perspex.Controls.Primitives /// /// Sets the TemplatedParent property for a control created from the control template and - /// applies the templates of nested templated controls. + /// applies the templates of nested templated controls. Also adds each control to its name + /// scope if it has a name. /// /// The control. - private void SetTemplatedParentAndApplyChildTemplates(IControl control) + /// The name scope. + private void SetupTemplateControls(IControl control, INameScope nameScope) { + // If control.TemplatedParent is null at this point, then the control is our templated + // child so set its TemplatedParent and register it with its name scope. if (control.TemplatedParent == null) { control.SetValue(TemplatedParentProperty, this); + + if (control.Name != null) + { + nameScope.Register(control.Name, control); + } } control.ApplyTemplate(); @@ -242,7 +251,7 @@ namespace Perspex.Controls.Primitives { foreach (IControl child in control.GetVisualChildren()) { - SetTemplatedParentAndApplyChildTemplates(child); + SetupTemplateControls(child, nameScope); } } } diff --git a/src/Perspex.SceneGraph/NameScope.cs b/src/Perspex.SceneGraph/NameScope.cs index 403a51bf1f..24d3fc5949 100644 --- a/src/Perspex.SceneGraph/NameScope.cs +++ b/src/Perspex.SceneGraph/NameScope.cs @@ -49,7 +49,19 @@ namespace Perspex Contract.Requires(name != null); Contract.Requires(element != null); - _inner.Add(name, element); + object existing; + + if (_inner.TryGetValue(name, out existing)) + { + if (existing != element) + { + throw new ArgumentException($"Control with the name '{name}' already registered."); + } + } + else + { + _inner.Add(name, element); + } } /// diff --git a/src/Perspex.Themes.Default/ScrollViewer.paml b/src/Perspex.Themes.Default/ScrollViewer.paml index c12ba0fa30..a459b11536 100644 --- a/src/Perspex.Themes.Default/ScrollViewer.paml +++ b/src/Perspex.Themes.Default/ScrollViewer.paml @@ -2,7 +2,8 @@ - () + { + new TabItem { - new TabItem - { - Name = "first", - Content = "foo", - }, - new TabItem - { - Name = "second", - Content = "bar", - }, - new TabItem - { - Name = "3rd", - Content = "barf", - }, - }; + Name = "first", + Content = "foo", + }, + new TabItem + { + Name = "second", + Content = "bar", + }, + new TabItem + { + Name = "3rd", + Content = "barf", + }, + }; var target = new TabControl { diff --git a/tests/Perspex.SceneGraph.UnitTests/NameScopeTests.cs b/tests/Perspex.SceneGraph.UnitTests/NameScopeTests.cs new file mode 100644 index 0000000000..a774e6884d --- /dev/null +++ b/tests/Perspex.SceneGraph.UnitTests/NameScopeTests.cs @@ -0,0 +1,55 @@ +// 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 Xunit; + +namespace Perspex.SceneGraph.UnitTests +{ + public class NameScopeTests + { + [Fact] + public void Register_Registers_Element() + { + var target = new NameScope(); + var element = new object(); + + target.Register("foo", element); + + Assert.Same(element, target.Find("foo")); + } + + [Fact] + public void Unregister_Unregisters_Element() + { + var target = new NameScope(); + var element = new object(); + + target.Register("foo", element); + target.Unregister("foo"); + + Assert.Null(target.Find("foo")); + } + + [Fact] + public void Cannot_Register_New_Element_With_Existing_Name() + { + var target = new NameScope(); + + target.Register("foo", new object()); + Assert.Throws(() => 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.Register("foo", element); + + Assert.Same(element, target.Find("foo")); + } + } +} diff --git a/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj b/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj index e48d6ac73b..5be971e814 100644 --- a/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj +++ b/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj @@ -72,6 +72,7 @@ + From 035f070953c08c430a3a826a5ee7c1a88daa8fff Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 5 Nov 2015 21:47:19 +0100 Subject: [PATCH 03/18] Make Window a name scope. --- src/Perspex.Controls/Window.cs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Perspex.Controls/Window.cs b/src/Perspex.Controls/Window.cs index 5b01d6c63b..12060bbab8 100644 --- a/src/Perspex.Controls/Window.cs +++ b/src/Perspex.Controls/Window.cs @@ -40,7 +40,7 @@ namespace Perspex.Controls /// /// A top-level window. /// - public class Window : TopLevel, IStyleable, IFocusScope + public class Window : TopLevel, IStyleable, IFocusScope, INameScope { /// /// Defines the property. @@ -54,8 +54,8 @@ namespace Perspex.Controls public static readonly PerspexProperty TitleProperty = PerspexProperty.Register(nameof(Title), "Window"); + private NameScope _nameScope = new NameScope(); private object _dialogResult; - private Size _maxPlatformClientSize; /// @@ -190,6 +190,24 @@ namespace Perspex.Controls } } + /// + void INameScope.Register(string name, object element) + { + _nameScope.Register(name, element); + } + + /// + object INameScope.Find(string name) + { + return _nameScope.Find(name); + } + + /// + void INameScope.Unregister(string name) + { + _nameScope.Unregister(name); + } + /// protected override Size MeasureOverride(Size availableSize) { From 77ed9e8dec69ec3088ca066a3733135d6854bf1f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 5 Nov 2015 21:47:28 +0100 Subject: [PATCH 04/18] Search nearest name scope to find a control. --- src/Perspex.Controls/ControlExtensions.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Perspex.Controls/ControlExtensions.cs b/src/Perspex.Controls/ControlExtensions.cs index 0c3831289f..81f3eed8f1 100644 --- a/src/Perspex.Controls/ControlExtensions.cs +++ b/src/Perspex.Controls/ControlExtensions.cs @@ -1,6 +1,7 @@ // 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 Perspex.LogicalTree; using Perspex.Styling; @@ -42,14 +43,22 @@ namespace Perspex.Controls /// Finds the named control in the specified control. /// /// The type of the control to find. - /// The control. + /// The control to look in. /// The name of the control to find. /// The control or null if not found. - public static T FindControl(this IControl control, string name) where T : IControl + public static T FindControl(this IControl control, string name) where T : class, IControl { - return control.GetLogicalDescendents() - .OfType() - .FirstOrDefault(x => x.Name == name); + var nameScope = control.GetSelfAndLogicalAncestors() + .OfType() + .Select(x => (x as INameScope) ?? NameScope.GetNameScope(x)) + .FirstOrDefault(x => x != null); + + if (nameScope == null) + { + throw new InvalidOperationException("Could not find parent name scope."); + } + + return nameScope.Find(name); } } } From ad9a2324d03850e211780d7970b4b950f29b9189 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 6 Nov 2015 19:57:31 +0100 Subject: [PATCH 05/18] Added namescope events. --- src/Perspex.Controls/Window.cs | 14 +++++++++++++ src/Perspex.SceneGraph/INameScope.cs | 12 +++++++++++ src/Perspex.SceneGraph/NameScope.cs | 19 +++++++++++++++++- src/Perspex.SceneGraph/NameScopeEventArgs.cs | 19 ++++++++++++++++++ .../Perspex.SceneGraph.csproj | 1 + .../Perspex.SceneGraph.UnitTests/TestRoot.cs | 20 +++++++++++++++---- 6 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 src/Perspex.SceneGraph/NameScopeEventArgs.cs diff --git a/src/Perspex.Controls/Window.cs b/src/Perspex.Controls/Window.cs index 12060bbab8..6d9a8c96b6 100644 --- a/src/Perspex.Controls/Window.cs +++ b/src/Perspex.Controls/Window.cs @@ -76,6 +76,20 @@ namespace Perspex.Controls _maxPlatformClientSize = this.PlatformImpl.MaxClientSize; } + /// + event EventHandler INameScope.Registered + { + add { _nameScope.Registered += value; } + remove { _nameScope.Registered -= value; } + } + + /// + event EventHandler INameScope.Unregistered + { + add { _nameScope.Unregistered += value; } + remove { _nameScope.Unregistered -= value; } + } + /// /// Gets the platform-specific window implementation. /// diff --git a/src/Perspex.SceneGraph/INameScope.cs b/src/Perspex.SceneGraph/INameScope.cs index 13b197f1ad..6e54c90afd 100644 --- a/src/Perspex.SceneGraph/INameScope.cs +++ b/src/Perspex.SceneGraph/INameScope.cs @@ -1,6 +1,8 @@ // 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; + namespace Perspex { /// @@ -8,6 +10,16 @@ namespace Perspex /// public interface INameScope { + /// + /// Raised when an element is registered with the name scope. + /// + event EventHandler Registered; + + /// + /// Raised when an element is unregistered with the name scope. + /// + event EventHandler Unregistered; + /// /// Registers an element eith the name scope. /// diff --git a/src/Perspex.SceneGraph/NameScope.cs b/src/Perspex.SceneGraph/NameScope.cs index 24d3fc5949..7334b9ddb0 100644 --- a/src/Perspex.SceneGraph/NameScope.cs +++ b/src/Perspex.SceneGraph/NameScope.cs @@ -19,6 +19,16 @@ namespace Perspex private Dictionary _inner = new Dictionary(); + /// + /// Raised when an element is registered with the name scope. + /// + public event EventHandler Registered; + + /// + /// Raised when an element is unregistered with the name scope. + /// + public event EventHandler Unregistered; + /// /// Gets the value of the attached on a visual. /// @@ -61,6 +71,7 @@ namespace Perspex else { _inner.Add(name, element); + Registered?.Invoke(this, new NameScopeEventArgs(name, element)); } } @@ -86,7 +97,13 @@ namespace Perspex { Contract.Requires(name != null); - _inner.Remove(name); + object element; + + if (_inner.TryGetValue(name, out element)) + { + _inner.Remove(name); + Registered?.Invoke(this, new NameScopeEventArgs(name, element)); + } } } } diff --git a/src/Perspex.SceneGraph/NameScopeEventArgs.cs b/src/Perspex.SceneGraph/NameScopeEventArgs.cs new file mode 100644 index 0000000000..4d19082a24 --- /dev/null +++ b/src/Perspex.SceneGraph/NameScopeEventArgs.cs @@ -0,0 +1,19 @@ +// 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; + +namespace Perspex +{ + public class NameScopeEventArgs : EventArgs + { + public NameScopeEventArgs(string name, object element) + { + Name = name; + Element = element; + } + + public string Name { get; } + public object Element { get; } + } +} diff --git a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj index 871e14750f..2121be7bfc 100644 --- a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj +++ b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj @@ -103,6 +103,7 @@ + diff --git a/tests/Perspex.SceneGraph.UnitTests/TestRoot.cs b/tests/Perspex.SceneGraph.UnitTests/TestRoot.cs index a1802b2cef..62bc782b78 100644 --- a/tests/Perspex.SceneGraph.UnitTests/TestRoot.cs +++ b/tests/Perspex.SceneGraph.UnitTests/TestRoot.cs @@ -9,7 +9,19 @@ namespace Perspex.SceneGraph.UnitTests { public class TestRoot : TestVisual, IRenderRoot, INameScope { - private NameScope nameScope = new NameScope(); + private NameScope _nameScope = new NameScope(); + + event EventHandler INameScope.Registered + { + add { _nameScope.Registered += value; } + remove { _nameScope.Registered -= value; } + } + + public event EventHandler Unregistered + { + add { _nameScope.Unregistered += value; } + remove { _nameScope.Unregistered -= value; } + } public IRenderTarget RenderTarget { @@ -28,17 +40,17 @@ namespace Perspex.SceneGraph.UnitTests public void Register(string name, object element) { - nameScope.Register(name, element); + _nameScope.Register(name, element); } public object Find(string name) { - return nameScope.Find(name); + return _nameScope.Find(name); } public void Unregister(string name) { - nameScope.Unregister(name); + _nameScope.Unregister(name); } } } From 2cb7bc8356aa45050d42b7848adfeadf19209d22 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 6 Nov 2015 19:58:00 +0100 Subject: [PATCH 06/18] Added attached/detached events to IVisual. --- src/Perspex.SceneGraph/IVisual.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Perspex.SceneGraph/IVisual.cs b/src/Perspex.SceneGraph/IVisual.cs index 0491d63fad..3acb3ac27c 100644 --- a/src/Perspex.SceneGraph/IVisual.cs +++ b/src/Perspex.SceneGraph/IVisual.cs @@ -1,6 +1,7 @@ // 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.Collections; using Perspex.Media; using Perspex.VisualTree; @@ -20,6 +21,16 @@ namespace Perspex /// public interface IVisual { + /// + /// Raised when the control is attached to a rooted visual tree. + /// + event EventHandler AttachedToVisualTree; + + /// + /// Raised when the control is detached from a rooted visual tree. + /// + event EventHandler DetachedFromVisualTree; + /// /// Gets the bounds of the scene graph node relative to its parent. /// From 6fd2ec36a253224baccd704d9a046db9d6f33c77 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 6 Nov 2015 20:02:10 +0100 Subject: [PATCH 07/18] Fixed directory name/namespace in tests. To make it same as the namespace being tested. --- .../ExpressionNodeBuilderTests.cs | 2 +- .../ExpressionNodeBuilderTests_Errors.cs | 2 +- .../ExpressionObserverTests_Indexer.cs | 2 +- .../ExpressionObserverTests_Negation.cs | 2 +- .../ExpressionObserverTests_Observable.cs | 2 +- ...ExpressionObserverTests_PerspexProperty.cs | 2 +- .../ExpressionObserverTests_Property.cs | 2 +- .../ExpressionObserverTests_SetValue.cs | 2 +- .../ExpressionObserverTests_Task.cs | 2 +- .../ExpressionSubjectTests.cs | 2 +- .../{Binding => Data}/NotifyingBase.cs | 2 +- .../Perspex.Markup.UnitTests.csproj | 22 +++++++++---------- 12 files changed, 22 insertions(+), 22 deletions(-) rename tests/Perspex.Markup.UnitTests/{Binding => Data}/ExpressionNodeBuilderTests.cs (99%) rename tests/Perspex.Markup.UnitTests/{Binding => Data}/ExpressionNodeBuilderTests_Errors.cs (98%) rename tests/Perspex.Markup.UnitTests/{Binding => Data}/ExpressionObserverTests_Indexer.cs (99%) rename tests/Perspex.Markup.UnitTests/{Binding => Data}/ExpressionObserverTests_Negation.cs (98%) rename tests/Perspex.Markup.UnitTests/{Binding => Data}/ExpressionObserverTests_Observable.cs (97%) rename tests/Perspex.Markup.UnitTests/{Binding => Data}/ExpressionObserverTests_PerspexProperty.cs (97%) rename tests/Perspex.Markup.UnitTests/{Binding => Data}/ExpressionObserverTests_Property.cs (99%) rename tests/Perspex.Markup.UnitTests/{Binding => Data}/ExpressionObserverTests_SetValue.cs (97%) rename tests/Perspex.Markup.UnitTests/{Binding => Data}/ExpressionObserverTests_Task.cs (98%) rename tests/Perspex.Markup.UnitTests/{Binding => Data}/ExpressionSubjectTests.cs (99%) rename tests/Perspex.Markup.UnitTests/{Binding => Data}/NotifyingBase.cs (96%) diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionNodeBuilderTests.cs similarity index 99% rename from tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs rename to tests/Perspex.Markup.UnitTests/Data/ExpressionNodeBuilderTests.cs index ea9d120101..33d8a6ff6c 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionNodeBuilderTests.cs @@ -6,7 +6,7 @@ using System.Linq; using Perspex.Markup.Data; using Xunit; -namespace Perspex.Markup.UnitTests.Binding +namespace Perspex.Markup.UnitTests.Data { public class ExpressionNodeBuilderTests { diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests_Errors.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionNodeBuilderTests_Errors.cs similarity index 98% rename from tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests_Errors.cs rename to tests/Perspex.Markup.UnitTests/Data/ExpressionNodeBuilderTests_Errors.cs index 05af5175b6..beaca3a30d 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests_Errors.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionNodeBuilderTests_Errors.cs @@ -4,7 +4,7 @@ using Perspex.Markup.Data; using Xunit; -namespace Perspex.Markup.UnitTests.Binding +namespace Perspex.Markup.UnitTests.Data { public class ExpressionNodeBuilderTests_Errors { diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs similarity index 99% rename from tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs rename to tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs index 814c252cbc..1359a853e3 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs @@ -8,7 +8,7 @@ using System.Reactive.Linq; using Perspex.Markup.Data; using Xunit; -namespace Perspex.Markup.UnitTests.Binding +namespace Perspex.Markup.UnitTests.Data { public class ExpressionObserverTests_Indexer { diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs similarity index 98% rename from tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs rename to tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs index 47261b9d5c..3581a71563 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs @@ -6,7 +6,7 @@ using System.Reactive.Linq; using Perspex.Markup.Data; using Xunit; -namespace Perspex.Markup.UnitTests.Binding +namespace Perspex.Markup.UnitTests.Data { public class ExpressionObserverTests_Negation { diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs similarity index 97% rename from tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs rename to tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs index ada9c198dd..bf8803912e 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs @@ -8,7 +8,7 @@ using System.Reactive.Subjects; using Perspex.Markup.Data; using Xunit; -namespace Perspex.Markup.UnitTests.Binding +namespace Perspex.Markup.UnitTests.Data { public class ExpressionObserverTests_Observable { diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_PerspexProperty.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_PerspexProperty.cs similarity index 97% rename from tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_PerspexProperty.cs rename to tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_PerspexProperty.cs index 0ef082c7bc..2c44baeda6 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_PerspexProperty.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_PerspexProperty.cs @@ -7,7 +7,7 @@ using System.Reactive.Linq; using Perspex.Markup.Data; using Xunit; -namespace Perspex.Markup.UnitTests.Binding +namespace Perspex.Markup.UnitTests.Data { public class ExpressionObserverTests_PerspexProperty { diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs similarity index 99% rename from tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs rename to tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index 24d0442ebb..ed8a1bfb7e 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -8,7 +8,7 @@ using System.Reactive.Subjects; using Perspex.Markup.Data; using Xunit; -namespace Perspex.Markup.UnitTests.Binding +namespace Perspex.Markup.UnitTests.Data { public class ExpressionObserverTests_Property { diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_SetValue.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs similarity index 97% rename from tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_SetValue.cs rename to tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs index 784dba605c..166c11eb42 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_SetValue.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs @@ -7,7 +7,7 @@ using System.Reactive.Linq; using Perspex.Markup.Data; using Xunit; -namespace Perspex.Markup.UnitTests.Binding +namespace Perspex.Markup.UnitTests.Data { public class ExpressionObserverTests_SetValue { diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs similarity index 98% rename from tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs rename to tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs index 7e532ab943..b6c39ab28c 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Perspex.Markup.Data; using Xunit; -namespace Perspex.Markup.UnitTests.Binding +namespace Perspex.Markup.UnitTests.Data { public class ExpressionObserverTests_Task { diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionSubjectTests.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionSubjectTests.cs similarity index 99% rename from tests/Perspex.Markup.UnitTests/Binding/ExpressionSubjectTests.cs rename to tests/Perspex.Markup.UnitTests/Data/ExpressionSubjectTests.cs index ff0aac8e1a..c38085ef5f 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionSubjectTests.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionSubjectTests.cs @@ -5,7 +5,7 @@ using System.Reactive.Linq; using Perspex.Markup.Data; using Xunit; -namespace Perspex.Markup.UnitTests.Binding +namespace Perspex.Markup.UnitTests.Data { public class ExpressionSubjectTests { diff --git a/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs b/tests/Perspex.Markup.UnitTests/Data/NotifyingBase.cs similarity index 96% rename from tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs rename to tests/Perspex.Markup.UnitTests/Data/NotifyingBase.cs index 5cc6a32fea..384c3ede02 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs +++ b/tests/Perspex.Markup.UnitTests/Data/NotifyingBase.cs @@ -4,7 +4,7 @@ using System.ComponentModel; using System.Linq; -namespace Perspex.Markup.UnitTests.Binding +namespace Perspex.Markup.UnitTests.Data { public class NotifyingBase : INotifyPropertyChanged { diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index 66c470fc05..ab28ea66ac 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -72,17 +72,17 @@ - - - - - - - - - - - + + + + + + + + + + + From 554383b61d599447b997043c9e9618e4004e6c30 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 7 Nov 2015 13:01:39 +0100 Subject: [PATCH 08/18] Added ControlLocator. To track named controls relative to another control. --- .../Perspex.Markup/Data/ControlLocator.cs | 60 +++++++++ .../Perspex.Markup/Perspex.Markup.csproj | 21 +++ src/Perspex.Controls/ControlExtensions.cs | 20 ++- .../Data/ControlLocatorTests.cs | 123 ++++++++++++++++++ .../Perspex.Markup.UnitTests.csproj | 35 +++++ tests/Perspex.Markup.UnitTests/TestRoot.cs | 61 +++++++++ 6 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 src/Markup/Perspex.Markup/Data/ControlLocator.cs create mode 100644 tests/Perspex.Markup.UnitTests/Data/ControlLocatorTests.cs create mode 100644 tests/Perspex.Markup.UnitTests/TestRoot.cs diff --git a/src/Markup/Perspex.Markup/Data/ControlLocator.cs b/src/Markup/Perspex.Markup/Data/ControlLocator.cs new file mode 100644 index 0000000000..082741ae06 --- /dev/null +++ b/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 +{ + /// + /// Locates controls relative to other controls. + /// + public static class ControlLocator + { + /// + /// Tracks a named control relative to another control. + /// + /// + /// The control relative from which the other control should be found. + /// + /// The name of the control to find. + public static IObservable Track(IControl relativeTo, string name) + { + var attached = Observable.FromEventPattern( + x => relativeTo.AttachedToVisualTree += x, + x => relativeTo.DetachedFromVisualTree += x) + .Select(x => x.EventArgs.NameScope) + .StartWith(relativeTo.FindNameScope()); + + var detached = Observable.FromEventPattern( + 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( + x => nameScope.Registered += x, + x => nameScope.Registered -= x) + .Where(x => x.EventArgs.Name == name) + .Select(x => x.EventArgs.Element) + .OfType(); + var unregistered = Observable.FromEventPattern( + x => nameScope.Unregistered += x, + x => nameScope.Unregistered -= x); + return registered + .StartWith(nameScope.Find(name)) + .TakeUntil(unregistered); + } + else + { + return Observable.Return(null); + } + }).Switch(); + } + } +} diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index 414ec0dee2..02ee1b1113 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -42,6 +42,7 @@ + @@ -91,10 +92,30 @@ {b09b78d8-9b26-48b0-9149-d64a2f120f3f} Perspex.Base + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Perspex.Controls + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Perspex.Input + + + {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} + Perspex.Interactivity + + + {42472427-4774-4c81-8aff-9f27b8e31721} + Perspex.Layout + {eb582467-6abb-43a1-b052-e981ba910e3a} Perspex.SceneGraph + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Perspex.Styling +