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> <AdditionalFilesToIncludeForSolution>
<Value>tests\TestFiles\**.*</Value> <Value>tests\TestFiles\**.*</Value>
</AdditionalFilesToIncludeForSolution> </AdditionalFilesToIncludeForSolution>
<AllowParallelTestExecution>False</AllowParallelTestExecution> <AllowParallelTestExecution>True</AllowParallelTestExecution>
<ProjectConfigStoragePathRelativeToSolutionDir>.ncrunch</ProjectConfigStoragePathRelativeToSolutionDir> <ProjectConfigStoragePathRelativeToSolutionDir>.ncrunch</ProjectConfigStoragePathRelativeToSolutionDir>
<SolutionConfigured>True</SolutionConfigured> <SolutionConfigured>True</SolutionConfigured>
</Settings> </Settings>

6
src/Avalonia.Base/AvaloniaObject.cs

@ -181,6 +181,7 @@ namespace Avalonia
public void ClearValue(AvaloniaProperty property) public void ClearValue(AvaloniaProperty property)
{ {
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
SetValue(property, AvaloniaProperty.UnsetValue); SetValue(property, AvaloniaProperty.UnsetValue);
} }
@ -193,6 +194,7 @@ namespace Avalonia
public object GetValue(AvaloniaProperty property) public object GetValue(AvaloniaProperty property)
{ {
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
if (property.IsDirect) if (property.IsDirect)
{ {
@ -234,7 +236,8 @@ namespace Avalonia
public bool IsSet(AvaloniaProperty property) public bool IsSet(AvaloniaProperty property)
{ {
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
PriorityValue value; PriorityValue value;
if (_values.TryGetValue(property, out value)) if (_values.TryGetValue(property, out value))
@ -332,6 +335,7 @@ namespace Avalonia
} }
subscription = source subscription = source
.Do(_ => VerifyAccess())
.Select(x => CastOrDefault(x, property.PropertyType)) .Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => _directBindings.Remove(subscription)) .Do(_ => { }, () => _directBindings.Remove(subscription))
.Subscribe(x => SetDirectValue(property, x)); .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="sender">The source of the change.</param>
/// <param name="notification">The notification.</param> /// <param name="notification">The notification.</param>
void BindingNotificationReceived(PriorityValue sender, BindingNotification notification); 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) private void ValueChanged(object value)
{ {
_owner.Owner.Owner?.VerifyAccess();
var notification = value as BindingNotification; var notification = value as BindingNotification;
if (notification != null) if (notification != null)

18
src/Avalonia.Base/PriorityLevel.cs

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

18
src/Avalonia.Base/PriorityValue.cs

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

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

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

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

@ -17,8 +17,8 @@ namespace Avalonia.Threading
/// </remarks> /// </remarks>
public class Dispatcher public class Dispatcher
{ {
private readonly IPlatformThreadingInterface _platform;
private readonly JobRunner _jobRunner; private readonly JobRunner _jobRunner;
private IPlatformThreadingInterface _platform;
public static Dispatcher UIThread { get; } = public static Dispatcher UIThread { get; } =
new Dispatcher(AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>()); new Dispatcher(AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>());
@ -26,22 +26,31 @@ namespace Avalonia.Threading
public Dispatcher(IPlatformThreadingInterface platform) public Dispatcher(IPlatformThreadingInterface platform)
{ {
_platform = platform; _platform = platform;
if(_platform == null)
//TODO: Unit test mode, fix that somehow
return;
_jobRunner = new JobRunner(platform); _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; 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() public void VerifyAccess()
{ {
if (!CheckAccess()) if (!CheckAccess())
throw new InvalidOperationException("Call from invalid thread"); throw new InvalidOperationException("Call from invalid thread");
} }
/// <summary> /// <summary>
/// Runs the dispatcher's main loop. /// Runs the dispatcher's main loop.
/// </summary> /// </summary>
@ -83,5 +92,24 @@ namespace Avalonia.Threading
{ {
_jobRunner?.Post(action, priority); _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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Platform; using Avalonia.Platform;
@ -14,8 +13,8 @@ namespace Avalonia.Threading
/// </summary> /// </summary>
internal class JobRunner internal class JobRunner
{ {
private readonly IPlatformThreadingInterface _platform;
private readonly Queue<Job> _queue = new Queue<Job>(); private readonly Queue<Job> _queue = new Queue<Job>();
private IPlatformThreadingInterface _platform;
public JobRunner(IPlatformThreadingInterface platform) public JobRunner(IPlatformThreadingInterface platform)
{ {
@ -82,6 +81,14 @@ namespace Avalonia.Threading
AddJob(new Job(action, priority, true)); 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) private void AddJob(Job job)
{ {
var needWake = false; var needWake = false;
@ -91,7 +98,7 @@ namespace Avalonia.Threading
_queue.Enqueue(job); _queue.Enqueue(job);
} }
if (needWake) if (needWake)
_platform.Signal(); _platform?.Signal();
} }
/// <summary> /// <summary>

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

@ -90,6 +90,7 @@
<Otherwise /> <Otherwise />
</Choose> </Choose>
<ItemGroup> <ItemGroup>
<Compile Include="AvaloniaObjectTests_Threading.cs" />
<Compile Include="AvaloniaObjectTests_DataValidation.cs" /> <Compile Include="AvaloniaObjectTests_DataValidation.cs" />
<Compile Include="Collections\CollectionChangedTracker.cs" /> <Compile Include="Collections\CollectionChangedTracker.cs" />
<Compile Include="Collections\AvaloniaDictionaryTests.cs" /> <Compile Include="Collections\AvaloniaDictionaryTests.cs" />
@ -124,6 +125,26 @@
<Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project> <Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
<Name>Avalonia.Base</Name> <Name>Avalonia.Base</Name>
</ProjectReference> </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"> <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj">
<Project>{88060192-33d5-4932-b0f9-8bd2763e857d}</Project> <Project>{88060192-33d5-4932-b0f9-8bd2763e857d}</Project>
<Name>Avalonia.UnitTests</Name> <Name>Avalonia.UnitTests</Name>

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

@ -365,7 +365,7 @@ namespace Avalonia.Base.UnitTests
} }
[Fact] [Fact]
public async void Bind_With_Scheduler_Executes_On_Scheduler() public async Task Bind_With_Scheduler_Executes_On_Scheduler()
{ {
var target = new Class1(); var target = new Class1();
var source = new Subject<object>(); var source = new Subject<object>();
@ -375,16 +375,16 @@ namespace Avalonia.Base.UnitTests
threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
.Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
using (AvaloniaLocator.EnterScope()) var services = new TestServices(
{ scheduler: AvaloniaScheduler.Instance,
AvaloniaLocator.CurrentMutable.Bind<IPlatformThreadingInterface>().ToConstant(threadingInterfaceMock.Object); threadingInterface: threadingInterfaceMock.Object);
AvaloniaLocator.CurrentMutable.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance);
using (UnitTestApplication.Start(services))
{
target.Bind(Class1.QuxProperty, source); target.Bind(Class1.QuxProperty, source);
await Task.Run(() => source.OnNext(6.7)); await Task.Run(() => source.OnNext(6.7));
} }
} }
/// <summary> /// <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.Styling;
using Avalonia.Themes.Default; using Avalonia.Themes.Default;
using Avalonia.Rendering; using Avalonia.Rendering;
using System.Reactive.Concurrency;
namespace Avalonia.UnitTests namespace Avalonia.UnitTests
{ {
@ -63,6 +64,7 @@ namespace Avalonia.UnitTests
IRenderer renderer = null, IRenderer renderer = null,
IPlatformRenderInterface renderInterface = null, IPlatformRenderInterface renderInterface = null,
IRenderLoop renderLoop = null, IRenderLoop renderLoop = null,
IScheduler scheduler = null,
IStandardCursorFactory standardCursorFactory = null, IStandardCursorFactory standardCursorFactory = null,
IStyler styler = null, IStyler styler = null,
Func<Styles> theme = null, Func<Styles> theme = null,
@ -79,6 +81,7 @@ namespace Avalonia.UnitTests
Renderer = renderer; Renderer = renderer;
RenderInterface = renderInterface; RenderInterface = renderInterface;
RenderLoop = renderLoop; RenderLoop = renderLoop;
Scheduler = scheduler;
StandardCursorFactory = standardCursorFactory; StandardCursorFactory = standardCursorFactory;
Styler = styler; Styler = styler;
Theme = theme; Theme = theme;
@ -96,6 +99,7 @@ namespace Avalonia.UnitTests
public IRenderer Renderer { get; } public IRenderer Renderer { get; }
public IPlatformRenderInterface RenderInterface { get; } public IPlatformRenderInterface RenderInterface { get; }
public IRenderLoop RenderLoop { get; } public IRenderLoop RenderLoop { get; }
public IScheduler Scheduler { get; }
public IStandardCursorFactory StandardCursorFactory { get; } public IStandardCursorFactory StandardCursorFactory { get; }
public IStyler Styler { get; } public IStyler Styler { get; }
public Func<Styles> Theme { get; } public Func<Styles> Theme { get; }
@ -113,6 +117,7 @@ namespace Avalonia.UnitTests
IRenderer renderer = null, IRenderer renderer = null,
IPlatformRenderInterface renderInterface = null, IPlatformRenderInterface renderInterface = null,
IRenderLoop renderLoop = null, IRenderLoop renderLoop = null,
IScheduler scheduler = null,
IStandardCursorFactory standardCursorFactory = null, IStandardCursorFactory standardCursorFactory = null,
IStyler styler = null, IStyler styler = null,
Func<Styles> theme = null, Func<Styles> theme = null,
@ -130,6 +135,7 @@ namespace Avalonia.UnitTests
renderer: renderer ?? Renderer, renderer: renderer ?? Renderer,
renderInterface: renderInterface ?? RenderInterface, renderInterface: renderInterface ?? RenderInterface,
renderLoop: renderLoop ?? RenderLoop, renderLoop: renderLoop ?? RenderLoop,
scheduler: scheduler ?? Scheduler,
standardCursorFactory: standardCursorFactory ?? StandardCursorFactory, standardCursorFactory: standardCursorFactory ?? StandardCursorFactory,
styler: styler ?? Styler, styler: styler ?? Styler,
theme: theme ?? Theme, theme: theme ?? Theme,

11
tests/Avalonia.UnitTests/UnitTestApplication.cs

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

Loading…
Cancel
Save