When I originally put Broadcast on Github, it provided synchronous and asynchronous broadcasting of messages that objects could register to receive.
I ran into a problem with the original release however. Any message that was broadcasted from another thread and received by a UI object, would throw an exception. UI elements can not be accessed from any thread other than the main thread.
For example, let's assume the user pressed a sync button. The sync process is very time consuming, so we need to run it asynchronously.
public class SyncManager
{
public void Sync()
{
// Run sync async and return control to the UI.
Task.Run(() => PerformSync());
}
private void PerformSync()
{
// Do sync stuff....
// Notify UI that sync is completed.
NotificationManager.PostNotification(this, "SyncFinished");
}
}
Inside of our UI constructor we register to receive the notification.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
NotificationManager.RegisterObserver(this, "SyncFinished", this.HandleSyncComplete);
}
public void HandleSyncComplete(object sender, Dictionary<string, data-preserve-html-node="true" object> userData)
{
MessageBox.Show("Sync finished!");
}
}
In the above code, when the MessageBox.Show
is invoked, an exception would be thrown. This is caused by the fact that a UI element is being accessed from another thread. How do we fix this?
We can wrap each registered method in a
public void HandleSyncComplete(object sender, Dictionary<string, data-preserve-html-node="true" object> userData)
{
Application.Current.Dispatcher.Invoke(() =>
{
MessageBox.Show("Sync completed!");
});
}
This is a real pain though. So in order to fix this, I looked into handling broadcasts on a specific thread. I discovered the SynchronizationContext class. So now we can tell the NotificationManager to run all synchronous broadcasts on the main thread.
NotificationManager.Context = System.Threading.SynchronizationContext.Current;
The SynchronizationContext.Current
will grab the current threads SynchronizationContext. If you access this property from within a UI elements constructor, it will always provide the thread associated with the main UI thread. So we can modify our above MainWindow constructor to be like the following.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
NotificationManager.Context = System.Threading.SynchronizationContext.Current;
NotificationManager.RegisterObserver(this, "SyncFinished", this.HandleSyncComplete);
}
public void HandleSyncComplete(object sender, Dictionary<string, data-preserve-html-node="true" object> userData)
{
MessageBox.Show("Sync finished!");
}
}
Now when the "SyncFinished" notification is broadcasted, registered HandleSyncComplete
method will be ran on the main thread. When the Messagebox.Show
method is invoked, it will be invoked on the main thread and no longer throw an exception.
If the NotificationManager.Context
property is null, the message will always be broadcasted on the same thread that the message was broadcasted from.
This only works in the message was broadcasted synchronously from within an Async method. If a message was broadcasted asynchronously from within an an Async method, the context is ignored and the message is broadcasted on it's own thread.
To provide support on the UI for these scenarios, you can register the observer and specify that it can not run async.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
NotificationManager.Context = System.Threading.SynchronizationContext.Current;
NotificationManager.RegisterObserver(this, "SyncFinished", this.HandleSyncComplete, canRunAsync: false);
}
public void HandleSyncComplete(object sender, Dictionary<string, data-preserve-html-node="true" object> userData)
{
MessageBox.Show("Sync finished!");
}
}
By setting the canRunAsync
argument to false, the observer will have its method invoked in a synchronous fashion even if the message was posted asynchronously. If context is null, then it will still run async.