Как ждать сигнала в WinForms, одновременно прослушивая события?


Случай 1

Вот моя установка.

internal class MyClass
{
    private ApiObject apiObject;

    private bool cond1;
    private bool cond2;

    internal MyClass()
    {
        this.apiObject = new ApiObject();
        this.apiObject.ApiStateUpdate += new ApiStateUpdateEventHandler(ApiStateHandler);

        //wait for both conditions to be true
    }

    private void ApiStateHandler(string who, int howMuch)
    {
        if(who.Equals("Something") && howMuch == 1)
            this.cond1 = true;
        else if(who.Equals("SomethingElse") && howMuch == 1)
            this.cond2 = true;
    }
}

Как я могу ждать, пока оба условия будут истинными?

Если я это сделаю:

while(!(this.cond1 && this.cond2))
{
    System.Threading.Thread.Sleep(1000);
}

Код в ApiStateHandler(), кажется, никогда не выполняется.

Если я это сделаю:

while(!(this.cond1 && this.cond2))
{
    System.Windows.Forms.Application.DoEvents();
}

Это работает, но кажется пустой тратой ресурсов и халтурой.

В основном я думаю, что мне нужен способ wait, но без блокировки потока. Как правильно это делать?

Кейс 2

Второй случай несколько похож (и связан) и иллюстрирует ту же проблему.
internal class MyClass
{
    private ApiNotifyClass apiNotifyClass;
    private shouldContinue = false;

    internal MyClass()
    {
        //in addition to the code from above
        this.apiNotifyClass = new ApiNotifyClass();
        this.apiNotifyClass.ApiFound += ApiNofityFoundEventHandler(ApiNotifyHandler);
    }

    internal void Send(SomethingToSend somethigToSend)
    {
        Verifyer verifier = this.apiObject.ApiGet(somethingToSend);
        this.apiNotifyClass.ApiAttach(verifier);

        //wait for the shouldContinue to be true

        this.apiObject.ApiSend(verifier);

        this.apiNotifyClass.ApiDetach(verifier);
    }

    private void ApiNotifyHandler()
    {
        this.shouldContinue = true;
    }
}

При вызове Send() будет создан объект Verifier, и метод должен дождаться выполнения ApiNotifyHandler (т. е. события ApiFound) перед вызовом ApiSend().

Таким образом, это та же самая ситуация, что и в случае 1 . Как я должен ждать, пока shouldContinue будет истинным ?

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

[обновление ]

Я вынужден использовать .Net 2.0.

3 2

3 ответа:

Лучший способ справиться с этим-перефакторировать код, чтобы использовать async/await и превратить ApiStateUpdate событие в ожидаемую задачу с TaskCompletionSource (шаблон ВП ).

Если вы действительно хотите синхронно ждать события в потоке пользовательского интерфейса, посмотрите на WaitWithDoEvents из здесь или CoWaitForMultipleHandles из здесь, они делают именно это. Имейте в виду, что этот подход создает вложенный модальный цикл сообщений, причем наиболее заметным следствием является возможное повторное включение кода (обсуждается подробно здесь ).

[отредактировано] то, что вы пытаетесь сделать здесь,-это асинхронно-синхронный мост, который почти всегда сам по себе плохая идея. Более того, я только что понял, что вы делаете это в конструкторе. Конструкторы, по своей природе, не должны иметь никакого асинхронного кода внутри, они атомарны. Всегда есть лучший способ учесть длительную процедуру инициализации из конструктора. @StephenCleary рассказывает об этом в своем очень информативном блоге сообщение .

Относительно ограничений .NET 2.0. Хотя async/await может быть революционной концепцией, Концепция государственной машины, стоящая за ней, не является чем-то новым. Вы всегда можете смоделировать его с помощью цепочки обратных вызовов делегатов и событий. Анонимные делегаты существуют с .NET 2.0. Например, ваш код может выглядеть следующим образом:

internal class MyClass
{
    private ApiObject apiObject;

    public event EventHandler Initialized;

    internal MyClass()
    {
        this.apiObject = new ApiObject();
    }

    public void Initialize()
    {
        ApiStateUpdateEventHandler handler = null;

        handler = delegate(string who, int howMuch) 
        {
            bool cond1 = false;
            bool cond2 = false;

            if(who.Equals("Something") && howMuch == 1)
                cond1 = true;
            else if(who.Equals("SomethingElse") && howMuch == 1)
                cond2 = true;           

            //wait for both conditions to be true

            if ( !cond1 && !cond2 )
                return;

            this.apiObject.ApiStateUpdate -= handler;

            // fire an event when both conditions are met
            if (this.Initialized != null)
                this.Initialized(this, new EventArgs());
        };

        this.apiObject.ApiStateUpdate += handler;
    }
}

