Come bloccare il flusso di codice fino a quando un evento non viene generato in C #


10

Qui abbiamo un Gridcon un Button. Quando l'utente fa clic sul pulsante, viene eseguito un metodo in una classe Utility che forza l'applicazione a ricevere un clic su Grid. Il flusso di codice deve arrestarsi qui e non continuare finché l'utente non ha fatto clic su Grid.

Ho avuto una domanda simile prima qui:

Attendere fino a quando l'utente non fa clic su C # WPF

In quella domanda, ho ricevuto una risposta usando async / wait che funziona, ma poiché la userò come parte di un'API, non voglio usare async / wait, poiché i consumatori dovranno quindi contrassegnare i loro metodi con asincrono che non voglio.

Come scrivo il Utility.PickPoint(Grid grid)metodo per raggiungere questo obiettivo?

Ho visto questo che può essere d'aiuto, ma non ho capito fino in fondo che si applica qui per essere onesti:

Blocco fino al completamento di un evento

Consideralo come un metodo Console.ReadKey () in un'applicazione Console. Quando chiamiamo questo metodo, il flusso del codice si interrompe fino a quando non immettiamo un valore. Il debugger non continua finché non immettiamo qualcosa. Voglio il comportamento esatto per il metodo PickPoint (). Il flusso di codice si interromperà fino a quando l'utente non fa clic sulla griglia.

<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)
    {

    }
}

Il modo ovvio è Aync/Awaitche ne dici di fare l'operazione A e salvare l'operazione STATE ora vuoi che l'utente faccia clic su Grid .. quindi se l'utente fa clic su Grid, controlli lo stato se vero, allora fai la tua operazione altrimenti fai quello che vuoi ??
Rao Hammas Hussain,

@RaoHammasHussain Ho aggiornato la mia domanda con un link che potrebbe essere d'aiuto. Il metodo di utilità farà parte di un'API che l'utente dell'API chiamerà ogni volta che desidera richiedere all'utente finale di fare clic sullo schermo. Consideralo come una finestra di prompt per il testo nelle normali applicazioni Windows o nel metodo Console.Readline (). In questi casi, il flusso del codice si interrompe fino a quando l'utente non immette qualcosa. Ora voglio la cosa esatta, ma questa volta l'utente fa clic sullo schermo.
Vahid

AutoResetEventnon è quello che vuoi?
Rao Hammas Hussain,

@RaoHammasHussain Penso di sì, ma davvero non so come usarlo qui.
Vahid

È come se stessimo implementando intenzionalmente WAIT STATE. è davvero richiesto? perché non puoi semplicemente inserirlo var point = Utility.PickPoint(Grid grid);nel metodo Grid Click? fare qualche operazione e restituire la risposta?
Rao Hammas Hussain,

Risposte:


8

"Come bloccare il flusso di codice fino a quando non viene generato un evento?"

Il tuo approccio è sbagliato. Event-driven non significa bloccare e attendere un evento. Non aspetti mai, almeno fai sempre del tuo meglio per evitarlo. L'attesa sta sprecando risorse, bloccando i thread e forse introducendo il rischio di deadlock o thread di zombi (nel caso in cui il segnale di rilascio non venga mai generato).
Dovrebbe essere chiaro che bloccare un thread in attesa di un evento è un anti-pattern in quanto contraddice l'idea di un evento.

Di solito hai due (moderne) opzioni: implementare un'API asincrona o un'API guidata dagli eventi. Poiché non desideri implementare l'API asincrona, ti rimane l'API guidata dagli eventi.

La chiave di un'API guidata dagli eventi è che, invece di forzare il chiamante ad attendere in modo sincrono un risultato o un sondaggio per un risultato, lasci che il chiamante continui e gli invii una notifica, una volta che il risultato è pronto o l'operazione è stata completata. Nel frattempo, il chiamante può continuare a eseguire altre operazioni.

Quando si esamina il problema da una prospettiva di threading, l'API guidata dagli eventi consente al thread chiamante, ad esempio il thread dell'interfaccia utente, che esegue il gestore eventi del pulsante, di essere libero di continuare a gestire, ad esempio, altre operazioni correlate all'interfaccia utente, come il rendering di elementi dell'interfaccia utente o gestire l'input dell'utente come il movimento del mouse e la pressione dei tasti. L'API guidata dagli eventi ha lo stesso effetto o obiettivo di un'API asincrona, sebbene sia molto meno conveniente.

