Reputation: 1310
I've been writing an API that facilitates communication with a serial port. I'm doing some refactoring and general cleanup and was wondering if there's a way to avoid the following issue.
The main class in the API has the capability to constantly read from the port and raise an event containing a value when the read bytes match a particular regex. The process of reading and parsing occurs on another thread. The event contains the value as an argument (string
) and because it's being raised from another thread, a client attempting to directly assign the value to, say, the Text
property of a control causes a cross-thread exception unless the handler has the proper Invoke
code.
I understand why this happens, and when I put the proper invocation code in my test client's event handler, all is well; my question is whether or not there's anything I can do in the API code itself such that clients don't have to worry about it.
Essentially, I'd like to turn this:
void PortAdapter_ValueChanged(Command command, string value)
{
if (this.InvokeRequired)
{
Invoke(new MethodInvoker(() =>
{
receivedTextBox.Text = value;
}));
}
else
{
receivedTextBox.Text = value;
}
}
into simply this:
void PortAdapter_ValueChanged(Command command, string value)
{
receivedTextBox.Text = value;
}
Upvotes: 2
Views: 1123
Reputation: 23169
You can trigger the event in the UI Thread, this way the event handler (if any) will already be in the UI thread.
public class PortAdapter
{
public event EventHandler<string> ValueChanged;
protected virtual void OnValueChanged(string e)
{
var handler = ValueChanged;
if (handler != null)
{
RunInUiThread(() => handler(this, e));
}
}
private void RunInUiThread(Action action)
{
if (InvokeRequired)
{
Invoke(action);
}
else
{
action.Invoke();
}
}
}
However this is not good design because you don't know if an handler will perform UI interaction.
Upvotes: 1
Reputation: 73442
Well there is a common pattern for that used many places in .Net framework itself. For example BackgroundWorker
uses this model.
For that you'll take a SynchronizationContext
as a parameter for your API, in this case I assume it is PortAdapter
.
When raising an event, you raise the event in given SynchronizationContext
using SynchronizationContext.Post
or SynchronizationContext.Send
. Former is asynchronous and latter is synchronous.
So, when client code creating a instance of your PortAdapter
, it passes WindowsFormsSynchronizationContext
instance as parameter. Which means that PortAdapter
will raise the event in given synchronization context and that also means that you don't need a InvokeRequired
or Invoke
calls.
public class PortAdapter
{
public event EventHandler SomethingHappened;
private readonly SynchronizationContext context;
public PortAdapter(SynchronizationContext context)
{
this.context = context ?? new SynchronizationContext();//If no context use thread pool
}
private void DoSomethingInteresting()
{
//Do something
EventHandler handler = SomethingHappened;
if (handler != null)
{
//Raise the event in client's context so that client doesn't needs Invoke
context.Post(x => handler(this, EventArgs.Empty), null);
}
}
}
Client code:
PortAdapter adpater = new PortAdapter(SynchronizationContext.Current);
...
It is very important to create instance of PortAdapter
in UI thread, otherwise SynchronizationContext.Current
will be null and hence events will be still raised in ThreadPool thread.
More about SynchronizationContext here.
Upvotes: 3
Reputation: 21969
TBH, the approach with checking for InvokeRequired
is fine and flexible.
But if you like, you can have all events in your application UI-safe. For this either all classes have to have invocation control registered
public class SomeClassWithEvent
{
private static Control _invoke = null;
public static void SetInvoke(Control control)
{
_invoke = control;
}
public event Action SomeEvent;
public OnSomeEvent()
{
// this event will be invoked in UI thread
if (_invoke != null && _invoke.IsHandleCreated && SomeEvent != null)
_invoke.BeginInvoke(SomeEvent);
}
}
// somewhere you have to register
SomeClassWithEvent.SetInvoke(mainWindow);
// and mayhaps unregister
SomeClassWithEvent.SetInvoke(null);
or have that invocation control exposed, to example:
// application class
public static class App
{
// will be set by main window and will be used even risers to invoke event
public static MainWindow {get; set;}
}
You will have difficulties if event occur when no handle is created or control registered.
Upvotes: 2