Browse Source

Ensure correct thread for AvaloniaProperty access.

Previously we ensured that `AvaloniaObject.SetValue` was run on the main
thread. This makes sure that `GetValue`, `IsSet` and `ClearValue` are
also run on the main thread. Unit testing this turned out to be more
complicated than expected, because `Dispatcher` keeps a hold of a
reference to the first `IPlatformThreadingInterface` it sees, so made
`UnitTestApplication` able to notify `Dispatcher` that it should update
its services.
pull/909/head
Steven Kirk 9 years ago
parent
commit
a46be4e200
  1. 2
      Avalonia.v3.ncrunchsolution
  2. 6
      src/Avalonia.Base/AvaloniaObject.cs
  3. 5
      src/Avalonia.Base/IPriorityValueOwner.cs
  4. 2
      src/Avalonia.Base/PriorityBindingEntry.cs
  5. 18
      src/Avalonia.Base/PriorityLevel.cs
  6. 18
      src/Avalonia.Base/PriorityValue.cs
  7. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  8. 40
      src/Avalonia.Base/Threading/Dispatcher.cs
  9. 13
      src/Avalonia.Base/Threading/JobRunner.cs
  10. 21
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  11. 12
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  12. 202
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs
  13. 6
      tests/Avalonia.UnitTests/TestServices.cs
  14. 11
      tests/Avalonia.UnitTests/UnitTestApplication.cs

2
Avalonia.v3.ncrunchsolution

@ -3,7 +3,7 @@
<AdditionalFilesToIncludeForSolution>
<Value>tests\TestFiles\**.*</Value>
</AdditionalFilesToIncludeForSolution>
<AllowParallelTestExecution>False</AllowParallelTestExecution>
<AllowParallelTestExecution>True</AllowParallelTestExecution>
<ProjectConfigStoragePathRelativeToSolutionDir>.ncrunch</ProjectConfigStoragePathRelativeToSolutionDir>
<SolutionConfigured>True</SolutionConfigured>
</Settings>

6
src/Avalonia.Base/AvaloniaObject.cs

@ -181,6 +181,7 @@ namespace Avalonia
public void ClearValue(AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
SetValue(property, AvaloniaProperty.UnsetValue);
}
@ -193,6 +194,7 @@ namespace Avalonia
public object GetValue(AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
if (property.IsDirect)
{
@ -234,7 +236,8 @@ namespace Avalonia
public bool IsSet(AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
PriorityValue value;
if (_values.TryGetValue(property, out value))
@ -332,6 +335,7 @@ namespace Avalonia
}
subscription = source
.Do(_ => VerifyAccess())
.Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => _directBindings.Remove(subscription))
.Subscribe(x => SetDirectValue(property, x));

5
src/Avalonia.Base/IPriorityValueOwner.cs

@ -25,5 +25,10 @@ namespace Avalonia
/// <param name="sender">The source of the change.</param>
/// <param name="notification">The notification.</param>
void BindingNotificationReceived(PriorityValue sender, BindingNotification notification);
/// <summary>
/// Ensures that the current thread is the UI thread.
/// </summary>
void VerifyAccess();
}
}

2
src/Avalonia.Base/PriorityBindingEntry.cs

@ -92,6 +92,8 @@ namespace Avalonia
private void ValueChanged(object value)
{
_owner.Owner.Owner?.VerifyAccess();
var notification = value as BindingNotification;
if (notification != null)

18
src/Avalonia.Base/PriorityLevel.cs

@ -33,7 +33,6 @@ namespace Avalonia
/// </remarks>
internal class PriorityLevel
{
private PriorityValue _owner;
private object _directValue;
private int _nextIndex;
@ -48,13 +47,18 @@ namespace Avalonia
{
Contract.Requires<ArgumentNullException>(owner != null);
_owner = owner;
Owner = owner;
Priority = priority;
Value = _directValue = AvaloniaProperty.UnsetValue;
ActiveBindingIndex = -1;
Bindings = new LinkedList<PriorityBindingEntry>();
}
/// <summary>
/// Gets the owner of the level.
/// </summary>
public PriorityValue Owner { get; }
/// <summary>
/// Gets the priority of this level.
/// </summary>
@ -73,7 +77,7 @@ namespace Avalonia
set
{
Value = _directValue = value;
_owner.LevelValueChanged(this);
Owner.LevelValueChanged(this);
}
}
@ -131,7 +135,7 @@ namespace Avalonia
{
Value = entry.Value;
ActiveBindingIndex = entry.Index;
_owner.LevelValueChanged(this);
Owner.LevelValueChanged(this);
}
else
{
@ -161,7 +165,7 @@ namespace Avalonia
/// <param name="error">The error.</param>
public void Error(PriorityBindingEntry entry, BindingNotification error)
{
_owner.LevelError(this, error);
Owner.LevelError(this, error);
}
/// <summary>
@ -175,14 +179,14 @@ namespace Avalonia
{
Value = binding.Value;
ActiveBindingIndex = binding.Index;
_owner.LevelValueChanged(this);
Owner.LevelValueChanged(this);
return;
}
}
Value = DirectValue;
ActiveBindingIndex = -1;
_owner.LevelValueChanged(this);
Owner.LevelValueChanged(this);
}
}
}