Dal momento che non hai fornito dettagli sufficienti su ciò che stai davvero cercando di fare, cosa Utility.PickPoint()sta effettivamente facendo e qual è il risultato dell'attività o perché l'utente deve fare clic su `Griglia, non posso offrirti una soluzione migliore . Posso solo offrire un modello generale su come implementare le tue esigenze.

Il flusso o l'obiettivo è ovviamente diviso in almeno due passaggi per renderlo una sequenza di operazioni:

  1. Eseguire l'operazione 1, quando l'utente fa clic sul pulsante
  2. Eseguire l'operazione 2 (continua / completa l'operazione 1), quando l'utente fa clic su Grid

con almeno due vincoli:

  1. Facoltativo: la sequenza deve essere completata prima che al client API sia consentito ripeterla. Una sequenza viene completata al termine dell'operazione 2.
  2. L'operazione 1 viene sempre eseguita prima dell'operazione 2. L'operazione 1 avvia la sequenza.
  3. L'operazione 1 deve essere completata prima che al client API sia consentito eseguire l'operazione 2

Ciò richiede due notifiche per il client dell'API per consentire l'interazione non bloccante:

  1. Operazione 1 completata (o interazione richiesta)
  2. Operazione 2 (o obiettivo) completata

Dovresti consentire all'API di implementare questo comportamento e questi vincoli esponendo due metodi pubblici e due eventi pubblici.

Implementare / refactor API di utilità

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 l'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>

Osservazioni

Gli eventi generati su un thread in background eseguiranno i loro gestori sullo stesso thread. L'accesso a DispatcherObjectun elemento UI simile da un gestore, che viene eseguito su un thread in background, richiede che l'operazione critica sia accodata Dispatcherall'utilizzo Dispatcher.Invokeo Dispatcher.InvokeAsyncper evitare eccezioni cross-thread.
Leggi le osservazioni su DispatcherObjectper saperne di più su questo fenomeno chiamato affinità dispatcher o affinità thread.


Alcuni pensieri: rispondi ai tuoi commenti

Dato che mi stavi avvicinando a me per trovare una soluzione di blocco "migliore", dato l'esempio delle applicazioni console, ho sentito di convincerti che la tua percezione o il tuo punto di vista sono totalmente sbagliati.

"Prendi in considerazione un'applicazione console con queste due righe di codice.

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

Cosa succede quando si esegue l'applicazione in modalità debug. Si fermerà alla prima riga di codice e ti costringerà a inserire un valore nell'interfaccia utente della console, quindi dopo aver inserito qualcosa e premuto Invio, eseguirà la riga successiva e stamperà effettivamente ciò che hai inserito. Stavo pensando esattamente allo stesso comportamento ma nell'applicazione WPF. "

Un'applicazione console è qualcosa di completamente diverso. Il concetto di threading è leggermente diverso. Le applicazioni della console non hanno una GUI. Solo flussi di input / output / errore. Non è possibile confrontare l'architettura di un'applicazione console con un'applicazione GUI avanzata. Questo non funzionerà. Devi davvero capire e accettare questo.

Inoltre, non lasciarti ingannare dagli sguardi . Sai cosa sta succedendo dentro Console.ReadLine ? Come viene implementato ? Sta bloccando il thread principale e in parallelo legge l'input? O è solo il polling?
Ecco l'implementazione originale di 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;
}

Come puoi vedere è una semplice operazione sincrona . Esegue il polling per l'input dell'utente in un ciclo "infinito". Nessun blocco magico e continua.

WPF è basato su un thread di rendering e un thread dell'interfaccia utente. Quei fili mantengono sempre la filatura per comunicare con il sistema operativo come gestire input dell'utente - mantenendo l'applicazione reattivo . Non vuoi mai mettere in pausa / bloccare questo thread poiché impedirà al framework di svolgere un lavoro di base essenziale, come rispondere agli eventi del mouse - non vuoi che il mouse si blocchi:

in attesa = blocco thread = mancanza di risposta = UX errato = utenti / clienti infastiditi = problemi in ufficio.

A volte, il flusso dell'applicazione richiede di attendere l'input o il completamento di una routine. Ma non vogliamo bloccare il thread principale.
Ecco perché le persone hanno inventato complessi modelli di programmazione asincrona, per consentire l'attesa senza bloccare il thread principale e senza costringere lo sviluppatore a scrivere codice multithreading complicato ed errato.

Ogni framework applicativo moderno offre operazioni asincrone o un modello di programmazione asincrono, per consentire lo sviluppo di codice semplice ed efficiente.

Il fatto che tu stia provando a resistere al modello di programmazione asincrona, mi mostra una certa mancanza di comprensione. Ogni sviluppatore moderno preferisce un'API asincrona rispetto a un'API sincrona. Nessuno sviluppatore serio si preoccupa di usare la awaitparola chiave o di dichiarare il suo metodo async. Nessuno. Sei il primo che incontro che si lamenta delle API asincrone e che le trova scomode da usare.

Se controllassi il tuo framework, che mira a risolvere i problemi relativi all'interfaccia utente o a semplificare le attività relative all'interfaccia utente, mi aspetterei che sia asincrono - fino in fondo.
L'API correlata all'interfaccia utente che non è asincrona è uno spreco, poiché complicherebbe il mio stile di programmazione, quindi il mio codice che diventa quindi più soggetto a errori e difficile da mantenere.

Una prospettiva diversa: quando riconosci che l'attesa blocca il thread dell'interfaccia utente, sta creando un'esperienza utente molto negativa e indesiderabile poiché l'interfaccia utente si bloccherà fino a quando l'attesa non sarà terminata, ora che te ne rendi conto, perché dovresti offrire un API o un modello di plug-in che incoraggia uno sviluppatore a fare esattamente questo: implementare l'attesa?
Non sai cosa farà il plug-in di terze parti e quanto tempo impiegherà una routine per completarla. Questa è semplicemente una cattiva progettazione API. Quando l'API opera sul thread dell'interfaccia utente, il chiamante dell'API deve essere in grado di effettuare chiamate non bloccanti.

Se neghi l'unica soluzione economica o aggraziata, usa un approccio basato sugli eventi, come mostrato nel mio esempio.
Fa quello che vuoi: avviare una routine - attendere l'input dell'utente - continuare l'esecuzione - raggiungere l'obiettivo.

Ho provato diverse volte a spiegare perché aspettare / bloccare è un cattivo design dell'applicazione. Ancora una volta, non è possibile confrontare un'interfaccia utente della console con una ricca interfaccia grafica, dove ad esempio la sola gestione dell'input è una moltitudine più complessa del semplice ascolto del flusso di input. Davvero non conosco il tuo livello di esperienza e dove hai iniziato, ma dovresti iniziare ad abbracciare il modello di programmazione asincrona. Non conosco il motivo per cui cerchi di evitarlo. Ma non è affatto saggio.

Oggi i modelli di programmazione asincrona sono implementati ovunque, su ogni piattaforma, compilatore, ogni ambiente, browser, server, desktop, database - ovunque. Il modello basato sugli eventi consente di raggiungere lo stesso obiettivo, ma è meno comodo da usare (iscriviti / annulla iscrizione a / da eventi, leggi documenti (quando ci sono documenti) per conoscere gli eventi), basandosi su thread in background. Event-driven è vecchio stile e dovrebbe essere utilizzato solo quando le librerie asincrone non sono disponibili o non applicabili.

Come nota a margine: .NET Framwork (.NET Standard) offre TaskCompletionSource(tra gli altri scopi) di fornire un modo semplice per convertire un'API esistente anche guidata in un'API asincrona.

"Ho visto il comportamento esatto in Autodesk Revit."

Il comportamento (ciò che si sperimenta o si osserva) è molto diverso da come viene implementata questa esperienza. Due cose diverse. È molto probabile che Autodesk utilizzi librerie asincrone o funzionalità di linguaggio o altri meccanismi di threading. Ed è anche correlato al contesto. Quando il metodo che stai pensando è in esecuzione su un thread in background, lo sviluppatore può scegliere di bloccare questo thread. Ha una buona ragione per farlo o ha semplicemente fatto una cattiva scelta nel design. Sei totalmente sulla strada sbagliata;) Il blocco non è buono.
(Il codice sorgente di Autodesk è open source? O come si fa a sapere come viene implementato?)

Non voglio offenderti, per favore, credimi. Ma ti preghiamo di riconsiderare di implementare l'API asincrona. È solo nella tua testa che agli sviluppatori non piace usare async / wait. Ovviamente hai una mentalità sbagliata. E dimentica l'argomento dell'applicazione console: è una sciocchezza;)

L'API correlata all'interfaccia utente DEVE utilizzare asincrona / wait quando possibile. Altrimenti, lasci tutto il lavoro per scrivere codice non bloccante sul client della tua API. Mi costringeresti a racchiudere ogni chiamata alla tua API in un thread in background. O utilizzare una gestione degli eventi meno confortevole. Credetemi: ogni sviluppatore preferisce decorare i suoi membri piuttosto asyncche fare la gestione degli eventi. Ogni volta che utilizzi eventi potresti rischiare una potenziale perdita di memoria - dipende da alcune circostanze, ma il rischio è reale e non raro quando si programma una negligenza.

Spero davvero che tu capisca perché il blocco è male. Spero davvero che tu decida di utilizzare asincrono / attendi per scrivere una moderna API asincrona. Tuttavia, ti ho mostrato un modo molto comune di attendere il non-blocco, usando gli eventi, sebbene ti esorto ad usare asincrono / wait.

"L'API consentirà al programmatore di avere accesso all'interfaccia utente e così via. Ora supponiamo che il programmatore voglia sviluppare un componente aggiuntivo che quando si fa clic su un pulsante, all'utente finale viene chiesto di scegliere un punto nell'interfaccia utente"

Se non si desidera consentire al plug-in di accedere direttamente agli elementi dell'interfaccia utente, è necessario fornire un'interfaccia per delegare eventi o esporre componenti interni tramite oggetti astratti.
L'API internamente si iscriverà agli eventi dell'interfaccia utente per conto del componente aggiuntivo e quindi delegherà l'evento esponendo un evento "wrapper" corrispondente al client API. L'API deve offrire alcuni hook in cui il componente aggiuntivo può connettersi per accedere a componenti specifici dell'applicazione. Un'API del plug-in si comporta come un adattatore o una facciata per fornire agli esterni l'accesso agli interni.
Per consentire un grado di isolamento.

Dai un'occhiata a come Visual Studio gestisce i plug-in o ci consente di implementarli. Fai finta di voler scrivere un plugin per Visual Studio e fai delle ricerche su come farlo. Ti renderai conto che Visual Studio espone i suoi interni tramite un'interfaccia o un'API. Ad esempio puoi manipolare l'editor di codice o ottenere informazioni sul contenuto dell'editor senza un vero accesso ad esso.


Ciao, grazie per aver affrontato la domanda da un'altra prospettiva. Scusate se la domanda era un po 'vaga sui dettagli. Prendi in considerazione un'applicazione console con queste due righe di codice. var str = Console.ReadLine(); Console.WriteLine(str);Cosa succede quando si esegue l'applicazione in modalità debug. Si fermerà alla prima riga di codice e ti costringerà a inserire un valore nell'interfaccia utente della console, quindi dopo aver inserito qualcosa e premuto Invio, eseguirà la riga successiva e stamperà effettivamente ciò che hai inserito. Stavo pensando esattamente allo stesso comportamento ma nell'applicazione WPF.
Vahid

Nell'applicazione CAD che sto sviluppando, si suppone che gli utenti siano in grado di estenderla tramite componenti aggiuntivi / plug-in. L'API consentirà al programmatore di avere accesso all'interfaccia utente e così via. Ora supponiamo che il programmatore voglia sviluppare un componente aggiuntivo che quando si fa clic su un pulsante, all'utente finale viene chiesto di scegliere un punto nell'interfaccia utente e quindi il codice farà altro cose interessanti con il punto dato. Forse chiederanno un altro punto per essere scelti e tracciare una linea, ecc.
Vahid

Ho visto il comportamento esatto in Autodesk Revit.
Vahid,

1
Ho qualcosa da dire sulle tue esigenze. Si prega di leggere la mia risposta aggiornata. Ho pubblicato la risposta lì perché è diventata in qualche modo più lunga. Ammetto che mi hai davvero innescato. Per favore, durante la lettura tieni presente che non voglio offenderti.
BionicCode

Grazie per la tua risposta aggiornata. Certo, non sono offeso. Al contrario, sono davvero grato per il tempo e lo sforzo che hai dedicato a questo.
Vahid

5

Personalmente penso che questo sia complicato da tutti, ma forse non sto comprendendo appieno il motivo per cui questo deve essere fatto in un certo modo, ma sembra che qui possa essere usato un semplice controllo bool.

Prima di tutto, rendi la tua griglia testabile selezionando le proprietà Backgrounde IsHitTestVisible, altrimenti non catturerà nemmeno i clic del mouse.

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

Quindi creare un valore booleare in grado di memorizzare se deve verificarsi l'evento "GridClick". Quando si fa clic sulla griglia, verificare quel valore ed eseguire l'esecuzione dall'evento clic sulla griglia se è in attesa del clic.

Esempio:

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
}

Ciao Tronald, penso che tu abbia frainteso la domanda. Ciò di cui ho bisogno è che il codice si fermi su Utility.PickPoint (Visualizza) e continui solo dopo che l'utente ha fatto clic su Grid.
Vahid,

Oh sì, ho completamente frainteso. Mi scuso, non mi ero reso conto che avevi bisogno di tutto per fermarti davvero. Non penso che sia possibile senza il multi-thread in quanto l'intera UI verrà bloccata.
Tronald,

Non sono ancora sicuro che non sia possibile. È sicuramente possibile con async / waitit che non è una soluzione multi-thread. Ma quello di cui ho bisogno è un'alternativa alla soluzione asincrona / wait.
Vahid,

1
Certo, ma hai detto che non puoi usare asincronizzazione / wait. Sembra che dovresti utilizzare un dispatcher e un thread separato dal thread principale (che viene eseguito sull'interfaccia utente). Spero che troverai un altro modo, anche se mi interessa
Tronald,

2

Ho provato alcune cose ma non riesco a farcela senza async/await. Perché se non lo usiamo provoca DeadLocko l'interfaccia utente è bloccata e quindi siamo in grado di accettare Grid_Clickinput.

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

    }
}

@Grazie, questa è la stessa risposta che ho ricevuto per l'altra mia domanda che usa async / waitit.
Vahid

Oh si ! l'ho notato ora ma immagino sia l'unico modo in cui ho scoperto che funziona
Rao Hammas Hussain

2

È possibile bloccare in modo asincrono utilizzando 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();
}

Non è possibile, e non si desidera nemmeno, bloccare il thread del dispatcher in modo sincrono perché in tal caso non sarà mai in grado di gestire il clic su Grid, ovvero non può essere bloccato e gestire gli eventi contemporaneamente.


Grazie per una risposta alternativa. Mi chiedo come si fa ad esempio in Console.Readline ()? Quando raggiungi questo metodo nel debugger, si ferma magicamente lì a meno che non inseriamo qualcosa? È fondamentalmente diverso nelle applicazioni console? Non possiamo avere lo stesso comportamento nell'applicazione WinForms / WPF? L'ho visto nell'API di Autodesk Revit, c'è un metodo PickPoint () che ti costringe a scegliere un punto sullo schermo e non ho visto alcun asincrono / wait usato! È almeno possibile nascondere la parola chiave wait e chiamarla in qualche modo da un metodo di sincronizzazione?
Vahid

@Vahid: Console.Readline blocchi , cioè non ritorna fino a quando non viene letta una riga. Il tuo PickPointmetodo no. Ritorna immediatamente. Potrebbe potenzialmente bloccare, ma nel frattempo non sarai in grado di gestire l'input dell'interfaccia utente come ho scritto nella mia risposta. In altre parole, dovresti gestire il clic all'interno del metodo per ottenere lo stesso comportamento.
mm8

Console.Realine () si blocca ma allo stesso tempo sono consentiti eventi KeyPress. Non possiamo avere lo stesso identico comportamento qui? Bloccare con PickPoint () e consentire solo MouseEvents? Non riesco a capire perché sia ​​possibile in Console ma non in un'applicazione basata sull'interfaccia utente.
Vahid il

Quindi è necessario impostare un dispatcher separato PickPointche gestisca gli eventi del mouse. Non riesco a vedere dove stai andando con questo?
mm8

1
@Vahind: rendere il codice asincrono e consentire quindi all'utente di attendere il metodo? Questa è l'API che mi aspetterei come sviluppatore dell'interfaccia utente. Chiamare un metodo di blocco in un'applicazione UI non ha alcun senso.
mm8

2

Tecnicamente è possibile con AutoResetEvente senza async/await, ma c'è un significativo svantaggio:

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

Lo svantaggio: se si chiama questo metodo direttamente in un gestore eventi button come fa il codice di esempio, si verificherà un deadlock e vedrai che l'applicazione smette di rispondere. Poiché stai utilizzando l'unico thread dell'interfaccia utente per attendere il clic dell'utente, non può rispondere all'azione di nessun utente, incluso il clic dell'utente sulla griglia.

I consumatori del metodo dovrebbero invocarlo in un altro thread per evitare deadlock. Se può essere garantito, va bene. Altrimenti, è necessario invocare il metodo in questo modo:

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

Potrebbe causare più problemi agli utenti della tua API, tranne per il fatto che gestivano i propri thread. Ecco perché è async/awaitstato inventato.


Grazie Ken, è possibile che il componente aggiuntivo inizi da un altro thread e quindi i suoi eventi non blocchino il thread dell'interfaccia utente principale?
Vahid,

@Vahid Sì e no. Sì, puoi chiamare il metodo di blocco in un altro thread e racchiuderlo in un altro metodo. Tuttavia, il metodo wrapper doveva ancora essere chiamato in un altro thread diverso dal thread dell'interfaccia utente per evitare il blocco dell'interfaccia utente. Perché il wrapper bloccherà il thread chiamante se è sincrono . Sebbene internamente il wrapper blocchi un altro thread, deve comunque attendere il risultato e bloccare il thread chiamante. Se il chiamante chiama il metodo wrapper nel thread dell'interfaccia utente, l'interfaccia utente viene bloccata.
Ken Hung,

0

Penso che il problema sia nel design stesso. Se l'API funziona su un elemento particolare, dovrebbe essere utilizzata nel gestore eventi di questo stesso elemento, non su un altro elemento.

Ad esempio, qui vogliamo ottenere la posizione dell'evento click sulla griglia, l'API deve essere utilizzata nel gestore eventi associato all'evento sull'elemento griglia, non sull'elemento pulsante.

Ora, se il requisito è gestire il clic sulla griglia solo dopo che abbiamo fatto clic sul pulsante, la responsabilità del pulsante sarà quella di aggiungere il gestore eventi sulla griglia e l'evento clic sulla griglia mostrerà la finestra di messaggio e rimuoverà questo gestore di eventi aggiunto dal pulsante in modo che non si attivi più dopo questo clic ... (non è necessario bloccare il thread dell'interfaccia utente)

Solo per dire che se blocchi il thread dell'interfaccia utente sul clic del pulsante, non penso che il thread dell'interfaccia utente sarà in grado di attivare l'evento clic sulla griglia in seguito.


0

Prima di tutto, il thread dell'interfaccia utente non può essere bloccato proprio come la risposta che hai avuto dalla tua domanda iniziale.
Se sei d'accordo, evita l'asincronizzazione / attendi che il tuo cliente faccia meno modifiche, e non ha nemmeno bisogno del multi-threading.

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

Ma se vuoi bloccare il thread dell'interfaccia utente o "flusso di codice", la risposta sarà che è impossibile. Perché se il thread dell'interfaccia utente è stato bloccato, non è possibile ricevere ulteriori input.
Dato che hai menzionato l'app console, faccio solo una semplice spiegazione.
Quando esegui un'app console o chiami AllocConsoleda un processo che non si collega a nessuna console (finestra), verrà eseguito conhost.exe che può fornire una console (finestra) e l'app console o il processo chiamante verranno collegati alla console ( finestra).
Quindi qualsiasi codice che scrivi che potrebbe bloccare il thread del chiamante come Console.ReadKeynon bloccherà il thread dell'interfaccia utente della finestra della console, qualunque sia il motivo per cui quando l'app console attende il tuo input ma può ancora rispondere ad altri input come il clic del mouse.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.