I’ve got a class which abstracts performing long running methods on a background thread away from my WPF MVVM view models. I also have this class interfaced and IoC injected into most of my view models.
public interface IAsyncActionManager : INotifyPropertyChanged
{
/// <summary>
/// Sets and gets the IsBusy property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
bool IsBusy { get; }
Task StartAsyncTask(Action backgroundAction);
}
My view models use this class in various ways such as:
private void LoadStuff()
{
ActionManager.StartAsyncTask(() => { // Load stuff from database here });
}
And in some of my XAML I bind directly to the IsBusy property:
<Grid Cursor="{Binding ActionManager.IsBusy, Converter={Converters:BusyMouseConverter}}">
Anyway – now you have the background, I’m now trying to do something a little more fancy:
private Task _saveChangesTask;
public void SaveChanges()
{
if (_saveChangesTask != null && _saveChangesTask.Status != TaskStatus.Running)
return;
_saveChangesTask = ActionManager.StartAsyncTask(() =>
{
// Save stuff here - slowly
});
}
This is simplified as I’ve also got it hooked up via a Command object which WPF uses in its view with CanExecute etc but this ‘caching’ of the task is so that the save action doesn’t get run twice.
Now, getting to the problem, I want to unit test this logic – how do I do so?
I’ve tried using a TaskCompletionSource in my test but I cannot get my Task into a ‘Running’ state…?
var tcs = new TaskCompletionSource<object>();
// tcs.Task.Status is now WaitingForActivation
// tcs.Task.Start(Synchronous.TaskScheduler); // Doesn't work - throws an Exception.
A.CallTo(() => mockAsyncActionManager.StartAsyncTask(A<Action>._, A<Action<Task>>._)).Returns(tcs.Task);
Anyone got any clues? Can I do this?
I’ve got an idea that I’m using the TPL wrongly – that I shouldn’t be relying on task Status – but not sure how to achieve a similar thing in another way (suggestions welcome).
Cheers,
I believe the problem here does lie (as you said) with the check of the
Statusproperty.The
TaskStatusenumeration indicates that aTaskinstance doesn’t just have a binary state of running/not running, but rather, can be in a number of states.When a
Taskis created, depending on theTaskScheduler, it will put theTaskin the following states before theRunningstate:Created– The task has been initialized but has not yet been scheduled.WaitingForActivation– The task is waiting to be activated and scheduled internally by the .NET Framework infrastructure.WaitingToRun– The task has been scheduled for execution but has not yet begun executing.With that, your check against the
TaskStatusofRunningcan fail because it is in one of the above states.What I’d recommend is you simply check against the reference for the
Task; if it’snull, then create a newTask, otherwise, just return.The assumption here is that the call to
SaveChangesis meant to be called once on the object (or not do anything until a save is complete).If you are going to call the method again (presumably, because other changes have been made), you should have a continuation on the
Taskthat will set the reference to theTasktonullwhen the operation is complete. This way, the check against the reference will succeed whenSaveChangesis called a second time.On a side note, I’ve pointed out in the comments that you have a race condition. If you are going to set the reference to the
Taskback tonullin the continuation, then you’ll need to perform the check and the assignment in a thread-safe way (since the continuation will run on another thread), like so: