Cómo bloquear el flujo de código hasta que se active un evento en C #

10

Aquí tenemos un Gridcon a Button. Cuando el usuario hace clic en el botón, se ejecuta un método en una clase de utilidad que obliga a la aplicación a recibir un clic en la cuadrícula. El flujo de código debe detenerse aquí y no continuar hasta que el usuario haya hecho clic en Grid.

He tenido una pregunta similar antes aquí:

Espere hasta que el usuario haga clic en C # WPF

En esa pregunta, recibí una respuesta usando async / await que funciona, pero como voy a usarlo como parte de una API, no quiero usar async / await, ya que los consumidores tendrán que marcar sus métodos con asíncrono que no quiero.

¿Cómo escribo el Utility.PickPoint(Grid grid)método para lograr este objetivo?

Vi esto que puede ayudar, pero no entendí completamente que se aplique aquí para ser honesto:

Bloqueo hasta que se complete un evento

Considérelo como algo así como el método Console.ReadKey () en una aplicación de consola. Cuando llamamos a este método, el flujo del código se detiene hasta que ingresamos algún valor. El depurador no continúa hasta que ingresamos algo. Quiero el comportamiento exacto para el método PickPoint (). El flujo de código se detendrá hasta que el usuario haga clic en la cuadrícula.

<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
fuente
La forma obvia es 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?
Rao Hammas Hussain
@RaoHammasHussain Actualicé mi pregunta con un enlace que puede ayudar. El método de utilidad será parte de una API que el usuario de la API llamará cuando quiera solicitar al usuario final que haga clic en la pantalla. Considérelo como una ventana de solicitud de texto en aplicaciones normales de Windows o el método Console.Readline (). En estos casos, el flujo de código se detiene hasta que el usuario ingresa algo. Ahora quiero lo exacto, pero esta vez el usuario hace clic en la pantalla.
Vahid
AutoResetEventno es lo que quieres
Rao Hammas Hussain
@RaoHammasHussain Creo que sí, pero realmente no sé cómo usarlo aquí.
Vahid
Es como si estuvieras implementando intencionalmente WAIT STATE. ¿Es realmente necesario? porque no puedes simplemente poner esto var point = Utility.PickPoint(Grid grid);en el método Grid Click? hacer alguna operación y devolver la respuesta?
Rao Hammas Hussain

Respuestas:

8

"¿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:

  1. Ejecute la operación 1, cuando el usuario hace clic en el botón
  2. Ejecute la operación 2 (continuar / completar la operación 1), cuando el usuario hace clic en Grid

con al menos dos restricciones:

  1. 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.
  2. La operación 1 siempre se ejecuta antes de la operación 2. La operación 1 inicia la secuencia.
  3. 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:

  1. Operación 1 completada (o interacción requerida)
  2. 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 DispatcherObjectun 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 Dispatcherusar Dispatcher.Invokeo Dispatcher.InvokeAsyncpara evitar excepciones entre subprocesos.
Lea las observaciones sobre DispatcherObjectpara 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 awaitpalabra 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.

Código Biónico
fuente
Hola, gracias por abordar la pregunta desde otra perspectiva. Lo siento si la pregunta fue un poco vaga en los detalles. 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.
Vahid
En la aplicación CAD que estoy desarrollando, se supone que los usuarios pueden extenderla mediante complementos / complementos. La API permitirá que el programador tenga acceso a la interfaz de usuario y 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 y luego el código hará otra cosas geniales con el punto dado. Tal vez pedirán que se elija otro punto y tracen una línea, etc.
Vahid
He visto el comportamiento exacto en Autodesk Revit.
Vahid
1
Tengo algo que decir sobre su requerimiento. Por favor lea mi respuesta actualizada. Publiqué la respuesta allí porque de alguna manera se hizo más larga. Admito que realmente me has desencadenado. Por favor, mientras lees ten en cuenta que no quiero ofenderte.
BionicCode
Gracias por tu respuesta actualizada. Por supuesto, no me ofende. Por el contrario, estoy realmente agradecido por el tiempo y el esfuerzo que han puesto en esto.
Vahid
5

