In the previous postI highlighted the issues with running WinRT tests on your build server and how we made our life a lot simpler by structuring our solution so we can test using standard .net testing frameworks (much like you would have done in Silverlight). Here is an applied example of that approach. It is an example of the dependency inversion principle. It is also an example to help folk remember that async actions can easily create reentrancy issues.
It is required that our application keeps itself up to date by automatically refreshing the current screen. From a non-functional perspective the timer must not be reentrant (if the service call exceeds the tick interval it shouldn’t kick off another request).
So we’re going to need a dispatcher timer (we want the callback on the UI thread). DispatcherTimer is in Windows.UI.XAML, a WinRT only namespace not available in Portable. We want to keep this code as minimal as possible because it is hard to test (it’s actually easy enough to test, the problem is those tests won’t run on our build box). We also need to invert the dependency such that other portable code (view models for example) can access the timer. If you think of the simplest thing you can do you come up with something which fires once after a given interval. You end up with an interface in Portable which looks like this:
public interface IDispatcherTimerFactory
{
IDelayedAction DelayInvoke(TimeSpan delay, Action action);
}
public interface IDelayedAction
{
void Cancel();
}
And you have a really simple implementation in WinRT which looks like this:
public class DispatcherTimerFactory : IDispatcherTimerFactory
public IDelayedAction DelayInvoke(TimeSpan delay, Action action)
{
var timer = new DispatcherTimer { Interval = delay };
timer.Tick += (s, e) =>
{
timer.Stop();
action();
};
timer.Start();
return new CancelableTimer(timer);
}
private class CancelableTimer : IDelayedAction
{
DispatcherTimer _timer;
public CancelableTimer(DispatcherTimer timer)
{
_timer = timer;
}
public void Cancel()
{
_timer.Stop();
_timer = null;
}
}
}
Now that’s cool but it doesn’t meet our requirement, we want this to keep firing over and over. Now we can write that in our Portable namespace. Here you can test using whatever testing framework tickles your fancy – we’re going to use MSpec.
As you know everything is async in WinRT which is great but you have to remember this when you design your interfaces. We want to build something which lets someone perform a screen update every 5 seconds or so. That means they are going to have to fetch some data asynchronously. The timer mustn’t be reentrant so we don’t want to start the timer until the async call finishes, that at least means the server isn’t constantly handling calls from the client. We also would like the ability to manually tick the timer. This has the benefit of providing a refresh button to the user but also makes testing the view models easier because you can force their underlying timer to tick. There is also the case where the tick action is not async. With all this in mind we ended up with an interface like this:
public interface ITimerService{
IAsyncTimer StartRepeatingAsyncTimer(TimeSpan interval, Func<Task> callback);
ITimer StartRepeatingTimer(TimeSpan interval, Action callback);
}
public interface IAsyncTimer : ITimer
{
Task AsyncForceTick();
}
public interface ITimer : IDisposable
{
void ForceTick();
}
Note: Something to point out is these interfaces were evolved by TDDing functional requirements and refactoring out the infrastructural code as we went along. For simplicity I’m skipping the process by which we got to these designs as this isn’t so much a lesson in TDD (although the git log for this is quite interesting too).
Here is the implementation (pretty sure the naming sucks) and tests. Because we’re able to use full .Net to test Portable libraries you have no restrictions on your testing framework.
public class TimerService : ITimerService
{
readonly IDispatcherTimerFactory _dispatcherTimerFactory;
public TimerService(IDispatcherTimerFactory dispatcherTimerFactory)
{
_dispatcherTimerFactory = dispatcherTimerFactory;
}
public IAsyncTimer StartRepeatingAsyncTimer(TimeSpan interval, Func<Task> callback)
{
return new AsyncRepeatingTimer(interval, callback, _dispatcherTimerFactory);
}
public ITimer StartRepeatingTimer(TimeSpan interval, Action callback)
{
return new SyncRepeatingTimer(interval, callback, _dispatcherTimerFactory);
}
private class SyncRepeatingTimer : RepeatingTimer
{
readonly Action _callback;
public SyncRepeatingTimer(TimeSpan interval, Action callback, IDispatcherTimerFactory dispatcherTimerFactory) : base(dispatcherTimerFactory, interval)
{
_callback = callback;
}
protected override void Tick()
{
_callback();
Restart();
}
}
private class AsyncRepeatingTimer : RepeatingTimer, IAsyncTimer
{
readonly Func<Task> _callback;
bool _isTicking;
public AsyncRepeatingTimer(TimeSpan interval, Func<Task> callback, IDispatcherTimerFactory dispatcherTimerFactory)
: base(dispatcherTimerFactory, interval)
{
_callback = callback;
}
async Task _DoTick()
{
// Don't worry about thread safety here we always tick on the UI thread
if (_isTicking)
{
return;
}
_isTicking = true;
await _callback();
_isTicking = false;
Restart();
}
protected async override void Tick()
{
await _DoTick();
}
public async Task AsyncForceTick()
{
Cancel();
await _DoTick();
}
}
private abstract class RepeatingTimer : ITimer
{
readonly IDispatcherTimerFactory _dispatcherTimerFactory;
readonly TimeSpan _interval;
bool _isDisposed;
IDelayedAction _delayedAction;
public RepeatingTimer(IDispatcherTimerFactory dispatcherTimerFactory, TimeSpan interval)
{
_dispatcherTimerFactory = dispatcherTimerFactory;
_interval = interval;
Restart();
}
protected bool IsDisposed
{
get { return _isDisposed; }
}
public void Dispose()
{
if (IsDisposed)
{
return;
}
_delayedAction.Cancel();
_delayedAction = null;
_isDisposed = true;
}
protected void Restart()
{
if (IsDisposed)
{
return;
}
_delayedAction = _dispatcherTimerFactory.DelayInvoke(_interval, Tick);
}
protected abstract void Tick();
protected void Cancel()
{
_delayedAction.Cancel();
}
public void ForceTick()
{
Cancel();
Tick();
}
}
}
public class when_async_timer_is_stopped_during_async_action_and_then_completes : when_async_timer_is_created
{
Establish context = () => InvokeTimer();
Because of = () =>
{
_timer.Dispose();
_taskCompletionSource.SetResult(new object());
};
It should_not_restart_itself = () => _createdDelayedActions.ShouldEqual(1);
}
public class when_async_timer_is_manually_triggered_during_async_action : when_async_timer_is_created
{
Establish context = () => InvokeTimer();
Because of = () => _timer.ForceTick();
It should_not_invoke_the_action_again = () => _callCount.ShouldEqual(1);
}
public class when_async_timer_is_stopped : when_async_timer_is_created
{
Because of = () => _timer.Dispose();
It should_cancel_delayed_action = () => _delayedAction.Verify(x => x.Cancel());
}
public class when_async_timer_tick_action_completes : when_async_timer_is_created
{
Establish context = () => InvokeTimer();
Because of = () => _taskCompletionSource.SetResult(new object());
It should_restart_timer = () => _createdDelayedActions.ShouldEqual(2);
}
public class when_async_timer_ticks : when_async_timer_is_created
{
Because of = () => InvokeTimer();
It should_call_callback = () => _wasCalled.ShouldBeTrue();
It should_not_restart_timer_as_async_action_not_yet_completed = () => _createdDelayedActions.ShouldEqual(1);
}
public class when_async_timer_is_manaully_triggered : when_async_timer_is_created
{
Because of = () => _timer.ForceTick();
It should_cancel_delayed_action = () => _delayedAction.Verify(x => x.Cancel());
It should_call_callback = () => _wasCalled.ShouldBeTrue();
It should_not_restart_timer_as_async_action_not_yet_completed = () => _createdDelayedActions.ShouldEqual(1);
}
public class when_async_timer_is_manaully_triggered_async : when_async_timer_is_created
{
Because of = () => _task = _timer.AsyncForceTick();
It should_cancel_delayed_action = () => _delayedAction.Verify(x => x.Cancel());
It should_call_callback = () => _wasCalled.ShouldBeTrue();
It should_not_restart_timer_as_async_action_not_yet_completed = () => _createdDelayedActions.ShouldEqual(1);
It should_return_the_incomplete_task = () => _task.IsCompleted.ShouldBeFalse();
static Task _task;
}
public class when_synchronous_timer_is_stopped : when_synchronous_timer_is_created
{
Because of = () => _timer.Dispose();
It should_cancel_delayed_action = () => _delayedAction.Verify(x => x.Cancel());
}
public class when_synchronous_is_manually_triggered : when_synchronous_timer_is_created
{
Because of = () => _timer.ForceTick();
It should_cancel_delayed_action = () => _delayedAction.Verify(x => x.Cancel());
It should_call_callback = () => _wasCalled.ShouldBeTrue();
It should_restart_timer = () => _createdDelayedActions.ShouldEqual(2);
}
public class when_synchronous_timer_fires : when_synchronous_timer_is_created
{
Because of = () => InvokeTimer();
It should_call_callback = () => _wasCalled.ShouldBeTrue();
It should_restart_timer = () => _createdDelayedActions.ShouldEqual(2);
}
public class when_async_timer_is_created : TimerServiceTests
{
Establish context = () =>
{
_taskCompletionSource = new TaskCompletionSource<object>();
_wasCalled = false;
_callCount = 0;
_timer = _timerService.StartRepeatingAsyncTimer(TimeSpan.FromSeconds(10), _handleTick);
};
protected static readonly Func<Task> _handleTick = () =>
{
_wasCalled = true;
_callCount++;
return _taskCompletionSource.Task;
};
protected static IAsyncTimer _timer;
protected static bool _wasCalled;
protected static TaskCompletionSource<object> _taskCompletionSource;
protected static int _callCount;
}
public class when_synchronous_timer_is_created : TimerServiceTest
{
Establish context = () =>
{
_wasCalled = false;
_timer = _timerService.StartRepeatingTimer(TimeSpan.FromSeconds(10), _handleTick);
};
protected static readonly Action _handleTick = () => _wasCalled = true;
protected static ITimer _timer;
protected static bool _wasCalled;
}
public class TimerServiceTests
{
Establish context = () =>
{
_timerFactory = new Mock<IDispatcherTimerFactory>();
_callback = null;
_createdDelayedActions = 0;
_delayedAction = new Mock<IDelayedAction>();
_timerFactory.Setup(x => x.DelayInvoke(Moq.It.IsAny<TimeSpan>(), Moq.It.IsAny<Action>()))
.Callback<TimeSpan, Action>((t, a) =>
_callback = a;
_createdDelayedActions++;
})
.Returns(_delayedAction.Object);
_timerService = new TimerService(_timerFactory.Object);
};
protected static void InvokeTimer()
{
_callback();
}
protected static Mock<IDispatcherTimerFactory> _timerFactory;
protected static TimerService _timerService;
protected static int _createdDelayedActions;
static Action _callback;
protected static Mock<IDelayedAction> _delayedAction;
}