"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:
- Eseguire l'operazione 1, quando l'utente fa clic sul pulsante
- Eseguire l'operazione 2 (continua / completa l'operazione 1), quando l'utente fa clic su
Grid
con almeno due vincoli:
- Facoltativo: la sequenza deve essere completata prima che al client API sia consentito ripeterla. Una sequenza viene completata al termine dell'operazione 2.
- L'operazione 1 viene sempre eseguita prima dell'operazione 2. L'operazione 1 avvia la sequenza.
- 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:
- Operazione 1 completata (o interazione richiesta)
- 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 DispatcherObject
un elemento UI simile da un gestore, che viene eseguito su un thread in background, richiede che l'operazione critica sia accodata Dispatcher
all'utilizzo Dispatcher.Invoke
o Dispatcher.InvokeAsync
per evitare eccezioni cross-thread.
Leggi le osservazioni su DispatcherObject
per 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 await
parola 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 async
che 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.
Aync/Await
che 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 ??