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);
}
/// <summary>
/// Interface encapsulating IReadOnlyList and INotifyCollectionChanged
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IReadOnlyObservableCollection<T> : IReadOnlyList<T>, INotifyCollectionChanged, INotifyPropertyChangedDispatcher
{
}
/// <summary>
/// ObservableCollection subclass which supports AddRange and RemoveRange
/// </summary>
/// <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;
/// <summary>

View File

@ -45,7 +45,7 @@ namespace Stylet
protected virtual void Start()
{
// 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
// 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.Tasks;
using System.Windows;
using System.Windows.Threading;
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
{
/// <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>
public static SynchronizationContext SynchronizationContext;
public static IDispatcher Dispatcher;
/// <summary>
/// FOR TESTING ONLY. Causes everything to execute synchronously
@ -29,9 +77,9 @@ namespace Stylet
/// </summary>
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");
}
@ -40,9 +88,9 @@ namespace Stylet
/// </summary>
public static void BeginOnUIThread(Action action)
{
EnsureSynchronizationContext();
EnsureDispatcher();
if (!TestExecuteSynchronously)
SynchronizationContext.Post(_ => action(), null);
Dispatcher.Post(action);
else
action();
}
@ -52,9 +100,9 @@ namespace Stylet
/// </summary>
public static void BeginOnUIThreadOrSynchronous(Action action)
{
EnsureSynchronizationContext();
if (SynchronizationContext != SynchronizationContext.Current && !TestExecuteSynchronously)
SynchronizationContext.Post(_ => action(), null);
EnsureDispatcher();
if (!TestExecuteSynchronously && !Dispatcher.IsCurrent)
Dispatcher.Post(action);
else
action();
}
@ -64,11 +112,11 @@ namespace Stylet
/// </summary>
public static void OnUIThread(Action action)
{
EnsureSynchronizationContext();
EnsureDispatcher();
Exception exception = null;
if (SynchronizationContext != SynchronizationContext.Current && !TestExecuteSynchronously)
if (!TestExecuteSynchronously && !Dispatcher.IsCurrent)
{
SynchronizationContext.Send(_ =>
Dispatcher.Send(() =>
{
try
{
@ -78,7 +126,7 @@ namespace Stylet
{
exception = e;
}
}, null);
});
if (exception != null)
throw new System.Reflection.TargetInvocationException("An error occurred while dispatching a call to the UI Thread", exception);
@ -94,11 +142,11 @@ namespace Stylet
/// </summary>
public static Task OnUIThreadAsync(Action action)
{
EnsureSynchronizationContext();
if (SynchronizationContext != SynchronizationContext.Current && !TestExecuteSynchronously)
EnsureDispatcher();
if (!TestExecuteSynchronously && !Dispatcher.IsCurrent)
{
var tcs = new TaskCompletionSource<object>();
SynchronizationContext.Post(_ =>
Dispatcher.Post(() =>
{
try
{
@ -109,7 +157,7 @@ namespace Stylet
{
tcs.SetException(e);
}
}, null);
});
return tcs.Task;
}
else

View File

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