Problem
How can I make two classes interact between themselves asynchronously without making my code harder to write, understand, run, and test?
Solution
Use a mediator between these two objects: a dispatcher; this dispatcher will be asynchronous in the production environment and synchronous in the test environment, making it easier to test and debug. It's a free lock solution!
Real life story
In my country (France), I've been used to going to the post office and waiting thirty minutes to send a packet. I'm not very patient waiting in the queue, away from my computer, it is like hell. Fortunately, some years ago, the post service improved. Now, you just have to tell what you want when you enter in the post office, then someone gives me a ticket with a number. Now, I can sit down, and write an article while I'm waiting for my turn.
So what's the big deal?
Let's translate this real world example to technical terms. I'm a client, and I enqueue a task which describes what I want the server to do in the dispatcher. The dispatcher forwards one task after another to the server. When my task is in the server's hands, it processes it and maybe, it will need my intervention. So, it will call me, and we will initialize a conversation to complete the task.
Actors
- Client
- Server
- Dispatcher
- Task
- Conversation
The point is that the client and the server don't need to be thread safe to communicate; the client is aware of the dispatcher, not the server.
So first, a dispatcher is an object which takes an Action and doesn't give any guarantee on when the action will be executed:
I'll talk about the classes later; quickly, WPFDispatcher
encapsulates a System.Threading.Dispatcher
object (from WPF, WindowBase.dll), as ThreadDispatcher
does, except that it also creates a new thread.
Example
Here is the class design for my example:
PersonsViewModel
is the client of IPersonRepository
which is an interface representing the server. Only one class implements IPersonRepository
: VerySlowPersonRepository
. In a real life scenario, you can imagine creating a SqlPersonRepository
. VerySlowPersonRepository
will never return the same persons (it's for simulating that data is shared).
public class VerySlowPersonRepository : IPersonRepository
{
string[] _Persons = new string[] { "Micky", "Pams",
"Kat", "Nick", "Tom",
"James", "Gil", "Joe" };
Random _Rand = new Random();
#region IPersonRepository Members
public string[] GetPersonsName()
{
Thread.Sleep(3000);
return GetRandomPersons(5);
}
private string[] GetRandomPersons(int number)
{
if(number > _Persons.Length)
number = _Persons.Length;
List<int> chosenPersons = new List<int>();
while(chosenPersons.Count < number)
{
int takenPerson = _Rand.Next(_Persons.Length);
if(!chosenPersons.Contains(takenPerson))
chosenPersons.Add(takenPerson);
}
return chosenPersons.Select(i => _Persons[i]).ToArray();
}
#endregion
}
A task is only a delegate passed by the client to the dispatcher:
protected void DoRefresh()
{
IsLoading = true;
_Repository.BeginInvoke((IPersonRepository repo) =>
{
...
});
}
The client can have its own dispatcher. By design, a dispatcher is thread safe, so a conversation can be initiated inside the task between the client and the server. Here is an example of a conversation, taken from the class PersonsViewModel
. A conversation is a cascading tasks exchange between a client and a server.
protected void DoRefresh()
{
IsLoading = true;
_Repository.BeginInvoke((IPersonRepository repo) =>
{
var persons = repo.GetPersonsName();
_CurrentDispatcher.BeginInvoke(() =>
{
Persons.Clear();
foreach(var person in persons)
{
Persons.Add(person);
}
IsLoading = false;
});
});
}
Before going further, you may have noticed that there is a generic and a non generic dispatcher. In reality, the generic version of IDispatcher
is only a dispatcher attached to a server; to create your own dispatcher, you should only implement the non-generic version. Every task has access to the server, thanks to the generic version of the dispatcher (the server's type is the type parameter of IDispatcher
).
This is an example of how I inject dispatchers in my client (given that the client will run in the current WPF dispatcher, and the server is on another thread). Note the use of the method extension Attach
. There are two overloads to the method: one which takes the server as a parameter, and the other which takes a Func<server>
delegate, which will create the server in the thread of the dispatcher.
My client constructor:
public PersonsViewModel(IDispatcher<ipersonrepository> repository,
IDispatcher currentDispatcher)
{
_Persons = new ObservableCollection<string>();
_Repository = repository;
_CurrentDispatcher = currentDispatcher ?? new WpfDispatcher();
DoRefresh();
}
Then the injection:
private void Button_Click(object sender, RoutedEventArgs e)
{
viewModel.ViewModel = CreateViewModel();
}
private PersonsViewModel CreateViewModel()
{
var currentDisp = new WpfDispatcher();
var otherDisp = new ThreadDispatcher().Attach<ipersonrepository>(
new VerySlowPersonRepository());
return new PersonsViewModel(otherDisp, currentDisp);
}
Here is the implementation of WPFDispatcher
and ThreadDispatcher
; the only important thing to note is that they use the System.Threading.Dispatcher
class of WindowBase.dll.
WPFDispatcher
:
public class WpfDispatcher : IDispatcher
{
private Dispatcher _disp;
public WpfDispatcher(Dispatcher disp)
{
_disp = disp ?? Dispatcher.CurrentDispatcher;
Priority = DispatcherPriority.Normal;
}
public WpfDispatcher()
: this(null)
{
}
public DispatcherPriority Priority
{
get;
set;
}
public void BeginInvoke(Action action)
{
_disp.BeginInvoke(action, Priority, null);
}
}
ThreadDispatcher
:
public class ThreadDispatcher : IDispatcher, IDisposable
{
Thread _Thread;
Dispatcher _DispThread;
private Dispatcher Disp
{
get
{
if(_DispThread == null)
{
_DispThread = Dispatcher.FromThread(_Thread);
if(_DispThread == null)
{
Thread.Sleep(100);
return Disp;
}
}
return _DispThread;
}
}
public ThreadDispatcher()
{
_Thread = new Thread(() =>
{
Dispatcher.CurrentDispatcher.UnhandledException +=
new DispatcherUnhandledExceptionEventHandler(
CurrentDispatcher_UnhandledException);
Dispatcher.Run();
});
_Thread.Start();
}
void CurrentDispatcher_UnhandledException(object sender,
DispatcherUnhandledExceptionEventArgs e)
{
e.Handled = true;
}
public void BeginInvoke(Action action)
{
Disp.BeginInvoke(action);
}
public void Dispose()
{
if(Disp != null)
Disp.BeginInvokeShutdown(DispatcherPriority.Normal);
}
}
Results
For those who dream of WPF, you have noticed that my client's name is not a random choice. So now, let's see my ViewModel in action in my real life WPF application:
Loading...
Persons:
Testability
I said that it was easy to test, so here I go, this is the test of the client:
public class FakePersonRepository : IPersonRepository
{
#region IPersonRepository Members
public string[] GetPersonsName()
{
return new String[] { "Mike", "Tom", "Joe" };
}
#endregion
}
[TestMethod]
public void DispatchViewModelTest()
{
SynchronizedDispatcher dispatcher = new SynchronizedDispatcher();
var repository = dispatcher.Attach<ipersonrepository>(new FakePersonRepository());
PersonsViewModel vm = new PersonsViewModel(repository, dispatcher);
Assert.AreEqual(3, vm.Persons.Count);
}
Not very hard to read and to write, is it?
Warning: SynchronizedDispatcher
should only be used for testing purposes, because all tasks sent to a dispatcher must run on the same thread. If the client and the server both don't have a SynchronizedDispatcher
, this contract breaks.
Miscellaneous
If you want to convert a IDispatcher<derivedclass>
to an IDispatcher<baseclass>
:
var dispatcherBase = IDispatcherExtensions.Cast<derived,>(dispatcher);
Conclusion
I don't know if you have already tried to unit test an asynchronous communication between two objects; if you've, now you certainly have more white hairs than brown ones. Now you can easily test the interaction between two objects which can be in different threads.