Browse Source

Use custom synchronous task-like awaitable

Nikita Tsukanov 6 years ago
parent
commit
6a90251654

+ 0 - 5
build/Base.props

@@ -1,10 +1,5 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
     <PackageReference Include="System.ValueTuple" Version="4.5.0" />
-    <!--
-       Do not upgrade, see 
-       https://github.com/xamarin/xamarin-android/issues/1879#issuecomment-412281057 
-    -->
-    <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.4.0" />
   </ItemGroup>
 </Project>

+ 106 - 0
src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs

@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Utilities
+{
+    /// <summary>
+    /// A task-like operation that is guaranteed to finish continuations synchronously,
+    /// can be used for parametrized one-shot events
+    /// </summary>
+    public struct SynchronousCompletionAsyncResult<T> : INotifyCompletion
+    {
+        private readonly SynchronousCompletionAsyncResultSource<T> _source;
+        private readonly T _result;
+        private readonly bool _isValid;
+        internal SynchronousCompletionAsyncResult(SynchronousCompletionAsyncResultSource<T> 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<T> GetAwaiter() => this;
+    }
+
+    /// <summary>
+    /// Source for incomplete SynchronousCompletionAsyncResult
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    public class SynchronousCompletionAsyncResultSource<T>
+    {
+        private T _result;
+        internal bool IsCompleted { get; private set; }
+        public SynchronousCompletionAsyncResult<T> AsyncResult => new SynchronousCompletionAsyncResult<T>(this);
+        
+        internal T Result => IsCompleted ?
+            _result :
+            throw new InvalidOperationException("Asynchronous operation is not yet completed");
+        
+        private List<Action> _continuations;
+
+        internal void OnCompleted(Action continuation)
+        {
+            if(_continuations==null)
+                _continuations = new List<Action>();
+            _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);
+        }
+    }
+}

+ 28 - 9
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<object> FindAsync(string name)
+        public SynchronousCompletionAsyncResult<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
+                return new SynchronousCompletionAsyncResult<object>(found);
+            // Not found and both current and parent scope are in completed state
             if(IsCompleted)
-                return new ValueTask<object>(null);
+                return new SynchronousCompletionAsyncResult<object>(null);
             return DoFindAsync(name);
         }
 
-        async ValueTask<object> DoFindAsync(string name)
+        public SynchronousCompletionAsyncResult<object> DoFindAsync(string name)
         {
+            var src = new SynchronousCompletionAsyncResultSource<object>();
+
+            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)

+ 2 - 1
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
         /// </summary>
         /// <param name="name">The name.</param>
         /// <returns>The element, or null if the name was not found.</returns>
-        ValueTask<object> FindAsync(string name);
+        SynchronousCompletionAsyncResult<object> FindAsync(string name);
         
         /// <summary>
         /// Finds a named element in the name scope, returns immediately, doesn't traverse the name scope stack

+ 9 - 8
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<string, object> _inner = new Dictionary<string, object>();
 
-        private readonly Dictionary<string, TaskCompletionSource<object>> _pendingSearches =
-            new Dictionary<string, TaskCompletionSource<object>>();
+        private readonly Dictionary<string, SynchronousCompletionAsyncResultSource<object>> _pendingSearches =
+            new Dictionary<string, SynchronousCompletionAsyncResultSource<object>>();
         
         /// <summary>
         /// Gets the value of the attached <see cref="NameScopeProperty"/> on a styled element.
@@ -79,18 +80,18 @@ namespace Avalonia.Controls
             }
         }
 
-        public ValueTask<object> FindAsync(string name)
+        public SynchronousCompletionAsyncResult<object> FindAsync(string name)
         {
             var found = Find(name);
             if (found != null)
-                return new ValueTask<object>(found);
+                return new SynchronousCompletionAsyncResult<object>(found);
             if (IsCompleted)
-                return new ValueTask<object>((object)null);
+                return new SynchronousCompletionAsyncResult<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);
+                _pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource<object>();
+
+            return tcs.AsyncResult;
         }
 
         /// <inheritdoc />

+ 11 - 10
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
         /// <param name="name">The name of the control to find.</param>
         public static IObservable<object> Track(INameScope scope, string name)
         {
-            return new NeverEndingValueTaskObservable<object>(scope.FindAsync(name));
+            return new NeverEndingSynchronousCompletionAsyncResultObservable<object>(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<T> : IObservable<T>
+        private class NeverEndingSynchronousCompletionAsyncResultObservable<T> : IObservable<T>
         {
             private T _value;
-            private Task<T> _task;
+            private SynchronousCompletionAsyncResult<T>? _task;
 
-            public NeverEndingValueTaskObservable(ValueTask<T> task)
+            public NeverEndingSynchronousCompletionAsyncResultObservable(SynchronousCompletionAsyncResult<T> task)
             {
                 if (task.IsCompleted)
-                    _value = task.Result;
+                    _value = task.GetResult();
                 else
-                    _task = task.AsTask();
+                    _task = task;
             }
             
             public IDisposable Subscribe(IObserver<T> 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);