diff --git a/build/Base.props b/build/Base.props index a8702d32a8..a60373ebb3 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,10 +1,5 @@  - - diff --git a/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs new file mode 100644 index 0000000000..f321b2b2f1 --- /dev/null +++ b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities +{ + /// + /// A task-like operation that is guaranteed to finish continuations synchronously, + /// can be used for parametrized one-shot events + /// + public struct SynchronousCompletionAsyncResult : INotifyCompletion + { + private readonly SynchronousCompletionAsyncResultSource _source; + private readonly T _result; + private readonly bool _isValid; + internal SynchronousCompletionAsyncResult(SynchronousCompletionAsyncResultSource source) + { + _source = source; + _result = default; + _isValid = true; + } + + public SynchronousCompletionAsyncResult(T result) + { + _result = result; + _source = null; + _isValid = true; + } + + static void ThrowNotInitialized() => + throw new InvalidOperationException("This SynchronousCompletionAsyncResult was not initialized"); + + public bool IsCompleted + { + get + { + if (!_isValid) + ThrowNotInitialized(); + return _source == null || _source.IsCompleted; + } + } + + public T GetResult() + { + if (!_isValid) + ThrowNotInitialized(); + return _source == null ? _result : _source.Result; + } + + + public void OnCompleted(Action continuation) + { + if (!_isValid) + ThrowNotInitialized(); + if (_source == null) + continuation(); + else + _source.OnCompleted(continuation); + } + + public SynchronousCompletionAsyncResult GetAwaiter() => this; + } + + /// + /// Source for incomplete SynchronousCompletionAsyncResult + /// + /// + public class SynchronousCompletionAsyncResultSource + { + private T _result; + internal bool IsCompleted { get; private set; } + public SynchronousCompletionAsyncResult AsyncResult => new SynchronousCompletionAsyncResult(this); + + internal T Result => IsCompleted ? + _result : + throw new InvalidOperationException("Asynchronous operation is not yet completed"); + + private List _continuations; + + internal void OnCompleted(Action continuation) + { + if(_continuations==null) + _continuations = new List(); + _continuations.Add(continuation); + } + + public void SetResult(T result) + { + if (IsCompleted) + throw new InvalidOperationException("Asynchronous operation is already completed"); + _result = result; + IsCompleted = true; + if(_continuations!=null) + foreach (var c in _continuations) + c(); + _continuations = null; + } + + public void TrySetResult(T result) + { + if(IsCompleted) + return; + SetResult(result); + } + } +} diff --git a/src/Avalonia.Styling/Controls/ChildNameScope.cs b/src/Avalonia.Styling/Controls/ChildNameScope.cs index c3c3109502..e6707e71db 100644 --- a/src/Avalonia.Styling/Controls/ChildNameScope.cs +++ b/src/Avalonia.Styling/Controls/ChildNameScope.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -14,27 +15,45 @@ namespace Avalonia.Controls public void Register(string name, object element) => _inner.Register(name, element); - public ValueTask FindAsync(string name) + public SynchronousCompletionAsyncResult 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 + return new SynchronousCompletionAsyncResult(found); + // Not found and both current and parent scope are in completed state if(IsCompleted) - return new ValueTask(null); + return new SynchronousCompletionAsyncResult(null); return DoFindAsync(name); } - async ValueTask DoFindAsync(string name) + public SynchronousCompletionAsyncResult DoFindAsync(string name) { + var src = new SynchronousCompletionAsyncResultSource(); + + void ParentSearch() + { + var parentSearch = _parentScope.FindAsync(name); + if (parentSearch.IsCompleted) + src.SetResult(parentSearch.GetResult()); + else + parentSearch.OnCompleted(() => src.SetResult(parentSearch.GetResult())); + } if (!_inner.IsCompleted) { - var found = await _inner.FindAsync(name); - if (found != null) - return found; + // Guaranteed to be incomplete at this point + var innerSearch = _inner.FindAsync(name); + innerSearch.OnCompleted(() => + { + var value = innerSearch.GetResult(); + if (value != null) + src.SetResult(value); + else ParentSearch(); + }); } + else + ParentSearch(); - return await _parentScope.FindAsync(name); + return src.AsyncResult; } public object Find(string name) diff --git a/src/Avalonia.Styling/Controls/INameScope.cs b/src/Avalonia.Styling/Controls/INameScope.cs index 8386157f3d..b4f4c8e57b 100644 --- a/src/Avalonia.Styling/Controls/INameScope.cs +++ b/src/Avalonia.Styling/Controls/INameScope.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -24,7 +25,7 @@ namespace Avalonia.Controls /// /// The name. /// The element, or null if the name was not found. - ValueTask FindAsync(string name); + SynchronousCompletionAsyncResult FindAsync(string name); /// /// Finds a named element in the name scope, returns immediately, doesn't traverse the name scope stack diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs index ff5e182146..ec01a53cfd 100644 --- a/src/Avalonia.Styling/Controls/NameScope.cs +++ b/src/Avalonia.Styling/Controls/NameScope.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.LogicalTree; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -24,8 +25,8 @@ namespace Avalonia.Controls private readonly Dictionary _inner = new Dictionary(); - private readonly Dictionary> _pendingSearches = - new Dictionary>(); + private readonly Dictionary> _pendingSearches = + new Dictionary>(); /// /// Gets the value of the attached on a styled element. @@ -79,18 +80,18 @@ namespace Avalonia.Controls } } - public ValueTask FindAsync(string name) + public SynchronousCompletionAsyncResult FindAsync(string name) { var found = Find(name); if (found != null) - return new ValueTask(found); + return new SynchronousCompletionAsyncResult(found); if (IsCompleted) - return new ValueTask((object)null); + return new SynchronousCompletionAsyncResult((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); + _pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource(); + + return tcs.AsyncResult; } /// diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Styling/Controls/NameScopeLocator.cs index ac588c2f5a..9cec6950e4 100644 --- a/src/Avalonia.Styling/Controls/NameScopeLocator.cs +++ b/src/Avalonia.Styling/Controls/NameScopeLocator.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Threading.Tasks; using Avalonia.LogicalTree; using Avalonia.Reactive; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -20,31 +21,31 @@ namespace Avalonia.Controls /// The name of the control to find. public static IObservable Track(INameScope scope, string name) { - return new NeverEndingValueTaskObservable(scope.FindAsync(name)); + return new NeverEndingSynchronousCompletionAsyncResultObservable(scope.FindAsync(name)); } // 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 - private class NeverEndingValueTaskObservable : IObservable + private class NeverEndingSynchronousCompletionAsyncResultObservable : IObservable { private T _value; - private Task _task; + private SynchronousCompletionAsyncResult? _task; - public NeverEndingValueTaskObservable(ValueTask task) + public NeverEndingSynchronousCompletionAsyncResultObservable(SynchronousCompletionAsyncResult task) { if (task.IsCompleted) - _value = task.Result; + _value = task.GetResult(); else - _task = task.AsTask(); + _task = task; } public IDisposable Subscribe(IObserver observer) { if (_task?.IsCompleted == true) { - _value = _task.Result; + _value = _task.Value.GetResult(); _task = null; } @@ -52,10 +53,10 @@ namespace Avalonia.Controls // We expect everything to handle callbacks synchronously, // so the object graph is ready after its built // so keep TaskContinuationOptions.ExecuteSynchronously - _task.ContinueWith(t => + _task.Value.OnCompleted(() => { - observer.OnNext(t.Result); - }, TaskContinuationOptions.ExecuteSynchronously); + observer.OnNext(_task.Value.GetResult()); + }); else observer.OnNext(_value);