18
src/Avalonia.Base/PriorityValue.cs

@ -26,7 +26,6 @@ namespace Avalonia
/// </remarks>
internal class PriorityValue
{
private readonly IPriorityValueOwner _owner;
private readonly Type _valueType;
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private object _value;
@ -45,7 +44,7 @@ namespace Avalonia
Type valueType,
Func<object, object> validate = null)
{
_owner = owner;
Owner = owner;
Property = property;
_valueType = valueType;
_value = AvaloniaProperty.UnsetValue;
@ -53,6 +52,11 @@ namespace Avalonia
_validate = validate;
}
/// <summary>
/// Gets the owner of the value.
/// </summary>
public IPriorityValueOwner Owner { get; }
/// <summary>
/// Gets the property that the value represents.
/// </summary>
@ -188,9 +192,9 @@ namespace Avalonia
Logger.Log(
LogEventLevel.Error,
LogArea.Binding,
_owner,
Owner,
"Error in binding to {Target}.{Property}: {Message}",
_owner,
Owner,
Property,
error.Error.Message);
}
@ -264,19 +268,19 @@ namespace Avalonia
if (notification == null || notification.HasValue)
{
_owner?.Changed(this, old, _value);
Owner?.Changed(this, old, _value);
}
if (notification != null)
{
_owner?.BindingNotificationReceived(this, notification);
Owner?.BindingNotificationReceived(this, notification);
}
}
else
{
Logger.Error(
LogArea.Binding,
_owner,
Owner,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
Property.Name,
_valueType,

1
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -6,4 +6,5 @@ using System.Runtime.CompilerServices;
[assembly: AssemblyTitle("Avalonia.Base")]
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

40
src/Avalonia.Base/Threading/Dispatcher.cs

@ -17,8 +17,8 @@ namespace Avalonia.Threading
/// </remarks>
public class Dispatcher
{
private readonly IPlatformThreadingInterface _platform;
private readonly JobRunner _jobRunner;
private IPlatformThreadingInterface _platform;
public static Dispatcher UIThread { get; } =
new Dispatcher(AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>());
@ -26,22 +26,31 @@ namespace Avalonia.Threading
public Dispatcher(IPlatformThreadingInterface platform)
{
_platform = platform;
if(_platform == null)
//TODO: Unit test mode, fix that somehow
return;
_jobRunner = new JobRunner(platform);
_platform.Signaled += _jobRunner.RunJobs;
if (_platform != null)
{
_platform.Signaled += _jobRunner.RunJobs;
}
}
/// <summary>
/// Checks that the current thread is the UI thread.
/// </summary>
public bool CheckAccess() => _platform?.CurrentThreadIsLoopThread ?? true;
/// <summary>
/// Checks that the current thread is the UI thread and throws if not.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The current thread is not the UI thread.
/// </exception>
public void VerifyAccess()
{
if (!CheckAccess())
throw new InvalidOperationException("Call from invalid thread");
}
/// <summary>
/// Runs the dispatcher's main loop.
/// </summary>
@ -83,5 +92,24 @@ namespace Avalonia.Threading
{
_jobRunner?.Post(action, priority);
}
/// <summary>
/// Allows unit tests to change the platform threading interface.
/// </summary>
internal void UpdateServices()
{
if (_platform != null)
{
_platform.Signaled -= _jobRunner.RunJobs;
}
_platform = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>();
_jobRunner.UpdateServices();
if (_platform != null)
{
_platform.Signaled += _jobRunner.RunJobs;
}
}
}
}

13
src/Avalonia.Base/Threading/JobRunner.cs

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Platform;
@ -14,8 +13,8 @@ namespace Avalonia.Threading
/// </summary>
internal class JobRunner
{
private readonly IPlatformThreadingInterface _platform;
private readonly Queue<Job> _queue = new Queue<Job>();
private IPlatformThreadingInterface _platform;
public JobRunner(IPlatformThreadingInterface platform)
{
@ -82,6 +81,14 @@ namespace Avalonia.Threading
AddJob(new Job(action, priority, true));
}
/// <summary>
/// Allows unit tests to change the platform threading interface.
/// </summary>
internal void UpdateServices()
{
_platform = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>();
}
private void AddJob(Job job)
{
var needWake = false;
@ -91,7 +98,7 @@ namespace Avalonia.Threading
_queue.Enqueue(job);
}
if (needWake)
_platform.Signal();
_platform?.Signal();
}
/// <summary>

21
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@ -90,6 +90,7 @@
<Otherwise />
</Choose>
<ItemGroup>
<Compile Include="AvaloniaObjectTests_Threading.cs" />
<Compile Include="AvaloniaObjectTests_DataValidation.cs" />
<Compile Include="Collections\CollectionChangedTracker.cs" />
<Compile Include="Collections\AvaloniaDictionaryTests.cs" />
@ -124,6 +125,26 @@
<Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
<Name>Avalonia.Base</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj">
<Project>{d2221c82-4a25-4583-9b43-d791e3f6820c}</Project>
<Name>Avalonia.Controls</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj">
<Project>{62024b2d-53eb-4638-b26b-85eeaa54866e}</Project>
<Name>Avalonia.Input</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj">
<Project>{42472427-4774-4c81-8aff-9f27b8e31721}</Project>
<Name>Avalonia.Layout</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj">
<Project>{f1baa01a-f176-4c6a-b39d-5b40bb1b148f}</Project>
<Name>Avalonia.Styling</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj">
<Project>{eb582467-6abb-43a1-b052-e981ba910e3a}</Project>
<Name>Avalonia.Visuals</Name>
</ProjectReference>
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj">
<Project>{88060192-33d5-4932-b0f9-8bd2763e857d}</Project>
<Name>Avalonia.UnitTests</Name>

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

@ -365,7 +365,7 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public async void Bind_With_Scheduler_Executes_On_Scheduler()
public async Task Bind_With_Scheduler_Executes_On_Scheduler()
{
var target = new Class1();
var source = new Subject<object>();
@ -375,16 +375,16 @@ namespace Avalonia.Base.UnitTests
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);
var services = new TestServices(
scheduler: AvaloniaScheduler.Instance,
threadingInterface: threadingInterfaceMock.Object);
using (UnitTestApplication.Start(services))
{
target.Bind(Class1.QuxProperty, source);
await Task.Run(() => source.OnNext(6.7));
}
}
/// <summary>

