Я писал API, который облегчает связь с последовательным портом. Я делаю рефакторинг и общую очистку, и мне было интересно, есть ли способ избежать следующей проблемы.
Основной класс в API имеет возможность постоянно читать из порта и поднять событие, содержащее значение, когда прочитанные байты соответствуют определенному регулярному выражению. Процесс чтения и разбора происходит в другом потоке. Событие содержит значение в качестве аргумента (string
), и поскольку оно создается из другого потока, клиент, пытающийся напрямую назначить значение, например, свойство Text
элемента управления вызывает исключение поперечной нити, если обработчик не имеет надлежащего Invoke
код.
Я понимаю, почему это происходит, и когда я помещаю правильный код вызова в обработчик событий тестового клиента, все хорошо; мой вопрос в том, есть ли что-нибудь, что я могу сделать в самом коде API, чтобы клиенты не беспокоились об этом.
По сути, я хотел бы включить это:
void PortAdapter_ValueChanged(Command command, string value)
{
if (this.InvokeRequired)
{
Invoke(new MethodInvoker(() =>
{
receivedTextBox.Text = value;
}));
}
else
{
receivedTextBox.Text = value;
}
}
в простое:
void PortAdapter_ValueChanged(Command command, string value)
{
receivedTextBox.Text = value;
}
Ну, есть общая схема, которая использовала многие места в самой инфраструктуре.Net. Например, BackgroundWorker
использует эту модель.
Для этого вы возьмете SynchronizationContext
в качестве параметра для вашего API, в этом случае я предполагаю, что это PortAdapter
.
При создании события вы поднимаете событие в заданном SynchronizationContext
используя SynchronizationContext.Post
или SynchronizationContext.Send
. Бывший асинхронный, а последний синхронный.
Итак, когда клиентский код создает экземпляр вашего PortAdapter
, он передает экземпляр WindowsFormsSynchronizationContext
качестве параметра. Это означает, что PortAdapter
поднимет событие в заданном контексте синхронизации, а это также означает, что вам не нужны InvokeRequired
или Invoke
.
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 context so that client doesn't needs Invoke
context.Post(x => handler(this, EventArgs.Empty), null);
}
}
}
Код клиента:
PortAdapter adpater = new PortAdapter(SynchronizationContext.Current);
...
Очень важно создать экземпляр PortAdapter
в потоке пользовательского интерфейса, иначе SynchronizationContext.Current
будет null, и, следовательно, события будут по-прежнему возникать в потоке ThreadPool.
UserControl
только для того, чтобы это работало). Сначала я видел необходимость передавать контекст как компромисс по сравнению с вызовом Invoke
, но, поскольку это аргумент для конструктора, по крайней мере, теперь это необходимо и легче объяснить в документации. Спасибо!
TBH, подход с проверкой InvokeRequired
является точным и гибким.
Но если вам нравится, вы можете использовать все события в своем приложении UI-safe. Для этого либо все классы должны иметь зарегистрированный контроль вызова
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);
или чтобы этот контроль вызова был открыт, например:
// 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;}
}
У вас возникнут трудности, если произойдет событие, когда дескриптор не создан или не зарегистрирован.
Вы можете инициировать событие в потоке пользовательского интерфейса, таким образом обработчик событий (если он есть) уже будет в потоке пользовательского интерфейса.
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();
}
}
}
Однако это не очень хороший дизайн, потому что вы не знаете, будет ли обработчик выполнять взаимодействие с пользовательским интерфейсом.
PortAdapter : UserControl
? Это противно Это злоупотребление пользовательским интерфейсом
UserControl