Como bloquear o fluxo de código até que um evento seja disparado em C #

9

Aqui temos um Gridcom a Button. Quando o usuário clica no botão, um método em uma classe Utility é executado, o que força o aplicativo a receber um clique em Grade. O fluxo de código deve parar aqui e não continuar até o usuário clicar no Grid.

Eu tive uma pergunta semelhante antes aqui:

Aguarde até o usuário clicar em C # WPF

Nessa pergunta, recebi uma resposta usando async / waitit, que funciona, mas como vou usá-lo como parte de uma API, não quero usar async / waitit, pois os consumidores terão que marcar seus métodos com assíncrono que eu não quero.

Como escrevo Utility.PickPoint(Grid grid)método para atingir esse objetivo?

Vi isso que pode ajudar, mas ainda não o entendi completamente, para ser sincero:

Bloqueando até que um evento seja concluído

Considere-o como algo como o método Console.ReadKey () em um aplicativo de console. Quando chamamos esse método, o fluxo de código para até inserirmos algum valor. O depurador não continua até inserirmos algo. Eu quero o comportamento exato para o método PickPoint (). O fluxo do código será interrompido até o usuário clicar na Grade.

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="3*"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>

        <Grid x:Name="View" Background="Green"/>
        <Button Grid.Row="1" Content="Pick" Click="ButtonBase_OnClick"/>
    </Grid>
</Window>

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        // do not continue the code flow until the user has clicked on the grid. 
        // so when we debug, the code flow will literally stop here.
        var point = Utility.PickPoint(View);


        MessageBox.Show(point.ToString());
    }
}

public static class Utility
{
    public static Point PickPoint(Grid grid)
    {

    }
}
Vahid
fonte
Maneira óbvia é Aync/Awaitcomo fazer a Operação A e salvar a operação STATE agora que você deseja que o usuário clique em Grade .. por isso, se o usuário clicar em Grade, você verificará o estado se verdadeiro e, em seguida, faça sua operação ou faça o que quiser?
Rao Hammas Hussain
@RaoHammasHussain Atualizei minha pergunta com um link que pode ajudar. O método utilitário fará parte de uma API que o usuário da API chamará sempre que desejar solicitar ao usuário final que clique na tela. Considere isso como uma janela de prompt para texto em aplicativos normais do Windows ou no método Console.Readline (). Nesses casos, o fluxo de código para até que o usuário insira algo. Agora eu quero exatamente, mas desta vez o usuário clica na tela.
Vahid
AutoResetEvent não é o que você quer?
Rao Hammas Hussain
@RaoHammasHussain Acho que sim, mas realmente não sei como usá-lo aqui.
Vahid 13/04
É como se você estivesse implementando intencionalmente WAIT STATE. é realmente necessário? porque você não pode simplesmente colocar isso var point = Utility.PickPoint(Grid grid);no método Grid Click? faça alguma operação e retorne a resposta?
Rao Hammas Hussain 13/04

Respostas:

7

"Como bloquear o fluxo de código até que um evento seja disparado?"

Sua abordagem está errada. Orientado a eventos não significa bloquear e aguardar um evento. Você nunca espera, pelo menos tenta sempre evitá-lo. Esperar está desperdiçando recursos, bloqueando threads e talvez introduzindo o risco de um deadlock ou thread zumbi (caso o sinal nunca seja gerado).
Deve ficar claro que o bloqueio de um encadeamento para aguardar um evento é um antipadrão, pois contradiz a ideia de um evento.

Você geralmente tem duas opções (modernas): implementar uma API assíncrona ou uma API orientada a eventos. Como você não deseja implementar sua API assíncrona, você fica com a API orientada a eventos.

A chave de uma API orientada a eventos é que, em vez de forçar o chamador a aguardar de forma síncrona por um resultado ou a pesquisar por um resultado, você deixa o chamador continuar e envia uma notificação a ele assim que o resultado estiver pronto ou a operação for concluída. Enquanto isso, o chamador pode continuar executando outras operações.