Personalmente, creo que esto está siendo demasiado complicado para todos, pero tal vez no entiendo completamente la razón por la que esto debe hacerse de cierta manera, pero parece que aquí se puede usar una simple verificación de bool.

En primer lugar, haga que su cuadrícula sea comprobable mediante éxito configurando las propiedades Backgroundy IsHitTestVisible, de lo contrario, ni siquiera capturará los clics del mouse.

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

A continuación, cree un valor bool que pueda almacenar si debe ocurrir el evento "GridClick". Cuando se hace clic en la cuadrícula, verifique ese valor y realice la ejecución desde el evento de clic de la cuadrícula si está esperando el clic.

Ejemplo:

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
fuente
Hola Tronald, creo que has entendido mal la pregunta. Lo que necesito es que el código se detenga en Utility.PickPoint (Ver) y continúe solo después de que el usuario haya hecho clic en Grid.
Vahid
Oh sí, entendí totalmente mal. Mis disculpas, no me di cuenta de que necesitabas todo para parar. No creo que sea posible sin subprocesos múltiples, ya que toda la interfaz de usuario se bloqueará.
Tronald
Todavía no estoy seguro si no es posible. Definitivamente es posible con async / await, que no es una solución multiproceso. Pero lo que necesito es una alternativa a la solución async / wait.
Vahid
1
Claro, pero mencionaste que no puedes usar async / wait. Parece que necesitaría hacer uso de un despachador y un subproceso que está separado del subproceso principal (que se ejecuta en la interfaz de usuario). Espero que encuentres otra forma, ya que estoy interesado
Tronald
2

Intenté algunas cosas pero no puedo hacerlo sin él async/await. Porque si no lo usamos causa DeadLocko la interfaz de usuario está bloqueada y entonces estamos habilitados para tomar 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
fuente
@gracias, esta es la misma respuesta que obtuve para mi otra pregunta que usa async / await.
Vahid
Oh si ! Lo noté ahora, pero supongo que es la única forma en que descubrí que funciona
Rao Hammas Hussain,
2

Puede bloquear de forma asincrónica usando un 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();
}

No puede, y tampoco quiere, bloquear el hilo del despachador sincrónicamente porque nunca podrá manejar el clic en el Grid, es decir, no puede ser bloqueado y manejar eventos al mismo tiempo.

mm8
fuente
Gracias por una respuesta alternativa. Me pregunto cómo se hace, por ejemplo, en Console.Readline ()? Cuando llega a este método en el depurador, ¿se detiene mágicamente allí a menos que ingresemos algo? ¿Es fundamentalmente diferente en las aplicaciones de consola? ¿No podemos tener el mismo comportamiento en la aplicación WinForms / WPF? ¡He visto esto en la API de Autodesk Revit, hay un método PickPoint () que te obliga a elegir un punto en la pantalla y no he visto ningún asíncrono / espera usado! ¿Es al menos posible ocultar la palabra clave de espera y llamarla de alguna manera desde un método de sincronización?
Vahid
@Vahid: Console.Readline bloquea , es decir, no vuelve hasta que se haya leído una línea. Tu PickPointmétodo no lo hace. Regresa de inmediato. Potencialmente podría bloquearse, pero no podrá manejar la entrada de la interfaz de usuario mientras tanto como escribí en mi respuesta. En otras palabras, tendría que manejar el clic dentro del método para obtener el mismo comportamiento.
mm8
Console.Realine () bloquea pero al mismo tiempo se permiten los eventos de KeyPress. ¿No podemos tener exactamente el mismo comportamiento aquí? ¿Bloqueo por PickPoint () y solo permite MouseEvents? No puedo entender por qué es posible en la consola, pero no en una aplicación basada en la interfaz de usuario.
Vahid
Luego debe configurar un despachador separado PickPointque maneje los eventos del mouse. ¿No veo a dónde vas con esto?
mm8
1
@Vahind: ¿Hacer el código asíncrono y dejar que el usuario espere el método entonces? Esta es la API que esperaría como desarrollador de UI. Llamar a un método de bloqueo en una aplicación de IU no tiene ningún sentido.
mm8
2

