diff --git a/sample/Comet.Samples/ProgressBarSample1.cs b/sample/Comet.Samples/ProgressBarSample1.cs index b1529fd5..166c233a 100644 --- a/sample/Comet.Samples/ProgressBarSample1.cs +++ b/sample/Comet.Samples/ProgressBarSample1.cs @@ -9,16 +9,14 @@ public class ProgressBarSample1 : View public ProgressBarSample1() { - _timer = new Timer(state => - { + _timer = new Timer(async state => { var p = (State)state; - Device.InvokeOnMainThread(() => - { - var current = p.Value; - var value = current < 101 ? current + 1 : 0; + await ThreadHelper.SwitchToMainThreadAsync(); - p.Value = value; - }); + var current = p.Value; + var value = current < 101 ? current + 1 : 0; + + p.Value = value; }, percentage, 100, 100); } diff --git a/src/Comet.Android/UI.cs b/src/Comet.Android/UI.cs index e5dd523d..81540b63 100644 --- a/src/Comet.Android/UI.cs +++ b/src/Comet.Android/UI.cs @@ -48,7 +48,7 @@ public static void Init() ModalView.PerformDismiss = ModalManager.DismisModal; // Device Services - Device.PerformInvokeOnMainThread = (a) => AndroidContext.CurrentContext.RunOnUiThread(a); + ThreadHelper.JoinableTaskContext = new Microsoft.VisualStudio.Threading.JoinableTaskContext(); Device.GraphicsService = new AndroidGraphicsService(); Device.BitmapService = new AndroidBitmapService(); } diff --git a/src/Comet.Blazor/UI.cs b/src/Comet.Blazor/UI.cs index 7aacecc7..4af92d2e 100644 --- a/src/Comet.Blazor/UI.cs +++ b/src/Comet.Blazor/UI.cs @@ -38,7 +38,7 @@ public static void Init() Registrar.Handlers.Register>(); Registrar.Handlers.Register>(); - Device.PerformInvokeOnMainThread = a => a(); + ThreadHelper.JoinableTaskContext = new Microsoft.VisualStudio.Threading.JoinableTaskContext(); ListView.HandlerSupportsVirtualization = false; } } diff --git a/src/Comet.Mac/UI.cs b/src/Comet.Mac/UI.cs index ab3c83b5..5e767318 100644 --- a/src/Comet.Mac/UI.cs +++ b/src/Comet.Mac/UI.cs @@ -41,7 +41,7 @@ public static void Init() Registrar.Handlers.Register(); // Device Features - Device.PerformInvokeOnMainThread = _invoker.BeginInvokeOnMainThread; + ThreadHelper.JoinableTaskContext = new Microsoft.VisualStudio.Threading.JoinableTaskContext(); Device.FontService = new MacFontService(); Device.GraphicsService = new MacGraphicsService(); Device.BitmapService = new MacBitmapService(); diff --git a/src/Comet.UWP/UI.cs b/src/Comet.UWP/UI.cs index ec7a1128..13df4166 100644 --- a/src/Comet.UWP/UI.cs +++ b/src/Comet.UWP/UI.cs @@ -45,7 +45,7 @@ public static void Init() Registrar.Handlers.Register(); Registrar.Handlers.Register(); - Device.PerformInvokeOnMainThread = async a => await GetDispatcher().RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => a()); + ThreadHelper.JoinableTaskContext = new Microsoft.VisualStudio.Threading.JoinableTaskContext(); Device.BitmapService = new UWPBitmapService(); diff --git a/src/Comet.WPF/UI.cs b/src/Comet.WPF/UI.cs index f3a8e916..5ac64bac 100644 --- a/src/Comet.WPF/UI.cs +++ b/src/Comet.WPF/UI.cs @@ -41,7 +41,7 @@ public static void Init() Device.BitmapService = new WPFBitmapService(); - Device.PerformInvokeOnMainThread = a => Application.Current.Dispatcher.Invoke(a); + ThreadHelper.JoinableTaskContext = new Microsoft.VisualStudio.Threading.JoinableTaskContext(); ListView.HandlerSupportsVirtualization = false; } diff --git a/src/Comet.iOS/UI.cs b/src/Comet.iOS/UI.cs index fbef03b3..b5f48710 100644 --- a/src/Comet.iOS/UI.cs +++ b/src/Comet.iOS/UI.cs @@ -52,8 +52,8 @@ public static void Init () }; ModalView.PerformDismiss = () => PresentingViewController.DismissModalViewController (true); - // Device Features - Device.PerformInvokeOnMainThread = _invoker.BeginInvokeOnMainThread; + ThreadHelper.JoinableTaskContext = new Microsoft.VisualStudio.Threading.JoinableTaskContext(); + Device.FontService = new iOSFontService(); Device.GraphicsService = new iOSGraphicsService(); Device.BitmapService = new iOSBitmapService(); diff --git a/src/Comet/Comet.csproj b/src/Comet/Comet.csproj index 87b21487..ad313e6d 100644 --- a/src/Comet/Comet.csproj +++ b/src/Comet/Comet.csproj @@ -18,6 +18,9 @@ + + + diff --git a/src/Comet/Controls/View.cs b/src/Comet/Controls/View.cs index b5e0115f..c9bcaed0 100644 --- a/src/Comet/Controls/View.cs +++ b/src/Comet/Controls/View.cs @@ -254,13 +254,12 @@ internal override void ContextPropertyChanged(string property, object value, boo ViewPropertyChanged(property, value); } - public static void SetGlobalEnvironment(string key, object value) + public static async void SetGlobalEnvironment(string key, object value) { Environment.SetValue(key, value); - Device.InvokeOnMainThread(() => - { - ActiveViews.ForEach(x => x.ViewPropertyChanged(key, value)); - }); + await ThreadHelper.SwitchToMainThreadAsync(); + ActiveViews.ForEach(x => x.ViewPropertyChanged(key, value)); + } public static void SetGlobalEnvironment(IDictionary data) { diff --git a/src/Comet/Device.cs b/src/Comet/Device.cs index 6b47dff9..c9f541ab 100644 --- a/src/Comet/Device.cs +++ b/src/Comet/Device.cs @@ -8,22 +8,6 @@ namespace Comet { public static class Device { - - static Device() - { - mainThread = Thread.CurrentThread; - } - - public static Action PerformInvokeOnMainThread; - internal static Thread mainThread; - public static void InvokeOnMainThread(Action action) - { - if (mainThread == Thread.CurrentThread) - action(); - else - PerformInvokeOnMainThread(action); - } - public static IFontService FontService = new FallbackFontService(); public static IGraphicsService GraphicsService; public static IBitmapService BitmapService; diff --git a/src/Comet/EnvironmentAware.cs b/src/Comet/EnvironmentAware.cs index 7a97ef46..27dacdf7 100644 --- a/src/Comet/EnvironmentAware.cs +++ b/src/Comet/EnvironmentAware.cs @@ -124,7 +124,7 @@ public static T SetEnvironment(this T contextualObject, Type type, string key var typedKey = ContextualObject.GetTypedKey(type, key); contextualObject.SetValue(typedKey, value, cascades); //TODO: Verify this is needed - Device.InvokeOnMainThread(() => { + ThreadHelper.RunOnMainThread(() => { contextualObject.ContextPropertyChanged(typedKey, value,cascades); }); return contextualObject; @@ -135,7 +135,7 @@ public static T SetEnvironment(this T contextualObject, string key, object va { if(!contextualObject.SetValue(key, value, cascades)) return contextualObject; - Device.InvokeOnMainThread(() => + ThreadHelper.RunOnMainThread(() => { contextualObject.ContextPropertyChanged(key, value,cascades); }); diff --git a/src/Comet/Helpers/ThreadHelper.cs b/src/Comet/Helpers/ThreadHelper.cs new file mode 100644 index 00000000..45c06da4 --- /dev/null +++ b/src/Comet/Helpers/ThreadHelper.cs @@ -0,0 +1,199 @@ +using Microsoft.VisualStudio.Threading; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Comet +{ + public class ThreadHelper + { + static JoinableTaskContext joinableTaskContext; + public static JoinableTaskContext JoinableTaskContext { + get => joinableTaskContext ?? (joinableTaskContext = new JoinableTaskContext()); + set => joinableTaskContext = value; + } + + + // + // Summary: + // Gets an awaitable whose continuations execute on the synchronization context + // that this instance was initialized with, in such a way as to mitigate both deadlocks + // and reentrancy. + // + // Parameters: + // alwaysYield: + // A value indicating whether the caller should yield even if already executing + // on the main thread. + // + // cancellationToken: + // A token whose cancellation will immediately schedule the continuation on a threadpool + // thread. + // + // Returns: + // An awaitable. + // + // Exceptions: + // T:System.OperationCanceledException: + // Thrown back at the awaiting caller from a background thread when cancellationToken + // is canceled before any required transition to the main thread is complete. No + // exception is thrown if alwaysYield is false and the caller was already on the + // main thread before calling this method, or if the main thread transition completes + // before the thread pool responds to cancellation. + // + // Remarks: + // private async Task SomeOperationAsync() { // This first part can be on the caller's + // thread, whatever that is. DoSomething(); // Now switch to the Main thread to + // talk to some STA object. // Supposing it is also important to *not* do this step + // on our caller's callstack, // be sure we yield even if we're on the UI thread. + // await this.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true); STAService.DoSomething(); + // } + public static JoinableTaskFactory.MainThreadAwaitable SwitchToMainThreadAsync(bool alwaysYield, CancellationToken cancellationToken = default) => JoinableTaskContext.Factory.SwitchToMainThreadAsync(alwaysYield,cancellationToken); + + // + // Summary: + // Gets an awaitable whose continuations execute on the synchronization context + // that this instance was initialized with, in such a way as to mitigate both deadlocks + // and reentrancy. + // + // Parameters: + // cancellationToken: + // A token whose cancellation will immediately schedule the continuation on a threadpool + // thread (if the transition to the main thread is not already complete). The token + // is ignored if the caller was already on the main thread. + // + // Returns: + // An awaitable. + // + // Exceptions: + // T:System.OperationCanceledException: + // Thrown back at the awaiting caller from a background thread when cancellationToken + // is canceled before any required transition to the main thread is complete. No + // exception is thrown if the caller was already on the main thread before calling + // this method, or if the main thread transition completes before the thread pool + // responds to cancellation. + // + // Remarks: + // private async Task SomeOperationAsync() { // on the caller's thread. await DoAsync(); + // // Now switch to a threadpool thread explicitly. await TaskScheduler.Default; + // // Now switch to the Main thread to talk to some STA object. await this.JobContext.SwitchToMainThreadAsync(); + // STAService.DoSomething(); } + public static JoinableTaskFactory.MainThreadAwaitable SwitchToMainThreadAsync(CancellationToken cancellationToken = default) => JoinableTaskContext.Factory.SwitchToMainThreadAsync(cancellationToken); + + + public static async void RunOnMainThread(Action action) + { + await SwitchToMainThreadAsync(); + action(); + } + + // + // Summary: + // Runs the specified asynchronous method to completion while synchronously blocking + // the calling thread. + // + // Parameters: + // asyncMethod: + // The asynchronous method to execute. + // + // Type parameters: + // T: + // The type of value returned by the asynchronous operation. + // + // Returns: + // The result of the Task returned by asyncMethod. + // + // Remarks: + // Any exception thrown by the delegate is rethrown in its original type to the + // caller of this method. + // When the delegate resumes from a yielding await, the default behavior is to resume + // in its original context as an ordinary async method execution would. For example, + // if the caller was on the main thread, execution resumes after an await on the + // main thread; but if it started on a threadpool thread it resumes on a threadpool + // thread. + // See the Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func{System.Threading.Tasks.Task}) + // overload documentation for an example. + public static T Run(Func> asyncMethod) => JoinableTaskContext.Factory.Run(asyncMethod); + // + // Summary: + // Runs the specified asynchronous method to completion while synchronously blocking + // the calling thread. + // + // Parameters: + // asyncMethod: + // The asynchronous method to execute. + // + // Remarks: + // Any exception thrown by the delegate is rethrown in its original type to the + // caller of this method. + // When the delegate resumes from a yielding await, the default behavior is to resume + // in its original context as an ordinary async method execution would. For example, + // if the caller was on the main thread, execution resumes after an await on the + // main thread; but if it started on a threadpool thread it resumes on a threadpool + // thread. + // // On threadpool or Main thread, this method will block // the calling thread + // until all async operations in the // delegate complete. joinableTaskFactory.Run(async + // delegate { // still on the threadpool or Main thread as before. await OperationAsync(); + // // still on the threadpool or Main thread as before. await Task.Run(async delegate + // { // Now we're on a threadpool thread. await Task.Yield(); // still on a threadpool + // thread. }); // Now back on the Main thread (or threadpool thread if that's where + // we started). }); + public static void Run(Func asyncMethod) => JoinableTaskContext.Factory.Run(asyncMethod); + + // + // Summary: + // Invokes an async delegate on the caller's thread, and yields back to the caller + // when the async method yields. The async delegate is invoked in such a way as + // to mitigate deadlocks in the event that the async method requires the main thread + // while the main thread is blocked waiting for the async method's completion. + // + // Parameters: + // asyncMethod: + // The method that, when executed, will begin the async operation. + // + // Returns: + // An object that tracks the completion of the async operation, and allows for later + // synchronous blocking of the main thread for completion if necessary. + // + // Remarks: + // Exceptions thrown by the delegate are captured by the returned Microsoft.VisualStudio.Threading.JoinableTask. + // When the delegate resumes from a yielding await, the default behavior is to resume + // in its original context as an ordinary async method execution would. For example, + // if the caller was on the main thread, execution resumes after an await on the + // main thread; but if it started on a threadpool thread it resumes on a threadpool + // thread. + public static JoinableTask RunAsync(Func asyncMethod) => JoinableTaskContext.Factory.RunAsync(asyncMethod); + + // + // Summary: + // Invokes an async delegate on the caller's thread, and yields back to the caller + // when the async method yields. The async delegate is invoked in such a way as + // to mitigate deadlocks in the event that the async method requires the main thread + // while the main thread is blocked waiting for the async method's completion. + // + // Parameters: + // asyncMethod: + // The method that, when executed, will begin the async operation. + // + // Type parameters: + // T: + // The type of value returned by the asynchronous operation. + // + // Returns: + // An object that tracks the completion of the async operation, and allows for later + // synchronous blocking of the main thread for completion if necessary. + // + // Remarks: + // Exceptions thrown by the delegate are captured by the returned Microsoft.VisualStudio.Threading.JoinableTask. + // When the delegate resumes from a yielding await, the default behavior is to resume + // in its original context as an ordinary async method execution would. For example, + // if the caller was on the main thread, execution resumes after an await on the + // main thread; but if it started on a threadpool thread it resumes on a threadpool + // thread. + + public static JoinableTask RunAsync(Func> asyncMethod) => JoinableTaskContext.Factory.RunAsync(asyncMethod); + + + } +} diff --git a/src/Comet/HotReloadHelper.cs b/src/Comet/HotReloadHelper.cs index ebbd15fc..88fb70f8 100644 --- a/src/Comet/HotReloadHelper.cs +++ b/src/Comet/HotReloadHelper.cs @@ -106,11 +106,13 @@ public static void RegisterReplacedView(string oldViewType, Type newViewType) } } - public static void TriggerReload() + public static async void TriggerReload() { var roots = View.ActiveViews.Where (x => x.Parent == null).ToList(); + + await ThreadHelper.SwitchToMainThreadAsync(); foreach(var view in roots) { - Device.InvokeOnMainThread (view.Reload); + view.Reload(); } } } diff --git a/tests/Comet.Tests/UI.cs b/tests/Comet.Tests/UI.cs index fdad0cc7..40ae3056 100644 --- a/tests/Comet.Tests/UI.cs +++ b/tests/Comet.Tests/UI.cs @@ -22,8 +22,8 @@ public static void Init () Registrar.Handlers.Register (); Registrar.Handlers.Register (); Registrar.Handlers.Register (); - - Device.PerformInvokeOnMainThread = (a) => a (); + + ThreadHelper.JoinableTaskContext = new Microsoft.VisualStudio.Threading.JoinableTaskContext(); HotReloadHelper.IsEnabled = true; }