Ao analisar o problema de uma perspectiva de encadeamento, a API orientada a eventos permite que o encadeamento de chamada, por exemplo, o encadeamento da interface do usuário, que executa o manipulador de eventos do botão, seja livre para continuar lidando com operações relacionadas à interface do usuário, como renderizar elementos da interface do usuário ou manipular entrada do usuário, como movimento do mouse e pressionamentos de teclas. O mesmo efeito como uma API assíncrona, embora menos conveniente.

Como você não forneceu detalhes suficientes sobre o que realmente está tentando fazer, o que Utility.PickPoint()realmente está fazendo e qual é o resultado da tarefa ou por que o usuário deve clicar no botão `Grade, não posso oferecer uma solução melhor . Apenas posso oferecer um padrão geral de como implementar sua exigência.

Seu fluxo ou objetivo é obviamente dividido em pelo menos duas etapas para torná-lo uma sequência de operações:

  1. Execute a operação 1, quando o usuário clicar no botão
  2. Execute a operação 2 (continuar / concluir a operação 1), quando o usuário clicar no Grid

com pelo menos duas restrições:

  1. Opcional: a sequência deve ser concluída antes que o cliente da API possa repeti-la. Uma sequência é concluída quando a operação 2 é concluída.
  2. A operação 1 é sempre executada antes da operação 2. A operação 1 inicia a sequência.
  3. A operação 1 deve ser concluída antes que o cliente da API possa executar a operação 2

Isso requer duas notificações para o cliente da API para permitir a interação sem bloqueio:

  1. Operação 1 concluída (ou interação necessária)
  2. Operação 2 (ou objetivo) concluída

Você deve permitir que sua API implemente esse comportamento e restrições expondo dois métodos públicos e dois eventos públicos.

Implementar / refatorar API do utilitário

Utility.cs

class Utility
{
  public event EventHandler InitializePickPointCompleted;
  public event EventHandler<PickPointCompletedEventArgs> PickPointCompleted;
  private bool IsPickPointInitialized { get; set; }
  private bool IsExecutingSequence { get; set; }

  // The prefix 'Begin' signals the caller or client of the API, 
  // that he also has to end the sequence explicitly
  public void BeginPickPoint(param)
  {
    // Implement constraint 1
    if (this.IsExecutingSequence)
    {
      // Alternatively just return or use Try-do pattern
      throw new InvalidOperationException("BeginPickPoint is already executing. Call EndPickPoint before starting another sequence.");
    }

    // Set the flag that a current sequence is in progress
    this.IsExecutingSequence = true;

    // Execute operation until caller interaction is required.
    // Execute in background thread to allow API caller to proceed with execution.
    Task.Run(() => StartOperationNonBlocking(param));
  }

  public void EndPickPoint(param)
  {
    // Implement constraint 2 and 3
    if (!this.IsPickPointInitialized)
    {
      // Alternatively just return or use Try-do pattern
      throw new InvalidOperationException("BeginPickPoint must have completed execution before calling EndPickPoint.");
    }

    // Execute operation until caller interaction is required.
    // Execute in background thread to allow API caller to proceed with execution.
    Task.Run(() => CompleteOperationNonBlocking(param));
  }

  private void StartOperationNonBlocking(param)
  {
    ... // Do something

    // Flag the completion of the first step of the sequence (to guarantee constraint 2)
    this.IsPickPointInitialized = true;

    // Request caller interaction to kick off EndPickPoint() execution
    OnInitializePickPointCompleted();
  }

  private void CompleteOperationNonBlocking(param)
  {
    // Execute goal and get the result of the completed task
    Point result = ExecuteGoal();

    // Reset API sequence
    this.IsExecutingSequence = false;
    this.IsPickPointInitialized = false;

    // Notify caller that execution has completed and the result is available
    OnPickPointCompleted(result);
  }

  private void OnInitializePickPointCompleted()
  {
    // Set the result of the task
    this.InitializePickPointCompleted?.Invoke(this, EventArgs.Empty);
  }

  private void OnPickPointCompleted(Point result)
  {
    // Set the result of the task
    this.PickPointCompleted?.Invoke(this, new PickPointCompletedEventArgs(result));
  }
}

PickPointCompletedEventArgs.cs

class PickPointCompletedEventArgs : EventArgs
{
  public Point Result { get; }

  public PickPointCompletedEventArgs(Point result)
  {
    this.Result = result;
  }
}

Use a API

MainWindow.xaml.cs

partial class MainWindow : Window
{
  private Utility Api { get; set; }

  public MainWindow()
  {
    InitializeComponent();

    this.Api = new Utility();
  }

  private void StartPickPoint_OnButtonClick(object sender, RoutedEventArgs e)
  {
    this.Api.InitializePickPointCompleted += RequestUserInput_OnInitializePickPointCompleted;

    // Invoke API and continue to do something until the first step has completed.
    // This is possible because the API will execute the operation on a background thread.
    this.Api.BeginPickPoint();
  }

  private void RequestUserInput_OnInitializePickPointCompleted(object sender, EventArgs e)
  {
    // Cleanup
    this.Api.InitializePickPointCompleted -= RequestUserInput_OnInitializePickPointCompleted;

    // Communicate to the UI user that you are waiting for him to click on the screen
    // e.g. by showing a Popup, dimming the screen or showing a dialog.
    // Once the input is received the input event handler will invoke the API to complete the goal   
    MessageBox.Show("Please click the screen");  
  }

  private void FinishPickPoint_OnGridMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  {
    this.Api.PickPointCompleted += ShowPoint_OnPickPointCompleted;

    // Invoke API to complete the goal
    // and continue to do something until the last step has completed
    this.Api.EndPickPoint();
  }

  private void ShowPoint_OnPickPointCompleted(object sender, PickPointCompletedEventArgs e)
  {
    // Cleanup
    this.Api.PickPointCompleted -= ShowPoint_OnPickPointCompleted;

    // Get the result from the PickPointCompletedEventArgs instance
    Point point = e.Result;

    // Handle the result
    MessageBox.Show(point.ToString());
  }
}

MainWindow.xaml

<Window>
  <Grid MouseLeftButtonUp="FinishPickPoint_OnGridMouseLeftButtonUp">
    <Button Click="StartPickPoint_OnButtonClick" />
  </Grid>
</Window>

Observações

Eventos gerados em um encadeamento em segundo plano executam seus manipuladores no mesmo encadeamento. O acesso a DispatcherObjectum elemento de interface do usuário de um manipulador, que é executado em um encadeamento em segundo plano, requer que a operação crítica seja enfileirada para o Dispatcheruso de um Dispatcher.Invokeou Dispatcher.InvokeAsyncpara evitar exceções de encadeamento.


Algumas reflexões - responda aos seus comentários

Como você estava se aproximando de mim para encontrar uma solução "melhor" de bloqueio, me deu o exemplo de aplicativos de console, senti-me convencido de que sua percepção ou ponto de vista está totalmente errado.

"Considere um aplicativo de console com essas duas linhas de código.

var str = Console.ReadLine(); 
Console.WriteLine(str);

O que acontece quando você executa o aplicativo no modo de depuração. Ele irá parar na primeira linha do código e forçará você a inserir um valor na interface do usuário do console e, depois de inserir algo e pressionar Enter, ele executará a próxima linha e realmente imprimirá o que você digitou. Eu estava pensando exatamente no mesmo comportamento, mas no aplicativo WPF ".

Um aplicativo de console é algo totalmente diferente. O conceito de segmentação é diferente. Os aplicativos de console não têm uma GUI. Apenas fluxos de entrada / saída / erro. Você não pode comparar a arquitetura de um aplicativo de console com um aplicativo GUI avançado. Isso não vai funcionar. Você realmente deve entender e aceitar isso.

O WPF é criado em torno de um thread de renderização e de um thread da interface do usuário. Esses threads continuam sempre girando para se comunicar com o sistema operacional, como lidar com a entrada do usuário - mantendo o aplicativo responsivo . Você nunca deseja pausar / bloquear esse segmento, pois isso impedirá que a estrutura faça um trabalho essencial em segundo plano (como responder a eventos do mouse - você não deseja que o mouse congele):

espera = bloqueio de thread = falta de resposta = UX ruim = usuários / clientes irritados = problemas no escritório.

Às vezes, o fluxo do aplicativo exige que a entrada ou a rotina seja concluída. Mas não queremos bloquear o segmento principal.
É por isso que as pessoas inventaram modelos de programação assíncronos complexos, para permitir a espera sem bloquear o encadeamento principal e sem forçar o desenvolvedor a escrever código de multithreading complicado e incorreto.

Toda estrutura de aplicativo moderna oferece operações assíncronas ou um modelo de programação assíncrono, para permitir o desenvolvimento de código simples e eficiente.

O fato de você estar se esforçando para resistir ao modelo de programação assíncrona mostra alguma falta de entendimento para mim. Todo desenvolvedor moderno prefere uma API assíncrona a uma síncrona. Nenhum desenvolvedor sério se preocupa em usar a awaitpalavra - chave ou em declarar seu método async. Ninguém. Você é o primeiro a encontrar pessoas que reclamam de APIs assíncronas e que as consideram inconvenientes de usar.

Se eu verificar sua estrutura, que é um objetivo para resolver problemas relacionados à interface do usuário ou facilitar as tarefas relacionadas à interface do usuário, espero que seja assíncrona - o tempo todo.
A API relacionada à interface do usuário que não é assíncrona é um desperdício, pois complicará meu estilo de programação, portanto, meu código, que se torna mais suscetível a erros e difícil de manter.

Uma perspectiva diferente: quando você reconhece que a espera bloqueia o encadeamento da interface do usuário, criando uma experiência do usuário muito ruim e indesejável, pois a interface congela até a espera terminar, agora que você percebe isso, por que ofereceria uma API ou um modelo de plug-in que incentive um desenvolvedor para fazer exatamente isso - implementar espera?
Você não sabe o que o plug-in de terceiros fará e quanto tempo uma rotina levará até ser concluída. Este é simplesmente um design de API ruim. Quando sua API opera no encadeamento da interface do usuário, o chamador da sua API deve poder fazer chamadas sem bloqueio.

Se você negar a única solução barata ou graciosa, use uma abordagem orientada a eventos, como mostrado no meu exemplo.
Ele faz o que você deseja: iniciar uma rotina - aguardar a entrada do usuário - continuar a execução - atingir a meta.

Eu realmente tentei várias vezes para explicar por que esperar / bloquear é um design de aplicativo ruim. Novamente, você não pode comparar uma interface do usuário do console com uma interface gráfica rica, onde, por exemplo, o tratamento de entrada por si só é uma multidão mais complexa do que apenas ouvir o fluxo de entrada. Realmente não sei o seu nível de experiência e onde você começou, mas você deve começar a adotar o modelo de programação assíncrona. Não sei o motivo pelo qual você tenta evitá-lo. Mas não é nada sábio.

Hoje, os modelos de programação assíncrona são implementados em qualquer lugar, em qualquer plataforma, compilador, ambiente, navegador, servidor, desktop, banco de dados - em qualquer lugar. O modelo orientado a eventos permite atingir o mesmo objetivo, mas é menos conveniente de usar (inscrever-se / cancelar a inscrição de / para eventos), contando com encadeamentos em segundo plano. O controle de eventos é antiquado e deve ser usado apenas quando bibliotecas assíncronas não estão disponíveis ou não são aplicáveis.

"Vi o comportamento exato no Autodesk Revit."

O comportamento (o que você experimenta ou observa) é muito diferente de como essa experiência é implementada. Duas coisas diferentes É muito provável que a sua Autodesk esteja usando bibliotecas assíncronas ou recursos de linguagem ou algum outro mecanismo de encadeamento. E também é relacionado ao contexto. Quando o método que está em sua mente está executando em um encadeamento em segundo plano, o desenvolvedor pode optar por bloquear esse encadeamento. Ele tem uma boa razão para fazer isso ou apenas fez uma má escolha de design. Você está totalmente no caminho errado;) O bloqueio não é bom.
(O código-fonte da Autodesk é de código aberto? Ou como você sabe como é implementado?)

Eu não quero te ofender, por favor acredite em mim. Mas, reconsidere a implementação da sua API assíncrona. É apenas na sua cabeça que os desenvolvedores não gostam de usar async / waitit. Você obviamente entendeu errado. E esqueça o argumento do aplicativo de console - é um absurdo;)

API relacionada à interface do usuário DEVE usar async / wait sempre que possível. Caso contrário, você deixará todo o trabalho para escrever um código sem bloqueio no cliente da sua API. Você me forçaria a agrupar todas as chamadas à sua API em um thread em segundo plano. Ou para usar um tratamento de eventos menos confortável. Acredite em mim - todo desenvolvedor prefere decorar seus membros do asyncque manipular eventos. Toda vez que você usa eventos, você pode arriscar um possível vazamento de memória - depende de algumas circunstâncias, mas o risco é real e não é raro na programação descuidada.

Eu realmente espero que você entenda por que o bloqueio é ruim. Eu realmente espero que você decida usar async / waitit para escrever uma API assíncrona moderna. No entanto, mostrei a você uma maneira muito comum de esperar sem bloqueio, usando eventos, embora exorto você a usar async / waitit.

"A API permitirá que o programador tenha acesso à interface do usuário e etc. Agora, suponha que o programador deseje desenvolver um complemento que, quando um botão é clicado, o usuário final é solicitado a escolher um ponto na interface do usuário"

Se você não deseja permitir que o plug-in tenha acesso direto aos elementos da interface do usuário, forneça uma interface para delegar eventos ou expor componentes internos por meio de objetos abstratos.
A API internamente se inscreverá em eventos da interface do usuário em nome do suplemento e, em seguida, delega o evento expondo um evento "wrapper" correspondente ao cliente da API. Sua API deve oferecer alguns ganchos nos quais o suplemento pode se conectar para acessar componentes de aplicativos específicos. Uma API de plug-in atua como um adaptador ou fachada para fornecer acesso externo a internos.
Para permitir um certo grau de isolamento.

Veja como o Visual Studio gerencia plug-ins ou nos permite implementá-los. Finja que deseja escrever um plug-in para o Visual Studio e faça alguma pesquisa sobre como fazer isso. Você perceberá que o Visual Studio expõe seus componentes internos por meio de uma interface ou API. Por exemplo, você pode manipular o editor de código ou obter informações sobre o conteúdo do editor sem acesso real a ele.

BionicCode
fonte
Olá, obrigado por abordar a questão de outra perspectiva. Desculpe se a pergunta foi um pouco vaga em detalhes. Considere um aplicativo de console com essas duas linhas de código. var str = Console.ReadLine(); Console.WriteLine(str);O que acontece quando você executa o aplicativo no modo de depuração. Ele irá parar na primeira linha do código e forçará você a inserir um valor na interface do usuário do console e, depois de inserir algo e pressionar Enter, ele executará a próxima linha e realmente imprimirá o que você digitou. Eu estava pensando exatamente o mesmo comportamento, mas no aplicativo WPF.
Vahid
No aplicativo CAD que estou desenvolvendo, os usuários devem poder estendê-lo por addins / plugins. A API permitirá que o programador tenha acesso à interface do usuário e etc. Agora, suponha que o programador deseje desenvolver um complemento que, ao clicar em um botão, o usuário final seja solicitado a escolher um ponto na interface do usuário e, em seguida, o código fará outras coisas legais com o ponto dado. Talvez eles solicitem que outro ponto seja escolhido e desenhem uma linha, etc.
Vahid
Eu vi o comportamento exato no Autodesk Revit.
Vahid
11
Eu tenho algo a dizer sobre sua exigência. Por favor, leia minha resposta atualizada. Publiquei a resposta lá porque ficou um pouco mais longa. Eu admito que você realmente me provocou. Por favor, ao ler, lembre-se de que não quero ofendê-lo.
BionicCode 17/04
Obrigado pela sua resposta atualizada. Claro, não estou ofendido. Pelo contrário, estou muito agradecido pelo tempo e esforço que você dedicou a isso.
Vahid
5

Pessoalmente, acho que isso está sendo muito complicado para todos, mas talvez eu não esteja entendendo completamente o motivo pelo qual isso precisa ser feito de uma certa maneira, mas parece que uma simples verificação de bool pode ser usada aqui.

Em primeiro lugar, torne sua grade testável, definindo as propriedades Backgrounde IsHitTestVisible, caso contrário ela nem capturará cliques do mouse.

<grid MouseLeftButtonUp="Grid_MouseLeftButtonUp" IsHitTestVisible="True" Background="Transparent">

Em seguida, crie um valor bool que possa armazenar se o evento "GridClick" deve ocorrer. Quando a grade é clicada, verifique esse valor e a execução do evento de clique da grade, se estiver aguardando o clique.

Exemplo:

bool awaitingClick = false;


private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
   awaitingClick=true;
}

private void Grid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{     
     //Stop here if the program shouldn't do anything when grid is clicked
     if (!awaitingClick) { return; } 

     //Run event
     var point = Utility.PickPoint(View);
     MessageBox.Show(point.ToString());

     awaitingClick=false;//Reset
}
Tronald
fonte
Olá Tronald, acho que você não entendeu a pergunta. O que eu preciso é que o código pare no Utility.PickPoint (Exibir) e continue somente depois que o usuário clicar em Grade.
Vahid
Sim, eu entendi totalmente. Minhas desculpas, eu não sabia que você precisava de tudo para realmente parar. Acho que isso não é possível sem o multi-threading, pois a interface do usuário inteira será bloqueada.
Tronald 15/04
Ainda não tenho certeza se isso não é possível. Definitivamente, é possível com o assíncrono / espera, que não é uma solução multiencadeada. Mas o que eu preciso é de uma alternativa à solução async / wait.
Vahid
11
Claro, mas você mencionou que não pode usar async / waitit. Parece que você precisaria fazer uso de um expedidor e de um thread separado do thread principal (que é executado na interface do usuário). Espero que você encontre outro caminho, pois estou interessado
Tronald 15/04
2

Eu tentei algumas coisas, mas eu sou incapaz de fazê-lo sem async/await. Porque, se não o usarmos, ele causa DeadLockou a interface do usuário está bloqueada e, em seguida, podemos ativar a Grid_Clickentrada.

private async void ToolBtn_OnClick(object sender, RoutedEventArgs e)
{
    var senderBtn = sender as Button;
    senderBtn.IsEnabled = false;

    var response = await Utility.PickPoint(myGrid);
    MessageBox.Show(response.ToString());
    senderBtn.IsEnabled = true;
}  

public static class Utility
{
    private static TaskCompletionSource<bool> tcs;
    private static Point _point = new Point();

    public static async Task<Point> PickPoint(Grid grid)
    {
        tcs = new TaskCompletionSource<bool>();
        _point = new Point();

        grid.MouseLeftButtonUp += GridOnMouseLeftButtonUp;


        await tcs.Task;

        grid.MouseLeftButtonUp -= GridOnMouseLeftButtonUp;
        return _point;
    }


    private static void GridOnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {

        // do something here ....
        _point = new Point { X = 23, Y = 34 };
        // do something here ....

        tcs.SetResult(true); // as soon its set it will go back

    }
}
Rao Hammas Hussain
fonte
@ obrigado, esta é a mesma resposta que recebi para minha outra pergunta que usa async / waitit.
Vahid
Ai sim ! notei isso agora, mas acho que é a única maneira que encontrei que está funcionando.
Rao Hammas Hussain 14/04
2

Você pode bloquear assincronamente usando um SemaphoreSlim:

public partial class MainWindow : Window, IDisposable
{
    private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(0, 1);

    public MainWindow()
    {
        InitializeComponent();
    }

    private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        var point = Utility.PickPoint(View);

        // do not continue the code flow until the user has clicked on the grid. 
        // so when we debug, the code flow will literally stop here.
        await _semaphoreSlim.WaitAsync();

        MessageBox.Show(point.ToString());
    }

    private void View_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        //click on grid detected....
        _semaphoreSlim.Release();
    }

    protected override void OnClosed(EventArgs e)
    {
        base.OnClosed(e);
        Dispose();
    }

    public void Dispose() => _semaphoreSlim.Dispose();
}

Você não pode nem deseja bloquear o encadeamento do expedidor de forma síncrona, porque nunca será capaz de manipular o clique no Grid, ou seja, não pode ser bloqueado nem manipular eventos ao mesmo tempo.

mm8
fonte
Obrigado por uma resposta alternativa. Gostaria de saber como é feito, por exemplo, em Console.Readline ()? Quando você alcança esse método no depurador, ele magicamente pára por aí, a menos que digamos alguma coisa? É fundamentalmente diferente nos aplicativos de console? Não podemos ter o mesmo comportamento no aplicativo WinForms / WPF? Eu já vi isso na API do Autodesk Revit, existe um método PickPoint () que o força a escolher um ponto na tela e não vi nenhum assíncrono / espera usado! É pelo menos possível ocultar a palavra-chave wait e chamá-la de alguma forma a partir de um método de sincronização?
Vahid 14/04
@Vahid: Console.Readline blocks , ou seja, ele não retorna até que uma linha seja lida. Seu PickPointmétodo não. Retorna imediatamente. Poderia bloquear potencialmente, mas você não conseguirá lidar com a entrada da interface do usuário enquanto eu escrevi na minha resposta. Em outras palavras, você precisaria manipular o clique dentro do método para obter o mesmo comportamento.
mm8 15/04
O Console.Realine () bloqueia, mas ao mesmo tempo, os eventos KeyPress são permitidos. Não podemos ter exatamente o mesmo comportamento aqui? Bloqueando por PickPoint () e permitindo apenas MouseEvents? Não consigo entender por que isso é possível no console, mas não em um aplicativo baseado em interface do usuário.
Vahid
Então você precisa configurar um despachante separado PickPointque lide com os eventos do mouse. Não consigo ver para onde você está indo com isso?
mm8 15/04
11
@Ahind: faça o código assíncrono e deixe o usuário aguardar o método então? Essa é a API que eu esperaria como desenvolvedor de interface do usuário. Chamar um método de bloqueio em um aplicativo de interface do usuário não faz sentido.
mm8 15/04
2

Tecnicamente, é possível com AutoResetEvente sem async/await, mas há uma desvantagem significativa:

public static Point PickPoint(Grid grid)
{
    var pointPicked = new AutoResetEvent(false);
    grid.MouseLeftButtonUp += (s, e) => 
    {
        // do whatever after the grid is clicked

        // signal the end of waiting
        pointPicked.Set();
    };

    // code flow will stop here and wait until the grid is clicked
    pointPicked.WaitOne();
    // return something...
}

A desvantagem: se você chamar esse método diretamente em um manipulador de eventos de botão como o seu código de exemplo, ocorrerá um conflito e você verá que o aplicativo para de responder. Como você está usando o único thread da interface do usuário para aguardar o clique do usuário, ele não pode responder à ação de nenhum usuário, incluindo o clique do usuário na grade.

Os consumidores do método devem invocá-lo em outro encadeamento para evitar conflitos. Se pode ser garantido, tudo bem. Caso contrário, você precisará invocar o método como este:

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
    // here I used ThreadPool, but you may use other means to run on another thread
    ThreadPool.QueueUserWorkItem(new WaitCallback(Capture));
}

private void Capture(object state)
{
    // do not continue the code flow until the user has clicked on the grid. 
    // so when we debug, the code flow will literally stop here.
    var point = Utility.PickPoint(View);


    MessageBox.Show(point.ToString());
}

Isso pode causar mais problemas para os consumidores da sua API, exceto se eles costumavam gerenciar seus próprios threads. Por isso async/awaitfoi inventado.

Ken Hung
fonte
Obrigado Ken, é possível que o suplemento inicie a partir de outro segmento e, em seguida, seus eventos não bloqueiem o segmento principal da interface do usuário?
Vahid 20/04
@Vahid Sim e não. Sim, você pode chamar o método de bloqueio em outro thread e envolvê-lo em outro método. No entanto, o método do invólucro ainda precisava ser chamado em outro segmento que não seja o segmento da interface do usuário para evitar o bloqueio da interface do usuário. Porque o wrapper bloqueará o encadeamento de chamada, se for síncrono . Embora internamente o wrapper bloqueie outro encadeamento, ele ainda precisará aguardar o resultado e bloquear o encadeamento de chamada. Se o chamador chamar o método wrapper no thread da interface do usuário, a interface do usuário será bloqueada.
Ken Hung
0

Eu acho que a questão está no próprio design. Se sua API funcionar em um elemento específico, ela deverá ser usada no manipulador de eventos desse mesmo elemento, não em outro elemento.

Por exemplo, aqui queremos obter a posição do evento click na grade, a API precisa ser usada no manipulador de eventos associado ao evento no elemento Grid, não no elemento button.

Agora, se o requisito é manipular o clique na Grade somente depois que clicarmos no Botão, a responsabilidade do Botão será adicionar o manipulador de eventos à Grade, e o evento clique na Grade mostrará a caixa de mensagem e removerá este manipulador de eventos adicionado pelo botão para que não seja mais acionado após esse clique ... (não é necessário bloquear o thread da interface do usuário)

Apenas para dizer que, se você bloquear o thread da interface do usuário no clique do botão, acho que o thread da interface do usuário não poderá acionar o evento click na grade posteriormente.

jsami
fonte
0

Primeiro de tudo, o thread da interface do usuário não pode ser bloqueado como a resposta que você recebeu da sua pergunta inicial.
Se você pode concordar com isso, é possível evitar async / wait para fazer com que seu cliente faça menos modificações, e nem precisa de multithreading.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        Utility.PickPoint(View, (x) => MessageBox.Show(x.ToString()));
    }
}

public static class Utility
{
    private static Action<Point> work;

    public static void PickPoint(Grid grid, Action<Point> work)
    {
        if (Utility.work == null)
        {
            grid.PreviewMouseLeftButtonUp += Grid_PreviewMouseLeftButtonUp;
            Utility.work = work;
        }
    }

    private static void Grid_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        var grid = (Grid)sender;
        work.Invoke(e.GetPosition(grid));
        grid.PreviewMouseLeftButtonUp -= Grid_PreviewMouseLeftButtonUp;
        Utility.work = null;
    }
}   

Mas se você deseja bloquear o thread da interface do usuário ou o "fluxo de código", a resposta será que é impossível. Porque se o encadeamento da interface do usuário foi bloqueado, não há mais entrada a ser recebida.
Já que você mencionou sobre o aplicativo de console, eu apenas faço uma explicação simples.
Quando você executa um aplicativo de console ou chama AllocConsolede um processo que não foi anexado a nenhum console (janela), o conhost.exe, que pode fornecer um console (janela), é executado e o processo do aplicativo ou chamador de console é anexado ao console ( janela).
Portanto, qualquer código que você escreva que possa bloquear o encadeamento do chamador, como Console.ReadKeynão bloqueará o encadeamento da interface do usuário da janela do console, esse é o motivo pelo qual o aplicativo do console aguarda sua entrada, mas ainda pode responder a outras entradas, como clicar com o mouse.

Alex.Wei
fonte