Предотвратить необходимость InvokeRequired в обработчике событий

1

Я писал 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;
}
  • 0
    Забавно получить 3 ответа почти одновременно, но через полчаса ничего не получается = D
Теги:
multithreading
winforms

3 ответа

2
Лучший ответ

Ну, есть общая схема, которая использовала многие места в самой инфраструктуре.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.

Подробнее о SynchronizationContext здесь.

  • 0
    Копирование SynchronizationContext.Current является стандартным подходом.
  • 0
    Эта опция, кажется, оказывает минимальное влияние на кодовую базу (я действительно не хочу, чтобы мой класс наследовал от UserControl только для того, чтобы это работало). Сначала я видел необходимость передавать контекст как компромисс по сравнению с вызовом Invoke , но, поскольку это аргумент для конструктора, по крайней мере, теперь это необходимо и легче объяснить в документации. Спасибо!
1

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;}
}

У вас возникнут трудности, если произойдет событие, когда дескриптор не создан или не зарегистрирован.

0

Вы можете инициировать событие в потоке пользовательского интерфейса, таким образом обработчик событий (если он есть) уже будет в потоке пользовательского интерфейса.

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();
        }
    }
}

Однако это не очень хороший дизайн, потому что вы не знаете, будет ли обработчик выполнять взаимодействие с пользовательским интерфейсом.

  • 0
    PortAdapter : UserControl ? Это противно Это злоупотребление пользовательским интерфейсом
  • 0
    Удален UserControl
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню