"¿Cómo bloquear el flujo de código hasta que se active un evento?"
Tu enfoque es incorrecto. Impulsado por eventos no significa bloquear y esperar un evento. Nunca esperas, al menos siempre te esfuerzas por evitarlo. Esperar es desperdiciar recursos, bloquear subprocesos y tal vez introducir el riesgo de un punto muerto o un hilo zombie (en caso de que la señal de liberación nunca se eleve).
Debe quedar claro que bloquear un hilo para esperar un evento es un antipatrón, ya que contradice la idea de un evento.
Generalmente tiene dos opciones (modernas): implementar una API asincrónica o una API controlada por eventos. Como no desea implementar su API de forma asincrónica, le queda la API controlada por eventos.
La clave de una API controlada por eventos es que, en lugar de obligar a la persona que llama a esperar sincrónicamente un resultado o sondear un resultado, deja que la persona que llama continúe y le envíe una notificación, una vez que el resultado esté listo o la operación se haya completado. Mientras tanto, la persona que llama puede continuar ejecutando otras operaciones.
Cuando se mira el problema desde una perspectiva de subprocesos, la API controlada por eventos permite que el subproceso de llamada, por ejemplo, el subproceso de interfaz de usuario, que ejecuta el controlador de eventos del botón, pueda continuar manejando, por ejemplo, otras operaciones relacionadas con la interfaz de usuario, como representar elementos de la interfaz de usuario o manejar la entrada del usuario como el movimiento del mouse y las pulsaciones de teclas. La API controlada por eventos tiene el mismo efecto u objetivo que una API asincrónica, aunque es mucho menos conveniente.
Dado que no proporcionó suficientes detalles sobre lo que realmente está tratando de hacer, qué Utility.PickPoint()
está haciendo realmente y cuál es el resultado de la tarea o por qué el usuario tiene que hacer clic en la `Cuadrícula, no puedo ofrecerle una mejor solución . Solo puedo ofrecer un patrón general de cómo implementar su requisito.
Su flujo o la meta obviamente se divide en al menos dos pasos para que sea una secuencia de operaciones:
- Ejecute la operación 1, cuando el usuario hace clic en el botón
- Ejecute la operación 2 (continuar / completar la operación 1), cuando el usuario hace clic en
Grid
con al menos dos restricciones:
- Opcional: la secuencia debe completarse antes de que el cliente API pueda repetirla. Una secuencia se completa una vez que la operación 2 se ha ejecutado hasta su finalización.
- La operación 1 siempre se ejecuta antes de la operación 2. La operación 1 inicia la secuencia.
- La operación 1 debe completarse antes de que el cliente API pueda ejecutar la operación 2
Esto requiere dos notificaciones para el cliente de la API para permitir la interacción sin bloqueo:
- Operación 1 completada (o interacción requerida)
- Operación 2 (u objetivo) completada
Debe permitir que su API implemente este comportamiento y restricciones exponiendo dos métodos públicos y dos eventos públicos.
Implementar / refactorizar API de utilidad
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;
}
}
Usa la 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>
Observaciones
Los eventos generados en un subproceso en segundo plano ejecutarán sus controladores en el mismo subproceso. Para acceder a DispatcherObject
un elemento similar a una interfaz de usuario desde un controlador, que se ejecuta en un subproceso en segundo plano, se requiere que la operación crítica se coloque en cola para Dispatcher
usar Dispatcher.Invoke
o Dispatcher.InvokeAsync
para evitar excepciones entre subprocesos.
Lea las observaciones sobre DispatcherObject
para aprender más sobre este fenómeno llamado afinidad de despachador o afinidad de subprocesos.
Algunas reflexiones - responde a tus comentarios
Debido a que se estaba acercando a mí para encontrar una solución de bloqueo "mejor", dado el ejemplo de las aplicaciones de consola, sentí convencerlo de que su percepción o punto de vista es totalmente erróneo.
"Considere una aplicación de consola con estas dos líneas de código.
var str = Console.ReadLine();
Console.WriteLine(str);
Qué sucede cuando ejecuta la aplicación en modo de depuración. Se detendrá en la primera línea de código y lo obligará a ingresar un valor en la interfaz de usuario de la consola y luego, después de ingresar algo y presionar Enter, ejecutará la siguiente línea e imprimirá lo que ingresó. Estaba pensando exactamente en el mismo comportamiento pero en la aplicación WPF ".
Una aplicación de consola es algo totalmente diferente. El concepto de enhebrado es un poco diferente. Las aplicaciones de consola no tienen una GUI. Solo flujos de entrada / salida / error. No puede comparar la arquitectura de una aplicación de consola con una aplicación GUI enriquecida. Esto no funcionara. Realmente debes entender y aceptar esto.
Tampoco te dejes engañar por el aspecto . ¿Sabes lo que está pasando adentro Console.ReadLine
? ¿Cómo se implementa ? ¿Está bloqueando el hilo principal y en paralelo lee la entrada? ¿O es solo una votación?
Aquí está la implementación original de Console.ReadLine
:
public virtual String ReadLine()
{
StringBuilder sb = new StringBuilder();
while (true)
{
int ch = Read();
if (ch == -1)
break;
if (ch == '\r' || ch == '\n')
{
if (ch == '\r' && Peek() == '\n')
Read();
return sb.ToString();
}
sb.Append((char)ch);
}
if (sb.Length > 0)
return sb.ToString();
return null;
}
Como puede ver, es una operación síncrona simple . Sondea la entrada del usuario en un bucle "infinito". Sin bloqueo mágico y continuar.
WPF se basa en un subproceso de representación y un subproceso de interfaz de usuario. Esos hilos siempre giran para comunicarse con el sistema operativo, como manejar la entrada del usuario, manteniendo la aplicación receptiva . Nunca querrá pausar / bloquear este hilo, ya que evitará que el marco realice un trabajo de fondo esencial, como responder a eventos del mouse; no desea que el mouse se congele:
esperando = bloqueo de hilos = falta de respuesta = mala experiencia de usuario = usuarios / clientes molestos = problemas en la oficina.
A veces, el flujo de la aplicación requiere esperar la entrada o completar una rutina. Pero no queremos bloquear el hilo principal.
Es por eso que la gente inventó modelos complejos de programación asincrónica, para permitir la espera sin bloquear el hilo principal y sin obligar al desarrollador a escribir código de subprocesos múltiples complicado y erróneo.
Cada marco de aplicación moderno ofrece operaciones asincrónicas o un modelo de programación asincrónica, para permitir el desarrollo de código simple y eficiente.
El hecho de que se esfuerce por resistir el modelo de programación asíncrono me muestra cierta falta de comprensión. Todo desarrollador moderno prefiere una API asincrónica sobre una síncrona. Ningún desarrollador serio se preocupa por usar la await
palabra clave o declarar su método async
. Nadie. Eres el primero con el que me encuentro que se queja de las API asincrónicas y las encuentra inconvenientes de usar.
Si verificara su marco, qué objetivos resolver problemas relacionados con la interfaz de usuario o facilitar las tareas relacionadas con la interfaz de usuario, esperaría que fuera asíncrono todo el tiempo.
La API relacionada con la interfaz de usuario que no es asíncrona es un desperdicio, ya que complicará mi estilo de programación, por lo tanto, mi código, que por lo tanto es más propenso a errores y difícil de mantener.
Una perspectiva diferente: cuando reconoce que la espera bloquea el hilo de la interfaz de usuario, está creando una experiencia de usuario muy mala e indeseable ya que la interfaz de usuario se congelará hasta que termine la espera, ahora que se da cuenta de esto, ¿por qué ofrecería una API o un modelo de complemento que alienta a un desarrollador a hacer exactamente esto: ¿implementar la espera?
No sabe qué hará el complemento de terceros y cuánto tiempo llevará una rutina hasta que se complete. Esto es simplemente un mal diseño de API. Cuando su API opera en el subproceso de la interfaz de usuario, la persona que llama de su API debe poder realizar llamadas sin bloqueo.
Si niega la única solución barata o elegante, utilice un enfoque basado en eventos como se muestra en mi ejemplo.
Hace lo que desea: iniciar una rutina - esperar la entrada del usuario - continuar la ejecución - cumplir el objetivo.
Realmente intenté varias veces explicar por qué esperar / bloquear es un mal diseño de la aplicación. Una vez más, no puede comparar una interfaz de usuario de consola con una interfaz gráfica de usuario rica, donde, por ejemplo, el manejo de entrada solo es una multitud más compleja que simplemente escuchar la secuencia de entrada. Realmente no sé su nivel de experiencia y dónde comenzó, pero debe comenzar a adoptar el modelo de programación asincrónica. No sé la razón por la que intentas evitarlo. Pero no es sabio en absoluto.
Hoy en día, los modelos de programación asincrónica se implementan en todas partes, en cada plataforma, compilador, cada entorno, navegador, servidor, escritorio, base de datos, en todas partes. El modelo basado en eventos permite lograr el mismo objetivo, pero es menos conveniente usarlo (suscribirse / cancelar suscripción a / desde eventos, leer documentos (cuando hay documentos) para aprender sobre los eventos), confiando en hilos de fondo. La gestión de eventos está pasada de moda y solo debe usarse cuando las bibliotecas asíncronas no están disponibles o no son aplicables.
Como nota al margen: el .NET Framwork (.NET Standard) ofrece TaskCompletionSource
(entre otros propósitos) proporcionar una manera simple de convertir una API existente controlada en una API asincrónica.
"He visto el comportamiento exacto en Autodesk Revit".
El comportamiento (lo que experimenta u observa) es muy diferente de cómo se implementa esta experiencia. Dos cosas diferentes Es muy probable que su Autodesk utilice bibliotecas asincrónicas o funciones de lenguaje o algún otro mecanismo de subprocesamiento. Y también está relacionado con el contexto. Cuando el método que tiene en mente se ejecuta en un subproceso en segundo plano, el desarrollador puede optar por bloquear este subproceso. Tiene una muy buena razón para hacer esto o simplemente hizo una mala elección de diseño. Estás totalmente en el camino equivocado;) El bloqueo no es bueno.
(¿El código fuente de Autodesk es de código abierto? ¿O cómo sabes cómo se implementa?)
No quiero ofenderte, por favor créeme. Pero reconsidere implementar su API de forma asincrónica. Es solo en su cabeza que a los desarrolladores no les gusta usar async / await. Obviamente tienes la mentalidad equivocada. Y olvídate del argumento de la aplicación de consola: no tiene sentido;)
La API relacionada con la interfaz de usuario DEBE usar async / wait siempre que sea posible. De lo contrario, deja todo el trabajo para escribir código sin bloqueo al cliente de su API. Me obligarías a ajustar cada llamada a tu API en un hilo de fondo. O para usar un manejo de eventos menos cómodo. Créame, cada desarrollador en lugar de decorar a sus miembros async
, en lugar de hacer el manejo de eventos. Cada vez que usa eventos, puede correr el riesgo de una posible pérdida de memoria, depende de algunas circunstancias, pero el riesgo es real y no raro cuando se programa descuidadamente.
Realmente espero que entiendas por qué el bloqueo es malo. Realmente espero que decidas usar async / wait para escribir una API asincrónica moderna. Sin embargo, le mostré una forma muy común de esperar sin bloqueo, usando eventos, aunque le insto a que use async / wait.
"La API permitirá que el programador tenga acceso a la interfaz de usuario, etc. Ahora suponga que el programador desea desarrollar un complemento que cuando se hace clic en un botón, se le pide al usuario final que elija un punto en la interfaz de usuario"
Si no desea permitir que el complemento tenga acceso directo a los elementos de la interfaz de usuario, debe proporcionar una interfaz para delegar eventos o exponer componentes internos a través de objetos abstraídos.
La API internamente se suscribirá a los eventos de la interfaz de usuario en nombre del complemento y luego delegará el evento exponiendo un evento "envoltorio" correspondiente al cliente API. Su API debe ofrecer algunos enlaces donde el complemento puede conectarse para acceder a componentes específicos de la aplicación. Una API de complemento actúa como un adaptador o fachada para dar acceso externo a los internos.
Para permitir un cierto grado de aislamiento.
Eche un vistazo a cómo Visual Studio administra los complementos o nos permite implementarlos. Imagina que quieres escribir un complemento para Visual Studio e investiga un poco sobre cómo hacerlo. Te darás cuenta de que Visual Studio expone sus elementos internos a través de una interfaz o API. Por ejemplo, puede manipular el editor de código u obtener información sobre el contenido del editor sin acceso real al mismo.
Aync/Await
¿qué hay de hacer la Operación A y guardar la operación ESTADO ahora que desea que el usuario haga clic en Cuadrícula? Entonces, si el usuario hace clic en la Cuadrícula, verificará el estado si es verdadero, ¿entonces su operación solo hará lo que quiera?