Browse Source

Merge pull request #790 from jkoritzinsky/UIThread-Scheduling

Schedule all binding updates on Dispatcher/UI thread
pull/800/head
Jeremy Koritzinsky 10 years ago
committed by GitHub
parent
commit
3f5d29101d
  1. 4
      samples/BindingTest/MainWindow.xaml
  2. 18
      samples/BindingTest/ViewModels/MainWindowViewModel.cs
  3. 10
      src/Avalonia.Base/AvaloniaObject.cs
  4. 31
      src/Avalonia.Base/Threading/AvaloniaScheduler.cs
  5. 4
      src/Avalonia.Controls/Application.cs
  6. 5
      src/Shared/PlatformSupport/StandardRuntimePlatform.cs
  7. 30
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

4
samples/BindingTest/MainWindow.xaml

@ -41,6 +41,10 @@
<TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True" <TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True"
Text="{Binding #first.Text, Mode=TwoWay}"/> Text="{Binding #first.Text, Mode=TwoWay}"/>
</StackPanel> </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>
</StackPanel> </StackPanel>
</TabItem> </TabItem>

18
samples/BindingTest/ViewModels/MainWindowViewModel.cs

@ -3,6 +3,8 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using ReactiveUI; using ReactiveUI;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Threading;
namespace BindingTest.ViewModels namespace BindingTest.ViewModels
{ {
@ -12,6 +14,7 @@ namespace BindingTest.ViewModels
private double _doubleValue = 5.0; private double _doubleValue = 5.0;
private string _stringValue = "Simple Binding"; private string _stringValue = "Simple Binding";
private bool _booleanFlag = false; private bool _booleanFlag = false;
private string _currentTime;
public MainWindowViewModel() public MainWindowViewModel()
{ {
@ -37,6 +40,15 @@ namespace BindingTest.ViewModels
BooleanFlag = !BooleanFlag; BooleanFlag = !BooleanFlag;
StringValue = param.ToString(); StringValue = param.ToString();
}); });
Task.Run(() =>
{
while (true)
{
CurrentTime = DateTimeOffset.Now.ToString();
Thread.Sleep(1000);
}
});
} }
public ObservableCollection<TestItem> Items { get; } public ObservableCollection<TestItem> Items { get; }
@ -67,6 +79,12 @@ namespace BindingTest.ViewModels
set { this.RaiseAndSetIfChanged(ref _booleanFlag, value); } 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 ReactiveCommand<object> StringValueCommand { get; }
public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel(); public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel();

10
src/Avalonia.Base/AvaloniaObject.cs

@ -12,6 +12,7 @@ using Avalonia.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities; using Avalonia.Utilities;
using System.Reactive.Concurrency;
namespace Avalonia namespace Avalonia
{ {
@ -304,6 +305,11 @@ namespace Avalonia
VerifyAccess(); VerifyAccess();
var description = GetDescription(source);
var scheduler = AvaloniaLocator.Current.GetService<IScheduler>() ?? ImmediateScheduler.Instance;
source = source.ObserveOn(scheduler);
if (property.IsDirect) if (property.IsDirect)
{ {
if (property.IsReadOnly) if (property.IsReadOnly)
@ -316,7 +322,7 @@ namespace Avalonia
this, this,
"Bound {Property} to {Binding} with priority LocalValue", "Bound {Property} to {Binding} with priority LocalValue",
property, property,
GetDescription(source)); description);
IDisposable subscription = null; IDisposable subscription = null;
@ -358,7 +364,7 @@ namespace Avalonia
this, this,
"Bound {Property} to {Binding} with priority {Priority}", "Bound {Property} to {Binding} with priority {Priority}",
property, property,
GetDescription(source), description,
priority); priority);
return v.Add(source, (int)priority); return v.Add(source, (int)priority);

31
src/Avalonia.Base/Threading/AvaloniaScheduler.cs

@ -3,6 +3,7 @@
using System; using System;
using System.Reactive.Concurrency; using System.Reactive.Concurrency;
using System.Reactive.Disposables;
namespace Avalonia.Threading namespace Avalonia.Threading
{ {
@ -26,13 +27,31 @@ namespace Avalonia.Threading
/// <inheritdoc/> /// <inheritdoc/>
public override IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action) 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); var cancellation = new CancellationDisposable();
return false; Dispatcher.UIThread.InvokeAsync(() =>
}, {
dueTime); 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;
} }
} }
} }

4
src/Avalonia.Controls/Application.cs

@ -11,6 +11,7 @@ using Avalonia.Layout;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading; using Avalonia.Threading;
using System.Reactive.Concurrency;
namespace Avalonia namespace Avalonia
{ {
@ -175,7 +176,8 @@ namespace Avalonia
.Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>() .Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>()
.Bind<IStyler>().ToConstant(_styler) .Bind<IStyler>().ToConstant(_styler)
.Bind<ILayoutManager>().ToSingleton<LayoutManager>() .Bind<ILayoutManager>().ToSingleton<LayoutManager>()
.Bind<IApplicationLifecycle>().ToConstant(this); .Bind<IApplicationLifecycle>().ToConstant(this)
.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance);
} }
} }
} }

5
src/Shared/PlatformSupport/StandardRuntimePlatform.cs

@ -16,10 +16,7 @@ namespace Avalonia.Shared.PlatformSupport
public void PostThreadPoolItem(Action cb) => ThreadPool.UnsafeQueueUserWorkItem(_ => cb(), null); public void PostThreadPoolItem(Action cb) => ThreadPool.UnsafeQueueUserWorkItem(_ => cb(), null);
public IDisposable StartSystemTimer(TimeSpan interval, Action tick) public IDisposable StartSystemTimer(TimeSpan interval, Action tick)
{ {
var timer = new Timer(delegate var timer = new Timer(_ => tick(), null, interval, interval);
{
}, null, interval, interval);
return Disposable.Create(() => timer.Dispose()); return Disposable.Create(() => timer.Dispose());
} }

30
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -11,6 +11,13 @@ using Avalonia.Data;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; 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 namespace Avalonia.Base.UnitTests
{ {
@ -356,6 +363,29 @@ namespace Avalonia.Base.UnitTests
Assert.True(called); 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> /// <summary>
/// Returns an observable that returns a single value but does not complete. /// Returns an observable that returns a single value but does not complete.

Loading…
Cancel
Save