Comment bloquer le flux de code jusqu'à ce qu'un événement soit déclenché en C #

10

Ici, nous avons un Gridavec un Button. Lorsque l'utilisateur clique sur le bouton, une méthode dans une classe Utility est exécutée, ce qui force l'application à recevoir un clic sur Grid. Le flux de code doit s'arrêter ici et ne pas continuer tant que l'utilisateur n'a pas cliqué sur le Grid.

J'ai déjà posé une question similaire ici:

Attendez que l'utilisateur clique sur C # WPF

Dans cette question, j'ai obtenu une réponse en utilisant async / wait qui fonctionne, mais comme je vais l'utiliser dans le cadre d'une API, je ne veux pas utiliser async / wait, car les consommateurs devront alors marquer leurs méthodes avec asynchrone dont je ne veux pas.

Comment écrire la Utility.PickPoint(Grid grid)méthode pour atteindre cet objectif?

J'ai vu cela qui peut aider mais je ne l'ai pas bien compris pour s'appliquer ici pour être honnête:

Blocage jusqu'à la fin d'un événement

Considérez-la comme quelque chose comme la méthode Console.ReadKey () dans une application console. Lorsque nous appelons cette méthode, le flux de code s'arrête jusqu'à ce que nous entrions une valeur. Le débogueur ne continue pas tant que nous n'avons pas entré quelque chose. Je veux le comportement exact de la méthode PickPoint (). Le flux de code s'arrêtera jusqu'à ce que l'utilisateur clique sur la grille.

<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
la source
La manière évidente est de Aync/Awaitsavoir comment faire l'opération A et enregistrer cette opération ÉTAT maintenant que vous voulez que l'utilisateur clique sur la grille.
Rao Hammas Hussain le
@RaoHammasHussain J'ai mis à jour ma question avec un lien qui peut aider. La méthode utilitaire fera partie d'une API que l'utilisateur de l'API appellera chaque fois qu'il voudra demander à l'utilisateur final de cliquer sur l'écran. Considérez-le comme une fenêtre d'invite pour le texte dans les applications Windows normales ou la méthode Console.Readline (). Dans ces cas, le flux de code s'arrête jusqu'à ce que l'utilisateur entre quelque chose. Maintenant, je veux la chose exacte, mais cette fois, l'utilisateur clique sur l'écran.
Vahid
AutoResetEventn'est pas ce que tu veux?
Rao Hammas Hussain le
@RaoHammasHussain Je pense que oui, mais je ne sais vraiment pas comment l'utiliser ici.
Vahid
C'est comme si vous implémentiez intentionnellement WAIT STATE. est-ce vraiment nécessaire? cuz ne pouvez-vous pas simplement mettre cela var point = Utility.PickPoint(Grid grid);dans la méthode Grid Click? faire une opération et retourner la réponse?
Rao Hammas Hussain le

Réponses:

8

"Comment bloquer le flux de code jusqu'à ce qu'un événement soit déclenché?"

Votre approche est fausse. L'événementiel ne signifie pas bloquer et attendre un événement. Vous n'attendez jamais, au moins vous essayez toujours de l'éviter. Attendre, c'est gaspiller des ressources, bloquer les threads et peut-être introduire le risque d'un blocage ou d'un thread zombie (au cas où le signal de libération ne serait jamais émis).
Il doit être clair que bloquer un thread pour attendre un événement est un anti-modèle car il contredit l'idée d'un événement.

Vous avez généralement deux options (modernes): implémentez une API asynchrone ou une API pilotée par les événements. Puisque vous ne voulez pas implémenter votre API asynchrone, vous vous retrouvez avec l'API événementielle.

La clé d'une API événementielle est qu'au lieu de forcer l'appelant à attendre un résultat de manière synchrone ou à interroger un résultat, vous laissez l'appelant continuer et lui envoyez une notification, une fois le résultat prêt ou l'opération terminée. Pendant ce temps, l'appelant peut continuer à exécuter d'autres opérations.

Lorsque vous examinez le problème du point de vue du thread, l'API événementielle permet au thread appelant, par exemple, le thread d'interface utilisateur, qui exécute le gestionnaire d'événements du bouton, d'être libre de continuer à gérer, par exemple, d'autres opérations liées à l'interface utilisateur, comme le rendu d'éléments d'interface utilisateur. ou gérer les entrées de l'utilisateur comme le mouvement de la souris et les pressions sur les touches. L'API événementielle a le même effet ou le même objectif qu'une API asynchrone, bien qu'elle soit beaucoup moins pratique.