Técnicamente es posible con AutoResetEventy sin async/await, pero hay un inconveniente significativo:

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

El inconveniente: si llama a este método directamente en un controlador de eventos de botón como lo hace su código de muestra, se producirá un bloqueo y verá que la aplicación deja de responder. Debido a que está utilizando el único subproceso de interfaz de usuario para esperar el clic del usuario, no puede responder a ninguna acción del usuario, incluido el clic del usuario en la cuadrícula.

Los consumidores del método deben invocarlo en otro hilo para evitar puntos muertos. Si se puede garantizar, está bien. De lo contrario, debe invocar el método de esta manera:

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

Puede causar más problemas a los consumidores de su API, excepto que solían administrar sus propios hilos. Por eso async/awaitfue inventado.

Ken Hung
fuente
Gracias Ken, ¿es posible que el complemento comience desde otro hilo y luego sus eventos no bloqueen el hilo principal de la interfaz de usuario?
Vahid
@Vahid Sí y no. Sí, puede llamar al método de bloqueo en otro hilo y envolverlo en otro método. Sin embargo, el método de envoltura todavía necesitaba llamarse en otro subproceso que no sea el de la interfaz de usuario para evitar el bloqueo de la interfaz de usuario. Porque el contenedor bloqueará el hilo de llamada si es sincrónico . Aunque internamente el contenedor bloquea otro subproceso, aún necesita esperar el resultado y bloquea el subproceso de llamada. Si la persona que llama llama al método contenedor en el subproceso de la interfaz de usuario, la interfaz de usuario se bloquea.
Ken Hung
0

Creo que el problema está en el diseño mismo. Si su API funciona en un elemento en particular, debe usarse en el controlador de eventos de este mismo elemento, no en otro elemento.

Por ejemplo, aquí queremos obtener la posición del evento de clic en la cuadrícula, la API debe usarse en el controlador de eventos asociado con el evento en el elemento de cuadrícula, no en el elemento del botón.

Ahora, si el requisito es manejar el clic en la Cuadrícula solo después de hacer clic en el Botón, entonces la responsabilidad del Botón será agregar el controlador de eventos en la Cuadrícula, y el evento de clic en la Cuadrícula mostrará el cuadro de mensaje y eliminará este controlador de eventos agregado por el botón para que no se active más después de este clic ... (no es necesario bloquear el hilo de la interfaz de usuario)

Solo para decir que si bloquea el hilo de la interfaz de usuario en el clic del botón, no creo que el hilo de la interfaz de usuario pueda activar el evento de clic en la cuadrícula después.

jsami
fuente
0

En primer lugar, el hilo de la interfaz de usuario no se puede bloquear al igual que la respuesta que recibió de su pregunta anterior.
Si puede estar de acuerdo con eso, entonces evitar async / esperar para que su cliente haga menos modificaciones es factible, e incluso no necesita ningún subproceso múltiple.

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

Pero si desea bloquear el hilo de la interfaz de usuario o el "flujo de código", la respuesta será que es imposible. Porque si el subproceso de la IU se bloqueó, no se puede recibir más información.
Como mencionó la aplicación de consola, solo hago una explicación simple.
Cuando ejecuta una aplicación de consola o una llamada AllocConsoledesde un proceso que no se adjuntó a ninguna consola (ventana), se ejecutará conhost.exe que puede proporcionar una consola (ventana) y la aplicación de consola o el proceso de llamada se adjuntará a la consola ( ventana).
Por lo tanto, cualquier código que escriba que pueda bloquear el hilo de la persona que llama, como Console.ReadKeyno bloqueará el hilo de la interfaz de usuario de la ventana de la consola, cualquiera es la razón por la cual la aplicación de la consola espera su entrada pero aún puede responder a otra entrada como hacer clic con el mouse.

Alex.Wei
fuente