From 428466ced3d97b37b737139505e8568c021f5599 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 30 Oct 2016 16:21:16 -0500 Subject: [PATCH 1/7] Fixed bug in scheduler and runtime platform. --- src/Avalonia.Base/Threading/AvaloniaScheduler.cs | 8 +------- src/Shared/PlatformSupport/StandardRuntimePlatform.cs | 5 +---- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs index de32057a00..43815b4ebc 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs @@ -26,13 +26,7 @@ namespace Avalonia.Threading /// public override IDisposable Schedule(TState state, TimeSpan dueTime, Func action) { - return DispatcherTimer.Run( - () => - { - action(this, state); - return false; - }, - dueTime); + return DispatcherTimer.RunOnce(() => action(this, state), dueTime); } } } diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index e5ede1c6b4..5c02be7d5b 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -16,10 +16,7 @@ namespace Avalonia.Shared.PlatformSupport public void PostThreadPoolItem(Action cb) => ThreadPool.UnsafeQueueUserWorkItem(_ => cb(), null); public IDisposable StartSystemTimer(TimeSpan interval, Action tick) { - var timer = new Timer(delegate - { - - }, null, interval, interval); + var timer = new Timer(_ => tick(), null, interval, interval); return Disposable.Create(() => timer.Dispose()); } From 298fa627f8d1fdd8d01f71362afaf19008ce2759 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 30 Oct 2016 16:55:47 -0500 Subject: [PATCH 2/7] Implemented binding scheduling via service injection. --- src/Avalonia.Base/AvaloniaObject.cs | 7 +++++++ src/Avalonia.Controls/Application.cs | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 48e937d6b2..5609fde7e9 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -12,6 +12,7 @@ using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.Threading; using Avalonia.Utilities; +using System.Reactive.Concurrency; namespace Avalonia { @@ -304,6 +305,12 @@ namespace Avalonia VerifyAccess(); + var scheduler = AvaloniaLocator.Current.GetService(); + if (scheduler != null) + { + source = source.ObserveOn(scheduler); + } + if (property.IsDirect) { if (property.IsReadOnly) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 326556f629..3d13608226 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -11,6 +11,7 @@ using Avalonia.Layout; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Threading; +using System.Reactive.Concurrency; namespace Avalonia { @@ -175,7 +176,8 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(_styler) .Bind().ToSingleton() - .Bind().ToConstant(this); + .Bind().ToConstant(this) + .Bind().ToConstant(AvaloniaScheduler.Instance); } } } From 6da1d620c9377d4624d888fa362a7b26311bd63b Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 30 Oct 2016 22:54:32 -0500 Subject: [PATCH 3/7] Added test to ensure that the IObservable subscription is on the AvaloniaScheduler when the AvaloniaScheduler is bound in the AvaloniaLocator. --- .../AvaloniaObjectTests_Binding.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 66fe3c7767..c72432029a 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -11,6 +11,13 @@ using Avalonia.Data; using Avalonia.Logging; using Avalonia.UnitTests; using Xunit; +using System.Threading.Tasks; +using Avalonia.Platform; +using System.Threading; +using Moq; +using System.Reactive.Disposables; +using System.Reactive.Concurrency; +using Avalonia.Threading; namespace Avalonia.Base.UnitTests { @@ -356,6 +363,39 @@ namespace Avalonia.Base.UnitTests Assert.True(called); } } + + [Fact] + public async void Bind_With_Scheduler_Executes_On_Scheduler() + { + var target = new Class1(); + var source = new Subject(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + var calledThreadingInterface = false; + + var threadingInterfaceMock = new Mock(); + threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); + threadingInterfaceMock.Setup(mock => mock.StartTimer(TimeSpan.Zero, It.IsAny())) + .Returns((ts, act) => + { + act(); + calledThreadingInterface = true; + return Disposable.Empty; + }); + + using (AvaloniaLocator.EnterScope()) + { + AvaloniaLocator.CurrentMutable.Bind().ToConstant(threadingInterfaceMock.Object); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(AvaloniaScheduler.Instance); + + target.Bind(Class1.QuxProperty, source); + + await Task.Run(() => source.OnNext(6.7)); + + Assert.True(calledThreadingInterface); + } + + } /// /// Returns an observable that returns a single value but does not complete. From 428229e9b0e4c17b26083ef0442666be312bf800 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 1 Nov 2016 23:34:27 -0500 Subject: [PATCH 4/7] Optimized the "zero wait time" path in the scheduler and updated the code to always use a scheduler, either the ImmediateScheduler (same as the old default behavior) or the registered scheduler. --- src/Avalonia.Base/AvaloniaObject.cs | 12 ++++++----- .../Threading/AvaloniaScheduler.cs | 20 ++++++++++++++++++- .../AvaloniaObjectTests_Binding.cs | 10 ---------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 5609fde7e9..72ff5187fa 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -305,10 +305,12 @@ namespace Avalonia VerifyAccess(); - var scheduler = AvaloniaLocator.Current.GetService(); - if (scheduler != null) + var description = GetDescription(source); + + if (priority == BindingPriority.LocalValue) { - source = source.ObserveOn(scheduler); + var scheduler = AvaloniaLocator.Current.GetService() ?? ImmediateScheduler.Instance; + source = source.ObserveOn(scheduler); } if (property.IsDirect) @@ -323,7 +325,7 @@ namespace Avalonia this, "Bound {Property} to {Binding} with priority LocalValue", property, - GetDescription(source)); + description); IDisposable subscription = null; @@ -365,7 +367,7 @@ namespace Avalonia this, "Bound {Property} to {Binding} with priority {Priority}", property, - GetDescription(source), + description, priority); return v.Add(source, (int)priority); diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs index 43815b4ebc..d9361fe7f0 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Concurrency; +using System.Reactive.Disposables; namespace Avalonia.Threading { @@ -26,7 +27,24 @@ namespace Avalonia.Threading /// public override IDisposable Schedule(TState state, TimeSpan dueTime, Func action) { - return DispatcherTimer.RunOnce(() => action(this, state), dueTime); + var composite = new CompositeDisposable(2); + if (dueTime == TimeSpan.Zero) + { + var cancellation = new CancellationDisposable(); + Dispatcher.UIThread.InvokeAsync(() => + { + if (!cancellation.Token.IsCancellationRequested) + { + composite.Add(action(this, state)); + } + }, DispatcherPriority.DataBind); + composite.Add(cancellation); + } + else + { + composite.Add(DispatcherTimer.RunOnce(() => composite.Add(action(this, state)), dueTime)); + } + return composite; } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index c72432029a..5e286305d2 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -370,18 +370,10 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); var source = new Subject(); var currentThreadId = Thread.CurrentThread.ManagedThreadId; - var calledThreadingInterface = false; var threadingInterfaceMock = new Mock(); threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); - threadingInterfaceMock.Setup(mock => mock.StartTimer(TimeSpan.Zero, It.IsAny())) - .Returns((ts, act) => - { - act(); - calledThreadingInterface = true; - return Disposable.Empty; - }); using (AvaloniaLocator.EnterScope()) { @@ -391,8 +383,6 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.QuxProperty, source); await Task.Run(() => source.OnNext(6.7)); - - Assert.True(calledThreadingInterface); } } From 4a8470d8a6886b277594ccd73a26fecb4edd5615 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 2 Nov 2016 14:06:47 -0500 Subject: [PATCH 5/7] Updated fast-path of scheduler to call directly if updates are on the UI thread instead of scheduling on the Dispatcher. --- .../Threading/AvaloniaScheduler.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs index d9361fe7f0..f9d67470c1 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs @@ -30,15 +30,22 @@ namespace Avalonia.Threading var composite = new CompositeDisposable(2); if (dueTime == TimeSpan.Zero) { - var cancellation = new CancellationDisposable(); - Dispatcher.UIThread.InvokeAsync(() => + if (!Dispatcher.UIThread.CheckAccess()) { - if (!cancellation.Token.IsCancellationRequested) + var cancellation = new CancellationDisposable(); + Dispatcher.UIThread.InvokeAsync(() => { - composite.Add(action(this, state)); - } - }, DispatcherPriority.DataBind); - composite.Add(cancellation); + if (!cancellation.Token.IsCancellationRequested) + { + composite.Add(action(this, state)); + } + }, DispatcherPriority.DataBind); + composite.Add(cancellation); + } + else + { + return action(this, state); + } } else { From 4c179634c1765fd3c23bd4862da6be00f7a4fd7e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 3 Nov 2016 21:10:16 +0100 Subject: [PATCH 6/7] Added a test for scheduler bindings. Added a property which is updated on a b/g thead to BindingTest. --- samples/BindingTest/MainWindow.xaml | 4 ++++ .../ViewModels/MainWindowViewModel.cs | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/samples/BindingTest/MainWindow.xaml b/samples/BindingTest/MainWindow.xaml index 02c364346d..95f671fd84 100644 --- a/samples/BindingTest/MainWindow.xaml +++ b/samples/BindingTest/MainWindow.xaml @@ -41,6 +41,10 @@ + + + + diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingTest/ViewModels/MainWindowViewModel.cs index 94f7ff595a..4b58bf2279 100644 --- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs @@ -3,6 +3,8 @@ using System.Collections.ObjectModel; using System.Linq; using ReactiveUI; using System.Reactive.Linq; +using System.Threading.Tasks; +using System.Threading; namespace BindingTest.ViewModels { @@ -12,6 +14,7 @@ namespace BindingTest.ViewModels private double _doubleValue = 5.0; private string _stringValue = "Simple Binding"; private bool _booleanFlag = false; + private string _currentTime; public MainWindowViewModel() { @@ -37,6 +40,15 @@ namespace BindingTest.ViewModels BooleanFlag = !BooleanFlag; StringValue = param.ToString(); }); + + Task.Run(() => + { + while (true) + { + CurrentTime = DateTimeOffset.Now.ToString(); + Thread.Sleep(1000); + } + }); } public ObservableCollection Items { get; } @@ -67,6 +79,12 @@ namespace BindingTest.ViewModels set { this.RaiseAndSetIfChanged(ref _booleanFlag, value); } } + public string CurrentTime + { + get { return _currentTime; } + private set { this.RaiseAndSetIfChanged(ref _currentTime, value); } + } + public ReactiveCommand StringValueCommand { get; } public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel(); From 104e0eb0789b50c0001e5bb7e217b493c17469e0 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 3 Nov 2016 15:29:25 -0500 Subject: [PATCH 7/7] Removed the priority condition for using the scheduler. --- src/Avalonia.Base/AvaloniaObject.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 72ff5187fa..409abfe8fa 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -307,11 +307,8 @@ namespace Avalonia var description = GetDescription(source); - if (priority == BindingPriority.LocalValue) - { - var scheduler = AvaloniaLocator.Current.GetService() ?? ImmediateScheduler.Instance; - source = source.ObserveOn(scheduler); - } + var scheduler = AvaloniaLocator.Current.GetService() ?? ImmediateScheduler.Instance; + source = source.ObserveOn(scheduler); if (property.IsDirect) {