202
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs

@ -0,0 +1,202 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_Threading
{
[Fact]
public void StyledProperty_GetValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface())))
{
var target = new Class1();
Assert.Throws<InvalidOperationException>(() => target.GetValue(Class1.StyledProperty));
}
}
[Fact]
public void StyledProperty_SetValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface())))
{
var target = new Class1();
Assert.Throws<InvalidOperationException>(() => target.SetValue(Class1.StyledProperty, "foo"));
}
}
[Fact]
public void Setting_StyledProperty_Binding_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface())))
{
var target = new Class1();
Assert.Throws<InvalidOperationException>(() =>
target.Bind(
Class1.StyledProperty,
new BehaviorSubject<string>("foo")));
}
}
[Fact]
public void StyledProperty_Binding_Producing_Value_Should_Throw()
{
var ti = new ThreadingInterface(true);
using (UnitTestApplication.Start(new TestServices(threadingInterface: ti)))
{
var target = new Class1();
var source = new BehaviorSubject<string>("foo");
target.Bind(Class1.StyledProperty, source);
ti.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() => source.OnNext("bar"));
}
}
[Fact]
public void StyledProperty_ClearValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface())))
{
var target = new Class1();
Assert.Throws<InvalidOperationException>(() => target.ClearValue(Class1.StyledProperty));
}
}
[Fact]
public void StyledProperty_IsSet_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface())))
{
var target = new Class1();
Assert.Throws<InvalidOperationException>(() => target.IsSet(Class1.StyledProperty));
}
}
[Fact]
public void DirectProperty_GetValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface())))
{
var target = new Class1();
Assert.Throws<InvalidOperationException>(() => target.GetValue(Class1.DirectProperty));
}
}
[Fact]
public void DirectProperty_SetValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface())))
{
var target = new Class1();
Assert.Throws<InvalidOperationException>(() => target.SetValue(Class1.DirectProperty, "foo"));
}
}
[Fact]
public void Setting_DirectProperty_Binding_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface())))
{
var target = new Class1();
Assert.Throws<InvalidOperationException>(() =>
target.Bind(
Class1.DirectProperty,
new BehaviorSubject<string>("foo")));
}
}
[Fact]
public void DirectProperty_Binding_Producing_Value_Should_Throw()
{
var ti = new ThreadingInterface(true);
using (UnitTestApplication.Start(new TestServices(threadingInterface: ti)))
{
var target = new Class1();
var source = new BehaviorSubject<string>("foo");
target.Bind(Class1.DirectProperty, source);
ti.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() => source.OnNext("bar"));
}
}
[Fact]
public void DirectProperty_ClearValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface())))
{
var target = new Class1();
Assert.Throws<InvalidOperationException>(() => target.ClearValue(Class1.DirectProperty));
}
}
[Fact]
public void DirectProperty_IsSet_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(threadingInterface: new ThreadingInterface())))
{
var target = new Class1();
Assert.Throws<InvalidOperationException>(() => target.IsSet(Class1.DirectProperty));
}
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> StyledProperty =
AvaloniaProperty.Register<Class1, string>("Foo", "foodefault");
public static readonly DirectProperty<Class1, string> DirectProperty =
AvaloniaProperty.RegisterDirect<Class1, string>("Qux", _ => null, (o, v) => { });
}
private class ThreadingInterface : IPlatformThreadingInterface
{
public ThreadingInterface(bool isLoopThread = false)
{
CurrentThreadIsLoopThread = isLoopThread;
}
public bool CurrentThreadIsLoopThread { get; set; }
public event Action Signaled;
public void RunLoop(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public void Signal()
{
throw new NotImplementedException();
}
public IDisposable StartTimer(TimeSpan interval, Action tick)
{
throw new NotImplementedException();
}
}
}
}

