diff --git a/build/Base.props b/build/Base.props index e565ab1664..2968fe5175 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,5 +1,6 @@  + - \ No newline at end of file + diff --git a/src/Avalonia.Styling/Controls/ChildNameScope.cs b/src/Avalonia.Styling/Controls/ChildNameScope.cs new file mode 100644 index 0000000000..c3c3109502 --- /dev/null +++ b/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 FindAsync(string name) + { + var found = Find(name); + if (found != null) + return new ValueTask(found); + // Not found and both current and parent scope are in completed stage + if(IsCompleted) + return new ValueTask(null); + return DoFindAsync(name); + } + + async ValueTask 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; + } +} diff --git a/src/Avalonia.Styling/Controls/INameScope.cs b/src/Avalonia.Styling/Controls/INameScope.cs index e4ff90c573..9e21055126 100644 --- a/src/Avalonia.Styling/Controls/INameScope.cs +++ b/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 /// 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 in the name scope. /// @@ -28,16 +19,29 @@ namespace Avalonia.Controls void Register(string name, object element); /// - /// 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 + /// + /// The name. + /// The element, or null if the name was not found. + ValueTask FindAsync(string name); + + /// + /// Finds a named element in the name scope, returns immediately, doesn't traverse the name scope stack /// /// The name. /// The element, or null if the name was not found. object Find(string name); /// - /// Unregisters an element with the name scope. + /// Marks the name scope as completed, no further registrations will be allowed /// - /// The name. - void Unregister(string name); + void Complete(); + + /// + /// Returns whether further registrations are allowed on the scope + /// + bool IsCompleted { get; } + + } } diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs index afafa03772..f17126a9c6 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 System.Threading.Tasks; using Avalonia.LogicalTree; namespace Avalonia.Controls @@ -18,44 +19,14 @@ namespace Avalonia.Controls public static readonly AttachedProperty NameScopeProperty = AvaloniaProperty.RegisterAttached("NameScope"); + /// + public bool IsCompleted { get; private set; } + private readonly 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; - - /// - /// Finds the containing name scope for a styled element. - /// - /// The styled element. - /// The containing name scope. - public static INameScope FindNameScope(StyledElement styled) - { - Contract.Requires(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> _pendingSearches = + new Dictionary>(); + /// /// Gets the value of the attached on a styled element. /// @@ -80,13 +51,11 @@ namespace Avalonia.Controls styled.SetValue(NameScopeProperty, value); } - /// - /// Registers an element with the name scope. - /// - /// The element name. - /// The element. + /// public void Register(string name, object element) { + if (IsCompleted) + throw new InvalidOperationException("NameScope is completed, no further registrations are allowed"); Contract.Requires(name != null); Contract.Requires(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); } } - - /// - /// Finds a named element in the name scope. - /// - /// The name. - /// The element, or null if the name was not found. + + public ValueTask FindAsync(string name) + { + var found = Find(name); + if (found != null) + return new ValueTask(found); + if (IsCompleted) + return new ValueTask((object)null); + if (!_pendingSearches.TryGetValue(name, out var tcs)) + // We are intentionally running continuations synchronously here + _pendingSearches[name] = tcs = new TaskCompletionSource(); + + return new ValueTask(tcs.Task); + } + + /// public object Find(string name) { Contract.Requires(name != null); @@ -120,21 +100,14 @@ namespace Avalonia.Controls return result; } - /// - /// Unregisters an element with the name scope. - /// - /// The name. - public void Unregister(string name) + public void Complete() { - Contract.Requires(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(); } + + } } diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Styling/Controls/NameScopeLocator.cs index 024fdb70a5..61e5b06402 100644 --- a/src/Avalonia.Styling/Controls/NameScopeLocator.cs +++ b/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 /// The name of the control to find. public static IObservable Track(INameScope scope, string name) { - return new ScopeTracker(scope, name); + return new NeverEndingValueTaskObservable(scope.FindAsync(name)); } - private class ScopeTracker : LightweightObservableBase - { - 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(_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 : IObservable + { + private T _value; + private Task _task; - protected override void Subscribed(IObserver observer, bool first) + public NeverEndingValueTaskObservable(ValueTask 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 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; } - } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs index d19770b8bd..805b733feb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs +++ b/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); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 7ddd815e47..1efae902c6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/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}); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 0b0e1c5c5e..747b21c679 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -21,8 +21,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime var rootObject = provider.GetService().RootObject; return sp => { - var scope = new NameScope(); - var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope)); + var parentScope = sp.GetService(); + 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); }; } diff --git a/tests/Avalonia.Controls.UnitTests/NameScopeTests.cs b/tests/Avalonia.Controls.UnitTests/NameScopeTests.cs index 802f7717e8..4814057db5 100644 --- a/tests/Avalonia.Controls.UnitTests/NameScopeTests.cs +++ b/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(() => 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(() => target.Register("foo", new object())); + target.Register("foo", element); + target.Complete(); + Assert.Throws(() => 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")); } + } } diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index d1fadd3281..b00ae47041 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/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); diff --git a/tests/Avalonia.Styling.UnitTests/ControlLocatorTests.cs b/tests/Avalonia.Styling.UnitTests/ControlLocatorTests.cs deleted file mode 100644 index 8806cad28b..0000000000 --- a/tests/Avalonia.Styling.UnitTests/ControlLocatorTests.cs +++ /dev/null @@ -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(); - - 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(); - 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(); - - 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 Registered - { - add - { - _inner.Registered += value; - RegisteredSubscribers++; - } - remove - { - _inner.Registered -= value; - RegisteredSubscribers--; - } - } - - public event EventHandler 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); - } - } -}