Use Dispatcher instead of SynchronizationContext for synchronization.

It looks like the SynchronizationContext can now change on the same thread
(see http://msdn.microsoft.com/en-us/library/system.windows.basecompatibilitypreferences.reusedispatchersynchronizationcontextinstance%28v=vs.110%29.aspx)
meaning that we can no longer do a reference comparison between SynchronizationContext.Current
and the captured SynchronizationConext to check whether a dispatch is required.
It turns out we shouldn't have been doing this anyway.... So switch to using a Dispatcher,
which does support this stuff.

Additionally, Execute now uses an IDispatcher, which means the implementation can be switched
again in the future. It also makes unit testing easier....
This commit is contained in:
Antony Male 2014-05-09 15:46:03 +01:00
parent 5ca2508fc0
commit a39bcfbadb
4 changed files with 118 additions and 62 deletions

View File

@ -29,11 +29,19 @@ namespace Stylet
void RemoveRange(IEnumerable<T> items); void RemoveRange(IEnumerable<T> items);
} }
/// <summary>
/// Interface encapsulating IReadOnlyList and INotifyCollectionChanged
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IReadOnlyObservableCollection<T> : IReadOnlyList<T>, INotifyCollectionChanged, INotifyPropertyChangedDispatcher
{
}
/// <summary> /// <summary>
/// ObservableCollection subclass which supports AddRange and RemoveRange /// ObservableCollection subclass which supports AddRange and RemoveRange
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
public class BindableCollection<T> : ObservableCollection<T>, IObservableCollection<T> public class BindableCollection<T> : ObservableCollection<T>, IObservableCollection<T>, IReadOnlyObservableCollection<T>
{ {
private Action<Action> _propertyChangedDispatcher = Execute.DefaultPropertyChangedDispatcher; private Action<Action> _propertyChangedDispatcher = Execute.DefaultPropertyChangedDispatcher;
/// <summary> /// <summary>

View File

@ -45,7 +45,7 @@ namespace Stylet
protected virtual void Start() protected virtual void Start()
{ {
// Use the current SynchronizationContext for the Execute helper // Use the current SynchronizationContext for the Execute helper
Execute.SynchronizationContext = SynchronizationContext.Current; Execute.Dispatcher = new DispatcherWrapper();
// Add the current assembly to the assemblies list - this will be needed by the IViewManager // Add the current assembly to the assemblies list - this will be needed by the IViewManager
// However it must be done *after* the SynchronizationContext has been set, or we'll try to raise a PropertyChanged notification and fail // However it must be done *after* the SynchronizationContext has been set, or we'll try to raise a PropertyChanged notification and fail

View File

@ -7,15 +7,63 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Threading;
namespace Stylet namespace Stylet
{ {
/// <summary>
/// Generalised dispatcher, which can post and end
/// </summary>
public interface IDispatcher
{
/// <summary>
/// Execute asynchronously
/// </summary>
void Post(Action action);
/// <summary>
/// Execute synchronously
/// </summary>
void Send(Action action);
/// <summary>
/// True if invocation isn't required
/// </summary>
bool IsCurrent { get; }
}
internal class DispatcherWrapper : IDispatcher
{
private Dispatcher dispatcher;
public DispatcherWrapper()
{
this.dispatcher = Dispatcher.CurrentDispatcher;
}
public void Post(Action action)
{
this.dispatcher.BeginInvoke(action);
}
public void Send(Action action)
{
this.dispatcher.Invoke(action);
}
public bool IsCurrent
{
get { return this.dispatcher.CheckAccess(); }
}
}
public static class Execute public static class Execute
{ {
/// <summary> /// <summary>
/// Should be set to the UI thread's SynchronizationContext. This is normally done by the Bootstrapper. /// Should be set to the UI thread's Dispatcher. This is normally done by the Bootstrapper.
/// </summary> /// </summary>
public static SynchronizationContext SynchronizationContext; public static IDispatcher Dispatcher;
/// <summary> /// <summary>
/// FOR TESTING ONLY. Causes everything to execute synchronously /// FOR TESTING ONLY. Causes everything to execute synchronously
@ -29,9 +77,9 @@ namespace Stylet
/// </summary> /// </summary>
public static Action<Action> DefaultPropertyChangedDispatcher = Execute.BeginOnUIThreadOrSynchronous; public static Action<Action> DefaultPropertyChangedDispatcher = Execute.BeginOnUIThreadOrSynchronous;
private static void EnsureSynchronizationContext() private static void EnsureDispatcher()
{ {
if (SynchronizationContext == null && !TestExecuteSynchronously) if (Dispatcher == null && !TestExecuteSynchronously)
throw new InvalidOperationException("Execute.SynchronizationContext must be set before this method can be called. This should normally have been done by the Bootstrapper"); throw new InvalidOperationException("Execute.SynchronizationContext must be set before this method can be called. This should normally have been done by the Bootstrapper");
} }
@ -40,9 +88,9 @@ namespace Stylet
/// </summary> /// </summary>
public static void BeginOnUIThread(Action action) public static void BeginOnUIThread(Action action)
{ {
EnsureSynchronizationContext(); EnsureDispatcher();
if (!TestExecuteSynchronously) if (!TestExecuteSynchronously)
SynchronizationContext.Post(_ => action(), null); Dispatcher.Post(action);
else else
action(); action();
} }
@ -52,9 +100,9 @@ namespace Stylet
/// </summary> /// </summary>
public static void BeginOnUIThreadOrSynchronous(Action action) public static void BeginOnUIThreadOrSynchronous(Action action)
{ {
EnsureSynchronizationContext(); EnsureDispatcher();
if (SynchronizationContext != SynchronizationContext.Current && !TestExecuteSynchronously) if (!TestExecuteSynchronously && !Dispatcher.IsCurrent)
SynchronizationContext.Post(_ => action(), null); Dispatcher.Post(action);
else else
action(); action();
} }
@ -64,11 +112,11 @@ namespace Stylet
/// </summary> /// </summary>
public static void OnUIThread(Action action) public static void OnUIThread(Action action)
{ {
EnsureSynchronizationContext(); EnsureDispatcher();
Exception exception = null; Exception exception = null;
if (SynchronizationContext != SynchronizationContext.Current && !TestExecuteSynchronously) if (!TestExecuteSynchronously && !Dispatcher.IsCurrent)
{ {
SynchronizationContext.Send(_ => Dispatcher.Send(() =>
{ {
try try
{ {
@ -78,7 +126,7 @@ namespace Stylet
{ {
exception = e; exception = e;
} }
}, null); });
if (exception != null) if (exception != null)
throw new System.Reflection.TargetInvocationException("An error occurred while dispatching a call to the UI Thread", exception); throw new System.Reflection.TargetInvocationException("An error occurred while dispatching a call to the UI Thread", exception);
@ -94,11 +142,11 @@ namespace Stylet
/// </summary> /// </summary>
public static Task OnUIThreadAsync(Action action) public static Task OnUIThreadAsync(Action action)
{ {
EnsureSynchronizationContext(); EnsureDispatcher();
if (SynchronizationContext != SynchronizationContext.Current && !TestExecuteSynchronously) if (!TestExecuteSynchronously && !Dispatcher.IsCurrent)
{ {
var tcs = new TaskCompletionSource<object>(); var tcs = new TaskCompletionSource<object>();
SynchronizationContext.Post(_ => Dispatcher.Post(() =>
{ {
try try
{ {
@ -109,7 +157,7 @@ namespace Stylet
{ {
tcs.SetException(e); tcs.SetException(e);
} }
}, null); });
return tcs.Task; return tcs.Task;
} }
else else

View File

@ -21,71 +21,71 @@ namespace StyletUnitTests
} }
[Test] [Test]
public void OnUIThreadExecutesUsingSynchronizationContext() public void OnUIThreadExecutesUsingDispatcher()
{ {
var sync = new Mock<SynchronizationContext>(); var sync = new Mock<IDispatcher>();
Execute.SynchronizationContext = sync.Object; Execute.Dispatcher = sync.Object;
SendOrPostCallback passedAction = null; Action passedAction = null;
sync.Setup(x => x.Send(It.IsAny<SendOrPostCallback>(), null)).Callback((SendOrPostCallback a, object o) => passedAction = a); sync.Setup(x => x.Send(It.IsAny<Action>())).Callback((Action a) => passedAction = a);
bool actionCalled = false; bool actionCalled = false;
Execute.OnUIThread(() => actionCalled = true); Execute.OnUIThread(() => actionCalled = true);
Assert.IsFalse(actionCalled); Assert.IsFalse(actionCalled);
passedAction(null); passedAction();
Assert.IsTrue(actionCalled); Assert.IsTrue(actionCalled);
} }
[Test] [Test]
public void BeginOnUIThreadExecutesUsingSynchronizationContext() public void BeginOnUIThreadExecutesUsingDispatcher()
{ {
var sync = new Mock<SynchronizationContext>(); var sync = new Mock<IDispatcher>();
Execute.SynchronizationContext = sync.Object; Execute.Dispatcher = sync.Object;
SendOrPostCallback passedAction = null; Action passedAction = null;
sync.Setup(x => x.Post(It.IsAny<SendOrPostCallback>(), null)).Callback((SendOrPostCallback a, object o) => passedAction = a); sync.Setup(x => x.Post(It.IsAny<Action>())).Callback((Action a) => passedAction = a);
bool actionCalled = false; bool actionCalled = false;
Execute.BeginOnUIThread(() => actionCalled = true); Execute.BeginOnUIThread(() => actionCalled = true);
Assert.IsFalse(actionCalled); Assert.IsFalse(actionCalled);
passedAction(null); passedAction();
Assert.IsTrue(actionCalled); Assert.IsTrue(actionCalled);
} }
[Test] [Test]
public void BeginOnUIThreadOrSynchronousExecutesUsingSynchronizationContextIfNotCurrent() public void BeginOnUIThreadOrSynchronousExecutesUsingDispatcherIfNotCurrent()
{ {
var sync = new Mock<SynchronizationContext>(); var sync = new Mock<IDispatcher>();
Execute.SynchronizationContext = sync.Object; Execute.Dispatcher = sync.Object;
SendOrPostCallback passedAction = null; Action passedAction = null;
sync.Setup(x => x.Post(It.IsAny<SendOrPostCallback>(), null)).Callback((SendOrPostCallback a, object o) => passedAction = a); sync.Setup(x => x.Post(It.IsAny<Action>())).Callback((Action a) => passedAction = a);
bool actionCalled = false; bool actionCalled = false;
Execute.BeginOnUIThreadOrSynchronous(() => actionCalled = true); Execute.BeginOnUIThreadOrSynchronous(() => actionCalled = true);
Assert.IsFalse(actionCalled); Assert.IsFalse(actionCalled);
passedAction(null); passedAction();
Assert.IsTrue(actionCalled); Assert.IsTrue(actionCalled);
} }
[Test] [Test]
public void OnUIThreadAsyncExecutesAsynchronouslyIfSynchronizationContextIsNotNull() public void OnUIThreadAsyncExecutesAsynchronouslyIfDispatcherIsNotNull()
{ {
var sync = new Mock<SynchronizationContext>(); var sync = new Mock<IDispatcher>();
Execute.SynchronizationContext = sync.Object; Execute.Dispatcher = sync.Object;
SendOrPostCallback passedAction = null; Action passedAction = null;
sync.Setup(x => x.Post(It.IsAny<SendOrPostCallback>(), null)).Callback((SendOrPostCallback a, object o) => passedAction = a); sync.Setup(x => x.Post(It.IsAny<Action>())).Callback((Action a) => passedAction = a);
bool actionCalled = false; bool actionCalled = false;
var task = Execute.OnUIThreadAsync(() => actionCalled = true); var task = Execute.OnUIThreadAsync(() => actionCalled = true);
Assert.IsFalse(task.IsCompleted); Assert.IsFalse(task.IsCompleted);
Assert.IsFalse(actionCalled); Assert.IsFalse(actionCalled);
passedAction(null); passedAction();
Assert.IsTrue(actionCalled); Assert.IsTrue(actionCalled);
Assert.IsTrue(task.IsCompleted); Assert.IsTrue(task.IsCompleted);
} }
@ -93,11 +93,11 @@ namespace StyletUnitTests
[Test] [Test]
public void OnUIThreadPropagatesException() public void OnUIThreadPropagatesException()
{ {
var sync = new Mock<SynchronizationContext>(); var sync = new Mock<IDispatcher>();
Execute.SynchronizationContext = sync.Object; Execute.Dispatcher = sync.Object;
var ex = new Exception("testy"); var ex = new Exception("testy");
sync.Setup(x => x.Send(It.IsAny<SendOrPostCallback>(), null)).Callback<SendOrPostCallback, object>((a, b) => a(b)); sync.Setup(x => x.Send(It.IsAny<Action>())).Callback<Action>(a => a());
Exception caughtEx = null; Exception caughtEx = null;
try { Execute.OnUIThread(() => { throw ex; }); } try { Execute.OnUIThread(() => { throw ex; }); }
@ -110,45 +110,45 @@ namespace StyletUnitTests
[Test] [Test]
public void OnUIThreadAsyncPropagatesException() public void OnUIThreadAsyncPropagatesException()
{ {
var sync = new Mock<SynchronizationContext>(); var sync = new Mock<IDispatcher>();
Execute.SynchronizationContext = sync.Object; Execute.Dispatcher = sync.Object;
SendOrPostCallback passedAction = null; Action passedAction = null;
sync.Setup(x => x.Post(It.IsAny<SendOrPostCallback>(), null)).Callback((SendOrPostCallback a, object o) => passedAction = a); sync.Setup(x => x.Post(It.IsAny<Action>())).Callback((Action a) => passedAction = a);
var ex = new Exception("test"); var ex = new Exception("test");
var task = Execute.OnUIThreadAsync(() => { throw ex; }); var task = Execute.OnUIThreadAsync(() => { throw ex; });
passedAction(null); passedAction();
Assert.IsTrue(task.IsFaulted); Assert.IsTrue(task.IsFaulted);
Assert.AreEqual(ex, task.Exception.InnerExceptions[0]); Assert.AreEqual(ex, task.Exception.InnerExceptions[0]);
} }
[Test] [Test]
public void ThrowsIfBeginOnUIThreadCalledWithNoSynchronizationContext() public void ThrowsIfBeginOnUIThreadCalledWithNoDispatcher()
{ {
Execute.SynchronizationContext = null; Execute.Dispatcher = null;
Assert.Throws<InvalidOperationException>(() => Execute.BeginOnUIThread(() => { })); Assert.Throws<InvalidOperationException>(() => Execute.BeginOnUIThread(() => { }));
} }
[Test] [Test]
public void ThrowsIfBeginOnUIThreadOrSynchronousCalledWithNoSynchronizationContext() public void ThrowsIfBeginOnUIThreadOrSynchronousCalledWithNoDispatcher()
{ {
Execute.SynchronizationContext = null; Execute.Dispatcher = null;
Assert.Throws<InvalidOperationException>(() => Execute.BeginOnUIThreadOrSynchronous(() => { })); Assert.Throws<InvalidOperationException>(() => Execute.BeginOnUIThreadOrSynchronous(() => { }));
} }
[Test] [Test]
public void ThrowsIfOnUIThreadCalledWithNoSynchronizationContext() public void ThrowsIfOnUIThreadCalledWithNoDispatcher()
{ {
Execute.SynchronizationContext = null; Execute.Dispatcher = null;
Assert.Throws<InvalidOperationException>(() => Execute.OnUIThread(() => { })); Assert.Throws<InvalidOperationException>(() => Execute.OnUIThread(() => { }));
} }
[Test] [Test]
public void ThrowsIfOnUIThreadAsyncCalledWithNoSynchronizationContext() public void ThrowsIfOnUIThreadAsyncCalledWithNoDispatcher()
{ {
Execute.SynchronizationContext = null; Execute.Dispatcher = null;
Assert.Throws<InvalidOperationException>(() => Execute.OnUIThreadAsync(() => { })); Assert.Throws<InvalidOperationException>(() => Execute.OnUIThreadAsync(() => { }));
} }
@ -157,7 +157,7 @@ namespace StyletUnitTests
{ {
Execute.TestExecuteSynchronously = true; Execute.TestExecuteSynchronously = true;
Execute.SynchronizationContext = null; Execute.Dispatcher = null;
bool called = false; bool called = false;
Execute.BeginOnUIThread(() => called = true); Execute.BeginOnUIThread(() => called = true);
Assert.True(called); Assert.True(called);
@ -168,7 +168,7 @@ namespace StyletUnitTests
{ {
Execute.TestExecuteSynchronously = true; Execute.TestExecuteSynchronously = true;
Execute.SynchronizationContext = null; Execute.Dispatcher = null;
bool called = false; bool called = false;
Execute.OnUIThread(() => called = true); Execute.OnUIThread(() => called = true);
Assert.True(called); Assert.True(called);
@ -179,7 +179,7 @@ namespace StyletUnitTests
{ {
Execute.TestExecuteSynchronously = true; Execute.TestExecuteSynchronously = true;
Execute.SynchronizationContext = null; Execute.Dispatcher = null;
bool called = false; bool called = false;
Execute.OnUIThreadAsync(() => called = true); Execute.OnUIThreadAsync(() => called = true);
Assert.True(called); Assert.True(called);