Browse Source

Introduced name scope chains

pull/2705/head
Nikita Tsukanov 7 years ago
parent
commit
3169f59da8
  1. 3
      build/Base.props
  2. 54
      src/Avalonia.Styling/Controls/ChildNameScope.cs
  3. 32
      src/Avalonia.Styling/Controls/INameScope.cs
  4. 97
      src/Avalonia.Styling/Controls/NameScope.cs
  5. 76
      src/Avalonia.Styling/Controls/NameScopeLocator.cs
  6. 19
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs
  7. 6
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  8. 6
      src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
  9. 125
      tests/Avalonia.Controls.UnitTests/NameScopeTests.cs
  10. 2
      tests/Avalonia.LeakTests/ControlTests.cs
  11. 221
      tests/Avalonia.Styling.UnitTests/ControlLocatorTests.cs

3
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
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;
}
}

32
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; }
}
}

97
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();
}
}
}

76
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;
}
}
}
}

19
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
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});

6
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);
};
}

125
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"));
}
}
}

2
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);

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);
}
}
}
Loading…
Cancel
Save