6
tests/Avalonia.UnitTests/TestServices.cs

@ -12,6 +12,7 @@ using Avalonia.Shared.PlatformSupport;
using Avalonia.Styling;
using Avalonia.Themes.Default;
using Avalonia.Rendering;
using System.Reactive.Concurrency;
namespace Avalonia.UnitTests
{
@ -63,6 +64,7 @@ namespace Avalonia.UnitTests
IRenderer renderer = null,
IPlatformRenderInterface renderInterface = null,
IRenderLoop renderLoop = null,
IScheduler scheduler = null,
IStandardCursorFactory standardCursorFactory = null,
IStyler styler = null,
Func<Styles> theme = null,
@ -79,6 +81,7 @@ namespace Avalonia.UnitTests
Renderer = renderer;
RenderInterface = renderInterface;
RenderLoop = renderLoop;
Scheduler = scheduler;
StandardCursorFactory = standardCursorFactory;
Styler = styler;
Theme = theme;
@ -96,6 +99,7 @@ namespace Avalonia.UnitTests
public IRenderer Renderer { get; }
public IPlatformRenderInterface RenderInterface { get; }
public IRenderLoop RenderLoop { get; }
public IScheduler Scheduler { get; }
public IStandardCursorFactory StandardCursorFactory { get; }
public IStyler Styler { get; }
public Func<Styles> Theme { get; }
@ -113,6 +117,7 @@ namespace Avalonia.UnitTests
IRenderer renderer = null,
IPlatformRenderInterface renderInterface = null,
IRenderLoop renderLoop = null,
IScheduler scheduler = null,
IStandardCursorFactory standardCursorFactory = null,
IStyler styler = null,
Func<Styles> theme = null,
@ -130,6 +135,7 @@ namespace Avalonia.UnitTests
renderer: renderer ?? Renderer,
renderInterface: renderInterface ?? RenderInterface,
renderLoop: renderLoop ?? RenderLoop,
scheduler: scheduler ?? Scheduler,
standardCursorFactory: standardCursorFactory ?? StandardCursorFactory,
styler: styler ?? Styler,
theme: theme ?? Theme,

11
tests/Avalonia.UnitTests/UnitTestApplication.cs

@ -8,6 +8,9 @@ using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Controls;
using Avalonia.Rendering;
using Avalonia.Threading;
using System.Reactive.Disposables;
using System.Reactive.Concurrency;
namespace Avalonia.UnitTests
{
@ -30,7 +33,12 @@ namespace Avalonia.UnitTests
var scope = AvaloniaLocator.EnterScope();
var app = new UnitTestApplication(services);
AvaloniaLocator.CurrentMutable.BindToSelf<Application>(app);
return scope;
Dispatcher.UIThread.UpdateServices();
return Disposable.Create(() =>
{
scope.Dispose();
Dispatcher.UIThread.UpdateServices();
});
}
public override void RegisterServices()
@ -47,6 +55,7 @@ namespace Avalonia.UnitTests
.Bind<IPlatformRenderInterface>().ToConstant(Services.RenderInterface)
.Bind<IRenderLoop>().ToConstant(Services.RenderLoop)
.Bind<IPlatformThreadingInterface>().ToConstant(Services.ThreadingInterface)
.Bind<IScheduler>().ToConstant(Services.Scheduler)
.Bind<IStandardCursorFactory>().ToConstant(Services.StandardCursorFactory)
.Bind<IStyler>().ToConstant(Services.Styler)
.Bind<IWindowingPlatform>().ToConstant(Services.WindowingPlatform)

Loading…
Cancel
Save