Клиентский код, использующий MyClass, может выглядеть следующим образом:

MyClass myObject = new MyClass();
myObject.Initialized += delegate 
{
    MessageBox.Show("Hello!"); 
};
myObject.Initialize();

Выше было бы правильным асинхронным шаблоном на основе событий для .NET 2.0 . Более простым, но худшим решением было бы реализовать асинхронно-синхронный мост, используя WaitWithDoEvents (из здесь, основанный на MsgWaitForMultipleObjects), который может выглядеть следующим образом:

internal class MyClass
{
    private ApiObject apiObject;

    internal MyClass()
    {
        this.apiObject = new ApiObject();
        Initialize();
    }

    private void Initialize()
    {
        using (ManualResetEvent syncEvent = new ManualResetEvent())
        {
            ApiStateUpdateEventHandler handler = null;

            handler = delegate(string who, int howMuch) 
            {
                bool cond1 = false;
                bool cond2 = false;

                if(who.Equals("Something") && howMuch == 1)
                cond1 = true;
                else if(who.Equals("SomethingElse") && howMuch == 1)
                    cond2 = true;           

                //wait for both conditions to be true

                if ( !cond1 && !cond2 )
                    return;

                this.apiObject.ApiStateUpdate -= handler;

                syncEvent.Set();
            };

            this.apiObject.ApiStateUpdate += handler;
            WaitWithDoEvents(syncEvent, Timeout.Infinite);
        }
    }
}

Однако это было бы еще более эффективно, чем цикл напряженного ожидания от вашего вопроса:

while(!(this.cond1 && this.cond2))
{
    System.Windows.Forms.Application.DoEvents();
}

Вам придется выполнять блокирующий код асинхронно. В противном случае вы бы повесили поток пользовательского интерфейса, который не является хорошим. Существуют различные способы сделать это. Вот тот, который использует новые ключевые слова async и await. Я признаю сразу, что это много, чтобы проглотить и возможно только в .NET 4.5+.

Относительно Случая #1

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

private Task<ApiObject> CreateApiObjectAsync()
{
  bool cond1 = false;
  bool cond2 = false;

  var tcs = new TaskCompletionSource<ApiObject>();

  ApiObject instance = null;
  ApiStateEventHandler handler = null;

  handler = (who, howmuch) =>
    {
      cond1 = cond1 || (who == "Something" && howmuch == 1);
      cond2 = cond2 || (who == "SomethingElse" && howmuch == 1);
      if (cond1 && cond2)
      {
        instance.ApiStateUpdate -= handler;
        tcs.SetResult(instance);
      }
    }

  var instance = new ApiObject();
  instance.ApiStateUpdate += handler;
  return tcs.Task;
} 

Как только вы это сделаете, он будет использоваться вот так.

internal class MyClass
{
    private ApiObject apiObject;

    internal MyClass()
    {
      InitializeAsync();
    }

    private async Task InitializeAsync()
    {
      apiObject = await CreateApiObjectAsync();
      // At this point the instance is created and fully initialized.
    }
}

Я рекомендую вам прочитать блог Стивена Клири на асинхронной инициализации с использованием async и await, прежде чем вы это сделаете. На самом деле, прочитайте всю его асинхронную серию ООП. Это действительно хорошо.

Относительно Случая #2

Во многих отношениях с этим случаем легче иметь дело, потому что конструкторы и инициализация объекта не вступают в игру. Однако вам все равно придется использовать ту же стратегию, что и выше. Во-первых, преобразуйте стиль API EAP в стиль TAP. Если это условие ожидания не зависит от события из ApiObject , то просто выберите любую логику ожидания, которая вам нужна в методе TAP, и периодически оценивайте ее. Желательно, чтобы это было сделано без напряженного ожидания. Затем await Метод TAP ты создал. Однако не забудьте отметить Send как async при этом.

В основном я думаю, что мне нужен способ ждать, но не блокируя Нить.

Вы действительно хотите заблокировать поток, но не поток пользовательского интерфейса. Поэтому просто создайте другой поток и заблокируйте его. Сделайте его легким и используйте управление BackgroundWorker ().