Étant donné que vous n'avez pas fourni suffisamment de détails sur ce que vous essayez vraiment de faire, ce qui Utility.PickPoint()se passe réellement et le résultat de la tâche ou pourquoi l'utilisateur doit cliquer sur la `Grille, je ne peux pas vous offrir une meilleure solution . Je peux juste offrir un modèle général de la façon de mettre en œuvre votre exigence.

Votre flux ou votre objectif est évidemment divisé en au moins deux étapes pour en faire une séquence d'opérations:

  1. Exécutez l'opération 1, lorsque l'utilisateur clique sur le bouton
  2. Exécutez l'opération 2 (continuer / terminer l'opération 1), lorsque l'utilisateur clique sur le Grid

avec au moins deux contraintes:

  1. Facultatif: la séquence doit être terminée avant que le client API ne soit autorisé à la répéter. Une séquence est terminée une fois l'opération 2 terminée.
  2. L'opération 1 est toujours exécutée avant l'opération 2. L'opération 1 démarre la séquence.
  3. L'opération 1 doit se terminer avant que le client API ne soit autorisé à exécuter l'opération 2

Cela nécessite au moins deux notifications pour que le client de l'API autorise une interaction non bloquante:

  1. Opération 1 terminée (ou interaction requise)
  2. Opération 2 (ou objectif) terminée

Vous devez laisser votre API implémenter ce comportement et ces contraintes en exposant deux méthodes publiques et deux événements publics.

API utilitaire d'implémentation / refactorisation

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

Utilisez 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>

Remarques

Les événements déclenchés sur un thread d'arrière-plan exécuteront leurs gestionnaires sur le même thread. L'accès DispatcherObjectà un élément d'interface utilisateur similaire à partir d'un gestionnaire, qui est exécuté sur un thread d'arrière-plan, nécessite que l'opération critique soit mise en file d'attente à l' Dispatcheraide de l'un Dispatcher.Invokeou l' autre ou Dispatcher.InvokeAsyncpour éviter les exceptions inter-thread.
Lisez les remarques sur DispatcherObjectpour en savoir plus sur ce phénomène appelé affinité de répartiteur ou affinité de thread.


Quelques réflexions - répondez à vos commentaires

Parce que vous vous approchiez de moi pour trouver une "meilleure" solution de blocage, me donnant l'exemple des applications console, j'ai eu l'impression de vous convaincre, que votre perception ou votre point de vue est totalement faux.

"Envisagez une application console contenant ces deux lignes de code.

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

Que se passe-t-il lorsque vous exécutez l'application en mode débogage. Il s'arrêtera à la première ligne de code et vous forcera à entrer une valeur dans l'interface utilisateur de la console, puis après avoir entré quelque chose et appuyé sur Entrée, il exécutera la ligne suivante et imprimera réellement ce que vous avez entré. Je pensais exactement au même comportement mais dans l'application WPF. "

Une application console est quelque chose de totalement différent. Le concept de filetage est un peu différent. Les applications de console n'ont pas d'interface graphique. Juste flux d'entrée / sortie / erreur. Vous ne pouvez pas comparer l'architecture d'une application console à une application GUI riche. Ça ne marchera pas. Vous devez vraiment comprendre et accepter cela.

Ne vous laissez pas tromper par les regards . Savez-vous ce qui se passe à l' intérieur Console.ReadLine ? Comment est-il mis en œuvre ? Est-ce qu'il bloque le thread principal et en parallèle, il lit l'entrée? Ou s'agit-il simplement d'un sondage?
Voici l'implémentation originale 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;
}

Comme vous pouvez le voir, c'est une opération synchrone simple . Il interroge l'entrée utilisateur dans une boucle "infinie". Pas de bloc magique et continuez.

WPF est construit autour d'un thread de rendu et d'un thread d'interface utilisateur. Ces threads tournent toujours afin de communiquer avec le système d'exploitation comme la gestion des entrées utilisateur - en gardant l'application réactive . Vous ne voulez jamais suspendre / bloquer ce thread car cela empêchera le framework de faire un travail d'arrière-plan essentiel, comme répondre aux événements de la souris - vous ne voulez pas que la souris se fige:

attente = blocage des threads = absence de réponse = mauvaise expérience utilisateur = utilisateurs / clients ennuyés = problèmes au bureau.

Parfois, le flux d'application nécessite d'attendre l'entrée ou l'exécution d'une routine. Mais nous ne voulons pas bloquer le thread principal.
C'est pourquoi les gens ont inventé des modèles de programmation asynchrones complexes, pour permettre d'attendre sans bloquer le thread principal et sans forcer le développeur à écrire du code multithreading compliqué et erroné.

Chaque framework d'application moderne propose des opérations asynchrones ou un modèle de programmation asynchrone, pour permettre le développement de code simple et efficace.

Le fait que vous vous efforcez de résister au modèle de programmation asynchrone, me montre un certain manque de compréhension. Chaque développeur moderne préfère une API asynchrone à une API synchrone. Aucun développeur sérieux ne se soucie d'utiliser le awaitmot - clé ou de déclarer sa méthode async. Personne. Vous êtes le premier que je rencontre qui se plaint des API asynchrones et qui les trouve peu pratiques à utiliser.

Si je vérifiais votre infrastructure, qui vise à résoudre les problèmes liés à l'interface utilisateur ou à faciliter les tâches liées à l'interface utilisateur, je m'attendrais à ce qu'elle soit asynchrone - tout le long.
L'API liée à l'interface utilisateur qui n'est pas asynchrone est un gaspillage, car cela compliquera mon style de programmation, donc mon code qui devient donc plus sujet aux erreurs et difficile à maintenir.

Une perspective différente: lorsque vous reconnaissez que l'attente bloque le thread d'interface utilisateur, crée une expérience utilisateur très mauvaise et indésirable car l'interface utilisateur se figera jusqu'à la fin de l'attente, maintenant que vous vous rendez compte, pourquoi offririez-vous un modèle d'API ou de plugin qui encourage un développeur à faire exactement cela - implémenter l'attente?
Vous ne savez pas ce que fera le plugin tiers et combien de temps une routine prendra jusqu'à ce qu'elle se termine. Il s'agit simplement d'une mauvaise conception de l'API. Lorsque votre API fonctionne sur le thread d'interface utilisateur, l'appelant de votre API doit pouvoir lui faire des appels non bloquants.

Si vous refusez la seule solution bon marché ou gracieuse, utilisez une approche événementielle comme indiqué dans mon exemple.
Il fait ce que vous voulez: démarrer une routine - attendre la saisie de l'utilisateur - poursuivre l'exécution - atteindre l'objectif.

J'ai vraiment essayé plusieurs fois d'expliquer pourquoi l'attente / le blocage est une mauvaise conception d'application. Encore une fois, vous ne pouvez pas comparer une interface utilisateur de console à une interface graphique riche, par exemple, la gestion des entrées à elle seule est une multitude plus complexe que la simple écoute du flux d'entrée. Je ne connais vraiment pas votre niveau d'expérience et où vous avez commencé, mais vous devriez commencer à adopter le modèle de programmation asynchrone. Je ne connais pas la raison pour laquelle vous essayez de l'éviter. Mais ce n'est pas du tout sage.

Aujourd'hui, les modèles de programmation asynchrones sont mis en œuvre partout, sur chaque plate-forme, compilateur, chaque environnement, navigateur, serveur, bureau, base de données - partout. Le modèle événementiel permet d'atteindre le même objectif, mais il est moins pratique à utiliser (s'abonner / se désabonner à / des événements, lire des documents (lorsqu'il y a des documents) pour en savoir plus sur les événements), en s'appuyant sur des threads d'arrière-plan. L'événementiel est désuet et ne doit être utilisé que lorsque les bibliothèques asynchrones ne sont pas disponibles ou ne s'appliquent pas.

En guise de remarque: le .NET Framwork (.NET Standard) offre la possibilité TaskCompletionSource(entre autres) de fournir un moyen simple de convertir une API existante pilotée par paires en une API asynchrone.

"J'ai vu le comportement exact dans Autodesk Revit."

Le comportement (ce que vous vivez ou observez) est très différent de la façon dont cette expérience est mise en œuvre. Deux choses différentes. Votre Autodesk utilise très probablement des bibliothèques asynchrones ou des fonctionnalités de langage ou un autre mécanisme de thread. Et c'est aussi lié au contexte. Lorsque la méthode qui vous vient à l'esprit s'exécute sur un thread d'arrière-plan, le développeur peut choisir de bloquer ce thread. Il a soit une très bonne raison de le faire, soit il a juste fait un mauvais choix de conception. Vous êtes totalement sur la mauvaise voie;) Le blocage n'est pas bon.
(Le code source d'Autodesk est-il open source? Ou comment savez-vous comment il est implémenté?)

Je ne veux pas vous offenser, croyez-moi. Mais veuillez reconsidérer pour implémenter votre API asynchrone. Ce n'est que dans votre tête que les développeurs n'aiment pas utiliser async / wait. Vous avez manifestement eu la mauvaise mentalité. Et oubliez cet argument d'application de console - c'est un non-sens;)

L'API liée à l'interface utilisateur DOIT utiliser async / wait chaque fois que possible. Sinon, vous laissez tout le travail pour écrire du code non bloquant au client de votre API. Vous me forceriez à envelopper chaque appel à votre API dans un fil d'arrière-plan. Ou pour utiliser une gestion d'événements moins confortable. Croyez-moi - chaque développeur décore ses membres plutôt asyncque de gérer des événements. Chaque fois que vous utilisez des événements, vous pouvez risquer une fuite de mémoire potentielle - cela dépend de certaines circonstances, mais le risque est réel et n'est pas rare lors d'une programmation imprudente.

J'espère vraiment que vous comprenez pourquoi le blocage est mauvais. J'espère vraiment que vous déciderez d'utiliser async / wait pour écrire une API asynchrone moderne. Néanmoins, je vous ai montré une façon très courante d'attendre le non-blocage, en utilisant des événements, bien que je vous exhorte à utiliser async / wait.

"L'API permettra au programmeur d'avoir accès à l'interface utilisateur, etc. Supposons maintenant que le programmeur souhaite développer un complément qui, lorsqu'un bouton est cliqué, l'utilisateur final est invité à choisir un point dans l'interface utilisateur"

Si vous ne souhaitez pas autoriser le plugin à accéder directement aux éléments de l'interface utilisateur, vous devez fournir une interface pour déléguer des événements ou exposer des composants internes via des objets abstraits.
L'API s'abonne en interne aux événements d'interface utilisateur au nom du complément, puis délègue l'événement en exposant un événement «wrapper» correspondant au client API. Votre API doit proposer des crochets sur lesquels le complément peut se connecter pour accéder à des composants d'application spécifiques. Une API de plugin agit comme un adaptateur ou une façade pour donner aux externes un accès aux internes.
Pour permettre un certain degré d'isolement.

Jetez un œil à la façon dont Visual Studio gère les plugins ou nous permet de les implémenter. Imaginez que vous souhaitez écrire un plugin pour Visual Studio et faites des recherches sur la façon de procéder. Vous vous rendrez compte que Visual Studio expose ses composants internes via une interface ou une API. EG, vous pouvez manipuler l'éditeur de code ou obtenir des informations sur le contenu de l'éditeur sans y avoir réellement accès.

BionicCode
la source
Bonjour, merci d'avoir abordé la question sous un autre angle. Désolé si la question était un peu vague sur les détails. Considérez une application console contenant ces deux lignes de code. var str = Console.ReadLine(); Console.WriteLine(str);Que se passe-t-il lorsque vous exécutez l'application en mode débogage. Il s'arrêtera à la première ligne de code et vous forcera à entrer une valeur dans l'interface utilisateur de la console, puis après avoir entré quelque chose et appuyé sur Entrée, il exécutera la ligne suivante et imprimera réellement ce que vous avez entré. Je pensais exactement au même comportement mais dans l'application WPF.
Vahid
Dans l'application de CAO que je développe, les utilisateurs sont censés pouvoir l'étendre par des compléments / plugins. L'API permettra au programmeur d'avoir accès à l'interface utilisateur, etc. Supposons maintenant que le programmeur souhaite développer un complément que lorsqu'un bouton est cliqué, l'utilisateur final est invité à choisir un point dans l'interface utilisateur, puis le code fera d'autres trucs cool avec le point donné. Peut-être qu'ils demanderont qu'un autre point soit choisi et qu'ils tracent une ligne, etc.
Vahid
J'ai vu le comportement exact dans Autodesk Revit.
Vahid
1
J'ai quelque chose à dire sur vos besoins. Veuillez lire ma réponse mise à jour. J'ai posté la réponse là-bas car elle est devenue plus longue. J'avoue que tu m'as vraiment déclenché. S'il vous plaît, pendant la lecture, gardez à l'esprit que je ne veux pas vous offenser.
BionicCode
Merci pour votre réponse mise à jour. Bien sûr, je ne suis pas offensé. Au contraire, je suis vraiment reconnaissant du temps et des efforts que vous avez consacrés à cela.
Vahid
5

Personnellement, je pense que tout le monde est trop compliqué, mais peut-être que je ne comprends pas bien la raison pour laquelle cela doit être fait d'une certaine manière, mais il semble qu'un simple contrôle booléen puisse être utilisé ici.

D'abord et avant tout, rendez votre grille testable en définissant les propriétés Backgroundet IsHitTestVisible, sinon elle ne capturera même pas les clics de souris.

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

Créez ensuite une valeur booléenne qui peut stocker si l'événement "GridClick" doit se produire. Lorsque la grille est cliquée, vérifiez cette valeur et effectuez l'exécution à partir de l'événement de clic de la grille s'il attend le clic.

Exemple:

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
la source
Salut Tronald, je pense que vous avez mal compris la question. Ce dont j'ai besoin, c'est que le code s'arrête à Utility.PickPoint (View) et continue uniquement après que l'utilisateur a cliqué sur Grid.
Vahid
Oh oui, j'ai totalement mal compris. Mes excuses, je ne savais pas que vous aviez besoin de tout pour vraiment vous arrêter. Je ne pense pas que ce soit possible sans le multi-threading car toute l'interface utilisateur sera bloquée.
Tronald
Je ne sais toujours pas si ce n'est pas possible. C'est certainement possible avec async / wait qui n'est pas une solution multi-thread. Mais ce dont j'ai besoin, c'est d'une alternative à la solution async / wait.
Vahid
1
Bien sûr, mais vous avez mentionné que vous ne pouvez pas utiliser async / wait. Il semble que vous auriez besoin d'utiliser un répartiteur et un thread séparé du thread principal (qui s'exécute sur l'interface utilisateur). J'espère que vous trouverez un autre moyen car je m'intéresse moi
Tronald
2

J'ai essayé quelques trucs mais je ne peux pas m'en passer async/await. Parce que si nous ne l'utilisons pas, cela provoque DeadLockou l'interface utilisateur est bloquée et nous sommes alors autorisés à prendre des Grid_Clickentrées.

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
la source
@ merci, c'est la même réponse que j'ai obtenue pour mon autre question qui utilise async / wait.
Vahid
Oh oui ! je l'ai remarqué maintenant, mais je suppose que c'est la seule façon dont j'ai trouvé que ça fonctionnait
Rao Hammas Hussain
2

Vous pouvez bloquer de manière asynchrone en utilisant 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();
}

Vous ne pouvez pas, et vous ne voulez pas non plus, bloquer le thread du répartiteur de manière synchrone, car il ne pourra jamais gérer le clic sur le Grid, c'est-à-dire qu'il ne pourra pas être bloqué et gérer les événements en même temps.

mm8
la source
Merci pour une réponse alternative. Je me demande comment cela se fait par exemple dans Console.Readline ()? Lorsque vous atteignez cette méthode dans le débogueur, elle s'arrête comme par magie à moins que nous entrions quelque chose? Est-il fondamentalement différent dans les applications console? Ne pouvons-nous pas avoir le même comportement dans l'application WinForms / WPF? J'ai vu cela dans l'API d'Autodesk Revit, il y a là une méthode PickPoint () qui vous oblige à choisir un point sur l'écran et je n'ai vu aucun asynchrone / attendre utilisé! Est-il au moins possible de masquer le mot-clé wait et de l'appeler d'une manière ou d'une autre à partir d'une méthode de synchronisation?
Vahid
@Vahid: Console.Readline bloque , c'est-à-dire qu'il ne revient pas tant qu'une ligne n'a pas été lue. Votre PickPointméthode ne fonctionne pas. Il revient immédiatement. Cela pourrait potentiellement se bloquer, mais vous ne pourrez pas gérer les entrées d'interface utilisateur pendant ce temps, comme je l'ai écrit dans ma réponse. En d'autres termes, vous devrez gérer le clic à l'intérieur de la méthode pour obtenir le même comportement.
mm8
Les blocs Console.Realine () mais en même temps les événements KeyPress sont autorisés. Ne pouvons-nous pas avoir exactement le même comportement ici? Bloquer par PickPoint () et autoriser uniquement les événements MouseEvents? Je ne comprends pas pourquoi c'est possible dans la console mais pas sur une application basée sur l'interface utilisateur.
Vahid
Ensuite, vous devez configurer un répartiteur distinct PickPointqui gère les événements de la souris. Je ne vois pas où tu vas avec ça?
mm8
1
@Vahind: rendre le code asynchrone et laisser l'utilisateur attendre la méthode? C'est l'API que j'attendrais en tant que développeur d'interface utilisateur. L'appel d'une méthode de blocage dans une application d'interface utilisateur n'a aucun sens.
mm8
2

Techniquement, c'est possible avec AutoResetEventet sans async/await, mais il y a un inconvénient important:

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

L'inconvénient: si vous appelez cette méthode directement dans un gestionnaire d'événements de bouton comme le fait votre exemple de code, un blocage se produira et vous verrez que l'application cesse de répondre. Étant donné que vous utilisez le seul thread d'interface utilisateur pour attendre le clic de l'utilisateur, il ne peut pas répondre à l'action de n'importe quel utilisateur, y compris le clic de l'utilisateur sur la grille.

Les consommateurs de la méthode doivent l'invoquer dans un autre thread pour éviter les blocages. Si cela peut être garanti, c'est bien. Sinon, vous devez appeler la méthode comme ceci:

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

Cela peut causer plus de problèmes aux consommateurs de votre API, sauf qu'ils géraient leurs propres threads. Voilà pourquoi a async/awaitété inventé.

Ken Hung
la source
Merci Ken, est-il possible que le complément démarre à partir d'un autre thread et que ses événements ne bloquent pas le thread principal de l'interface utilisateur?
Vahid
@Vahid Oui et non. Oui, vous pouvez appeler la méthode de blocage dans un autre thread et l'encapsuler dans une autre méthode. Cependant, la méthode d'encapsulage devait encore être appelée dans un autre thread autre que le thread d'interface utilisateur pour éviter le blocage de l'interface utilisateur. Parce que l'encapsuleur bloquera le thread appelant s'il est synchrone . Bien qu'en interne le wrapper bloque un autre thread, il doit encore attendre le résultat et bloque le thread appelant. Si l'appelant appelle la méthode wrapper dans le thread d'interface utilisateur, l'interface utilisateur est bloquée.
Ken Hung
0

Je pense que le problème concerne le design lui-même. Si votre API fonctionne sur un élément particulier, elle doit être utilisée dans le gestionnaire d'événements de cet élément, et non sur un autre élément.

Par exemple, ici, nous voulons obtenir la position de l'événement click sur la grille, l'API doit être utilisée dans le gestionnaire d'événements associé à l'événement sur l'élément Grid, pas sur l'élément button.

Maintenant, si l'exigence est de gérer le clic sur la grille uniquement après avoir cliqué sur le bouton, la responsabilité du bouton sera d'ajouter le gestionnaire d'événements sur la grille, et l'événement de clic sur la grille affichera la boîte de message et supprimera ce gestionnaire d'événements ajouté par le bouton pour qu'il ne se déclenche plus après ce clic ... (pas besoin de bloquer le Thread UI)

Juste pour dire que si vous bloquez le thread d'interface utilisateur sur le clic du bouton, je ne pense pas que le thread d'interface utilisateur sera en mesure de déclencher l'événement de clic sur la grille par la suite.

jsami
la source
0

Tout d'abord, le thread d'interface utilisateur ne peut pas être bloqué, tout comme la réponse que vous avez obtenue de votre première question.
Si vous êtes d'accord avec cela, éviter l'asynchronisation / attendre pour que votre client fasse moins de modifications est faisable, et n'a même pas besoin de 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;
    }
}   

Mais si vous voulez bloquer le thread d'interface utilisateur ou le "flux de code", la réponse sera que c'est impossible. Parce que si le thread d'interface utilisateur a été bloqué, aucune autre entrée ne peut être reçue.
Étant donné que vous avez parlé de l'application console, je fais juste une explication simple.
Lorsque vous exécutez une application console ou appelez à AllocConsolepartir d'un processus qui ne s'est attaché à aucune console (fenêtre), conhost.exe qui peut fournir une console (fenêtre) sera exécuté et une application console ou un processus d'appel sera attaché à la console ( fenêtre).
Donc, tout code que vous écrivez qui pourrait bloquer le thread de l'appelant tel que Console.ReadKeyne bloquera pas le thread de l'interface utilisateur de la fenêtre de la console, tout cela est la raison pour laquelle l'application de la console attend votre entrée mais peut toujours répondre à une autre entrée comme un clic de souris.

Alex.Wei
la source