Browse Source

Merge pull request #790 from jkoritzinsky/UIThread-Scheduling

Schedule all binding updates on Dispatcher/UI thread
Jeremy Koritzinsky 9 years ago
parent
commit
3f5d29101d

+ 4 - 0
samples/BindingTest/MainWindow.xaml

@@ -41,6 +41,10 @@
             <TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True" 
                      Text="{Binding #first.Text, Mode=TwoWay}"/>
           </StackPanel>
+          <StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
+            <TextBlock FontSize="16" Text="Scheduler"/>
+            <TextBox Watermark="Background Thread" Text="{Binding CurrentTime, Mode=OneWay}"/>
+          </StackPanel>
         </StackPanel>
       </StackPanel>
     </TabItem>

+ 18 - 0
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<TestItem> 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<object> StringValueCommand { get; }
 
         public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel();

+ 8 - 2
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,11 @@ namespace Avalonia
 
             VerifyAccess();
 
+            var description = GetDescription(source);
+
+            var scheduler = AvaloniaLocator.Current.GetService<IScheduler>() ?? ImmediateScheduler.Instance;
+            source = source.ObserveOn(scheduler); 
+
             if (property.IsDirect)
             {
                 if (property.IsReadOnly)
@@ -316,7 +322,7 @@ namespace Avalonia
                     this,
                     "Bound {Property} to {Binding} with priority LocalValue", 
                     property, 
-                    GetDescription(source));
+                    description);
 
                 IDisposable subscription = null;
 
@@ -358,7 +364,7 @@ namespace Avalonia
                     this,
                     "Bound {Property} to {Binding} with priority {Priority}",
                     property,
-                    GetDescription(source),
+                    description,
                     priority);
 
                 return v.Add(source, (int)priority);

+ 25 - 6
src/Avalonia.Base/Threading/AvaloniaScheduler.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Reactive.Concurrency;
+using System.Reactive.Disposables;
 
 namespace Avalonia.Threading
 {
@@ -26,13 +27,31 @@ namespace Avalonia.Threading
         /// <inheritdoc/>
         public override IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
         {
-            return DispatcherTimer.Run(
-                () =>
+            var composite = new CompositeDisposable(2);
+            if (dueTime == TimeSpan.Zero)
+            {
+                if (!Dispatcher.UIThread.CheckAccess())
                 {
-                    action(this, state);
-                    return false;
-                },
-                dueTime);
+                    var cancellation = new CancellationDisposable();
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                    {
+                        if (!cancellation.Token.IsCancellationRequested)
+                        {
+                            composite.Add(action(this, state));
+                        }
+                    }, DispatcherPriority.DataBind);
+                    composite.Add(cancellation); 
+                }
+                else
+                {
+                    return action(this, state);
+                }
+            }
+            else
+            {
+                composite.Add(DispatcherTimer.RunOnce(() => composite.Add(action(this, state)), dueTime));
+            }
+            return composite;
         }
     }
 }

+ 3 - 1
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<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>()
                 .Bind<IStyler>().ToConstant(_styler)
                 .Bind<ILayoutManager>().ToSingleton<LayoutManager>()
-                .Bind<IApplicationLifecycle>().ToConstant(this);
+                .Bind<IApplicationLifecycle>().ToConstant(this)
+                .Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance);
         }
     }
 }

+ 1 - 4
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());
         }
 

+ 30 - 0
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,29 @@ 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<object>();
+            var currentThreadId = Thread.CurrentThread.ManagedThreadId;
+
+            var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
+            threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
+                .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
+
+            using (AvaloniaLocator.EnterScope())
+            {
+                AvaloniaLocator.CurrentMutable.Bind<IPlatformThreadingInterface>().ToConstant(threadingInterfaceMock.Object);
+                AvaloniaLocator.CurrentMutable.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance);
+
+                target.Bind(Class1.QuxProperty, source);
+
+                await Task.Run(() => source.OnNext(6.7));
+            }
+
+        }
 
         /// <summary>
         /// Returns an observable that returns a single value but does not complete.