"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:
- Execute a operação 1, quando o usuário clicar no botão
- Execute a operação 2 (continuar / concluir a operação 1), quando o usuário clicar no
Grid
com pelo menos duas restrições:
- 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.
- A operação 1 é sempre executada antes da operação 2. A operação 1 inicia a sequência.
- 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:
- Operação 1 concluída (ou interação necessária)
- 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 DispatcherObject
um 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 Dispatcher
uso de um Dispatcher.Invoke
ou Dispatcher.InvokeAsync
para 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 await
palavra - 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 async
que 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.
Aync/Await
como 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?