diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 5cd2ddfc35..ef253a28e2 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -671,6 +671,23 @@ namespace Avalonia.Controls if (Name != null) { _nameScope?.Register(Name, this); + + var visualParent = Parent as Visual; + + if (this is INameScope && visualParent != null) + { + // If we have e.g. a named UserControl in a window then we want that control + // to be findable by name from the Window, so register with both name scopes. + // This differs from WPF's behavior in that XAML manually registers controls + // with name scopes based on the XAML file in which the name attribute appears, + // but we're trying to avoid XAML magic in Avalonia in order to made code- + // created UIs easy. This will cause problems if a UserControl declares a name + // in its XAML and that control is included multiple times in a parent control + // (as the name will be duplicated), however at the moment I'm fine with saying + // "don't do that". + var parentNameScope = NameScope.FindNameScope(visualParent); + parentNameScope?.Register(Name, this); + } } } diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs index ddfb3ea173..4c5875479e 100644 --- a/src/Avalonia.Styling/Controls/NameScope.cs +++ b/src/Avalonia.Styling/Controls/NameScope.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Avalonia.LogicalTree; namespace Avalonia.Controls { @@ -29,6 +30,32 @@ namespace Avalonia.Controls /// public event EventHandler Unregistered; + /// + /// Finds the containing name scope for a visual. + /// + /// The visual. + /// The containing name scope. + public static INameScope FindNameScope(Visual visual) + { + Contract.Requires(visual != null); + + INameScope result; + + while (visual != null) + { + result = visual as INameScope ?? GetNameScope(visual); + + if (result != null) + { + return result; + } + + visual = (visual as ILogical).LogicalParent as Visual; + } + + return null; + } + /// /// Gets the value of the attached on a visual. /// @@ -36,6 +63,8 @@ namespace Avalonia.Controls /// The value of the NameScope attached property. public static INameScope GetNameScope(Visual visual) { + Contract.Requires(visual != null); + return visual.GetValue(NameScopeProperty); } @@ -46,6 +75,8 @@ namespace Avalonia.Controls /// The value to set. public static void SetNameScope(Visual visual, INameScope value) { + Contract.Requires(visual != null); + visual.SetValue(NameScopeProperty, value); } diff --git a/tests/Avalonia.Controls.UnitTests/ControlTests_NameScope.cs b/tests/Avalonia.Controls.UnitTests/ControlTests_NameScope.cs index ec75c2390b..9f39f7a47a 100644 --- a/tests/Avalonia.Controls.UnitTests/ControlTests_NameScope.cs +++ b/tests/Avalonia.Controls.UnitTests/ControlTests_NameScope.cs @@ -70,5 +70,23 @@ namespace Avalonia.Controls.UnitTests Assert.Null(NameScope.GetNameScope((Control)root.Presenter).Find("foo")); } + + [Fact] + public void Control_That_Is_NameScope_Should_Register_With_Parent_NameScope() + { + UserControl userControl; + var root = new TestTemplatedRoot + { + Content = userControl = new UserControl + { + Name = "foo", + } + }; + + root.ApplyTemplate(); + + Assert.Same(userControl, root.FindControl("foo")); + Assert.Same(userControl, userControl.FindControl("foo")); + } } }