Question

J'ai une application console dans laquelle je souhaite donner à l'utilisateur X secondes pour répondre à l'invite.Si aucune entrée n'est effectuée après un certain temps, la logique du programme doit continuer.Nous supposons qu'un délai d'attente signifie une réponse vide.

Quelle est la manière la plus simple d’aborder cela ?

Était-ce utile?

La solution

Je suis surpris d'apprendre qu'après 5 ans, toutes les réponses souffrent toujours d'un ou plusieurs des problèmes suivants :

  • Une fonction autre que ReadLine est utilisée, entraînant une perte de fonctionnalité.(Supprimer/retour arrière/touche haut pour la saisie précédente).
  • La fonction se comporte mal lorsqu'elle est invoquée plusieurs fois (génération de plusieurs threads, de nombreux ReadLine suspendus ou comportement inattendu).
  • La fonction repose sur une attente occupée.Ce qui est un gaspillage horrible puisque l'attente devrait durer de quelques secondes jusqu'au délai d'attente, qui peut durer plusieurs minutes.Une attente chargée qui dure aussi longtemps est une horrible perte de ressources, ce qui est particulièrement mauvais dans un scénario multithreading.Si l'attente occupée est modifiée par un sommeil, cela a un effet négatif sur la réactivité, même si j'admets que ce n'est probablement pas un gros problème.

Je pense que ma solution résoudra le problème d'origine sans souffrir d'aucun des problèmes ci-dessus :

class Reader {
  private static Thread inputThread;
  private static AutoResetEvent getInput, gotInput;
  private static string input;

  static Reader() {
    getInput = new AutoResetEvent(false);
    gotInput = new AutoResetEvent(false);
    inputThread = new Thread(reader);
    inputThread.IsBackground = true;
    inputThread.Start();
  }

  private static void reader() {
    while (true) {
      getInput.WaitOne();
      input = Console.ReadLine();
      gotInput.Set();
    }
  }

  // omit the parameter to read a line without a timeout
  public static string ReadLine(int timeOutMillisecs = Timeout.Infinite) {
    getInput.Set();
    bool success = gotInput.WaitOne(timeOutMillisecs);
    if (success)
      return input;
    else
      throw new TimeoutException("User did not provide input within the timelimit.");
  }
}

Bien entendu, appeler est très simple :

try {
  Console.WriteLine("Please enter your name within the next 5 seconds.");
  string name = Reader.ReadLine(5000);
  Console.WriteLine("Hello, {0}!", name);
} catch (TimeoutException) {
  Console.WriteLine("Sorry, you waited too long.");
}

Alternativement, vous pouvez utiliser le TryXX(out) convention, comme Shmueli l'a suggéré :

  public static bool TryReadLine(out string line, int timeOutMillisecs = Timeout.Infinite) {
    getInput.Set();
    bool success = gotInput.WaitOne(timeOutMillisecs);
    if (success)
      line = input;
    else
      line = null;
    return success;
  }

Qui s'appelle ainsi :

Console.WriteLine("Please enter your name within the next 5 seconds.");
string name;
bool success = Reader.TryReadLine(out name, 5000);
if (!success)
  Console.WriteLine("Sorry, you waited too long.");
else
  Console.WriteLine("Hello, {0}!", name);

Dans les deux cas, vous ne pouvez pas mélanger les appels vers Reader avec des normales Console.ReadLine appels:si la Reader le temps est écoulé, il y aura une pendaison ReadLine appel.Au lieu de cela, si vous souhaitez avoir un flux normal (non chronométré) ReadLine appelez, utilisez simplement le Reader et omettez le délai d'attente, de sorte qu'il soit par défaut un délai d'attente infini.

Alors qu’en est-il des problèmes des autres solutions que j’ai mentionnées ?

  • Comme vous pouvez le constater, ReadLine est utilisé, évitant ainsi le premier problème.
  • La fonction se comporte correctement lorsqu'elle est invoquée plusieurs fois.Qu'un délai d'attente se produise ou non, un seul thread d'arrière-plan sera en cours d'exécution et un seul appel à ReadLine au maximum sera actif.L'appel de la fonction entraînera toujours la dernière entrée ou un délai d'attente, et l'utilisateur n'aura pas à appuyer sur Entrée plus d'une fois pour soumettre sa saisie.
  • Et, évidemment, la fonction ne repose pas sur une attente intense.Au lieu de cela, il utilise des techniques multithreading appropriées pour éviter le gaspillage de ressources.

Le seul problème que je prévois avec cette solution est qu’elle n’est pas thread-safe.Cependant, plusieurs threads ne peuvent pas vraiment demander une entrée à l'utilisateur en même temps, la synchronisation doit donc avoir lieu avant d'appeler Reader.ReadLine de toute façon.

Autres conseils

string ReadLine(int timeoutms)
{
    ReadLineDelegate d = Console.ReadLine;
    IAsyncResult result = d.BeginInvoke(null, null);
    result.AsyncWaitHandle.WaitOne(timeoutms);//timeout e.g. 15000 for 15 secs
    if (result.IsCompleted)
    {
        string resultstr = d.EndInvoke(result);
        Console.WriteLine("Read: " + resultstr);
        return resultstr;
    }
    else
    {
        Console.WriteLine("Timed out!");
        throw new TimedoutException("Timed Out!");
    }
}

delegate string ReadLineDelegate();

Cette approche utilisant-t-elle Console.KeyAvailable aide?

class Sample 
{
    public static void Main() 
    {
    ConsoleKeyInfo cki = new ConsoleKeyInfo();

    do {
        Console.WriteLine("\nPress a key to display; press the 'x' key to quit.");

// Your code could perform some useful task in the following loop. However, 
// for the sake of this example we'll merely pause for a quarter second.

        while (Console.KeyAvailable == false)
            Thread.Sleep(250); // Loop until input is entered.
        cki = Console.ReadKey(true);
        Console.WriteLine("You pressed the '{0}' key.", cki.Key);
        } while(cki.Key != ConsoleKey.X);
    }
}

D'une manière ou d'une autre, vous avez besoin d'un deuxième fil.Vous pouvez utiliser des E/S asynchrones pour éviter de déclarer les vôtres :

  • déclarez un ManualResetEvent, appelez-le "evt"
  • appelez System.Console.OpenStandardInput pour obtenir le flux d’entrée.Spécifiez une méthode de rappel qui stockera ses données et définira evt.
  • appelez la méthode BeginRead de ce flux pour démarrer une opération de lecture asynchrone
  • puis entrez une attente chronométrée sur un ManualResetEvent
  • si le délai d'attente expire, annulez la lecture

Si la lecture renvoie des données, définissez l'événement et votre thread principal continuera, sinon vous continuerez après le délai d'attente.

// Wait for 'Enter' to be pressed or 5 seconds to elapse
using (Stream s = Console.OpenStandardInput())
{
    ManualResetEvent stop_waiting = new ManualResetEvent(false);
    s.BeginRead(new Byte[1], 0, 1, ar => stop_waiting.Set(), null);

    // ...do anything else, or simply...

    stop_waiting.WaitOne(5000);
    // If desired, other threads could also set 'stop_waiting' 
    // Disposing the stream cancels the async read operation. It can be
    // re-opened if needed.
}

Cela a fonctionné pour moi.

ConsoleKeyInfo k = new ConsoleKeyInfo();
Console.WriteLine("Press any key in the next 5 seconds.");
for (int cnt = 5; cnt > 0; cnt--)
  {
    if (Console.KeyAvailable == true)
      {
        k = Console.ReadKey();
        break;
      }
    else
     {
       Console.WriteLine(cnt.ToString());
       System.Threading.Thread.Sleep(1000);
     }
 }
Console.WriteLine("The key pressed was " + k.Key);

Je pense que vous devrez créer un fil de discussion secondaire et rechercher une clé sur la console.Je ne connais aucun moyen intégré pour y parvenir.

J'ai lutté contre ce problème pendant 5 mois avant de trouver une solution qui fonctionne parfaitement en entreprise.

Le problème avec la plupart des solutions jusqu'à présent est qu'elles reposent sur autre chose que Console.ReadLine(), et Console.ReadLine() présente de nombreux avantages :

  • Prise en charge de la suppression, du retour arrière, des touches fléchées, etc.
  • La possibilité d'appuyer sur la touche "haut" et de répéter la dernière commande (cela s'avère très pratique si vous implémentez une console de débogage en arrière-plan qui est très utilisée).

Ma solution est la suivante :

  1. Générer un fil séparé pour gérer l'entrée de l'utilisateur à l'aide de Console.ReadLine().
  2. Après le délai d'attente, débloquez Console.ReadLine() en envoyant une touche [entrée] dans la fenêtre actuelle de la console, en utilisant http://inputsimulator.codeplex.com/.

Exemple de code :

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Plus d'informations sur cette technique, y compris la technique correcte pour abandonner un thread qui utilise Console.ReadLine :

Appel .NET pour envoyer la frappe [Entrée] dans le processus en cours, qui est une application console ?

Comment abandonner un autre thread dans .NET, lorsque ledit thread exécute Console.ReadLine ?

Appeler Console.ReadLine() dans le délégué est mauvais car si l'utilisateur n'appuie pas sur « Entrée », cet appel ne reviendra jamais.Le thread exécutant le délégué sera bloqué jusqu'à ce que l'utilisateur appuie sur « Entrée », sans aucun moyen de l'annuler.

L’émission d’une séquence de ces appels ne se comportera pas comme prévu.Considérez ce qui suit (en utilisant l'exemple de classe Console ci-dessus) :

System.Console.WriteLine("Enter your first name [John]:");

string firstName = Console.ReadLine(5, "John");

System.Console.WriteLine("Enter your last name [Doe]:");

string lastName = Console.ReadLine(5, "Doe");

L'utilisateur laisse le délai d'attente expirer pour la première invite, puis saisit une valeur pour la deuxième invite.firstName et lastName contiendront les valeurs par défaut.Lorsque l'utilisateur appuie sur « Entrée », le d'abord L’appel ReadLine se terminera, mais le code a abandonné cet appel et a essentiellement ignoré le résultat.Le deuxième L'appel ReadLine continuera à se bloquer, le délai d'attente finira par expirer et la valeur renvoyée sera à nouveau la valeur par défaut.

BTW- Il y a un bug dans le code ci-dessus.En appelant waitHandle.Close(), vous fermez l'événement sous le thread de travail.Si l'utilisateur appuie sur « Entrée » après l'expiration du délai d'attente, le thread de travail tentera de signaler l'événement qui lève une ObjectDisposedException.L'exception est levée depuis le thread de travail et si vous n'avez pas configuré de gestionnaire d'exceptions non géré, votre processus se terminera.

Je lis peut-être trop de choses sur la question, mais je suppose que l'attente serait similaire au menu de démarrage où il attend 15 secondes à moins que vous n'appuyiez sur une touche.Vous pouvez soit utiliser (1) une fonction de blocage, soit (2) utiliser un fil de discussion, un événement et une minuterie.L'événement agirait comme une « continuation » et se bloquerait jusqu'à ce que le minuteur expire ou qu'une touche soit enfoncée.

Le pseudo-code pour (1) serait :

// Get configurable wait time
TimeSpan waitTime = TimeSpan.FromSeconds(15.0);
int configWaitTimeSec;
if (int.TryParse(ConfigManager.AppSetting["DefaultWaitTime"], out configWaitTimeSec))
    waitTime = TimeSpan.FromSeconds(configWaitTimeSec);

bool keyPressed = false;
DateTime expireTime = DateTime.Now + waitTime;

// Timer and key processor
ConsoleKeyInfo cki;
// EDIT: adding a missing ! below
while (!keyPressed && (DateTime.Now < expireTime))
{
    if (Console.KeyAvailable)
    {
        cki = Console.ReadKey(true);
        // TODO: Process key
        keyPressed = true;
    }
    Thread.Sleep(10);
}

Si vous êtes dans le Main() méthode, vous ne pouvez pas utiliser await, vous devrez donc utiliser Task.WaitAny():

var task = Task.Factory.StartNew(Console.ReadLine);
var result = Task.WaitAny(new Task[] { task }, TimeSpan.FromSeconds(5)) == 0
    ? task.Result : string.Empty;

Cependant, C# 7.1 introduit la possibilité de créer un fichier asynchrone Main() méthode, il est donc préférable d'utiliser la Task.WhenAny() version chaque fois que vous avez cette option :

var task = Task.Factory.StartNew(Console.ReadLine);
var completedTask = await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(5)));
var result = object.ReferenceEquals(task, completedTask) ? task.Result : string.Empty;

Je ne peux malheureusement pas commenter le message de Gulzar, mais voici un exemple plus complet :

            while (Console.KeyAvailable == false)
            {
                Thread.Sleep(250);
                i++;
                if (i > 3)
                    throw new Exception("Timedout waiting for input.");
            }
            input = Console.ReadLine();

MODIFIER:résolu le problème en faisant en sorte que le travail réel soit effectué dans un processus distinct et en supprimant ce processus s'il expire.Voir ci-dessous pour plus de détails.Ouf!

Je viens de l'essayer et cela a semblé bien fonctionner.Mon collègue avait une version qui utilisait un objet Thread, mais je trouve que la méthode BeginInvoke() des types délégués est un peu plus élégante.

namespace TimedReadLine
{
   public static class Console
   {
      private delegate string ReadLineInvoker();

      public static string ReadLine(int timeout)
      {
         return ReadLine(timeout, null);
      }

      public static string ReadLine(int timeout, string @default)
      {
         using (var process = new System.Diagnostics.Process
         {
            StartInfo =
            {
               FileName = "ReadLine.exe",
               RedirectStandardOutput = true,
               UseShellExecute = false
            }
         })
         {
            process.Start();

            var rli = new ReadLineInvoker(process.StandardOutput.ReadLine);
            var iar = rli.BeginInvoke(null, null);

            if (!iar.AsyncWaitHandle.WaitOne(new System.TimeSpan(0, 0, timeout)))
            {
               process.Kill();
               return @default;
            }

            return rli.EndInvoke(iar);
         }
      }
   }
}

Le projet ReadLine.exe est un projet très simple qui possède une classe qui ressemble à ceci :

namespace ReadLine
{
   internal static class Program
   {
      private static void Main()
      {
         System.Console.WriteLine(System.Console.ReadLine());
      }
   }
}

.NET 4 rend cela incroyablement simple en utilisant les tâches.

Tout d’abord, construisez votre assistant :

   Private Function AskUser() As String
      Console.Write("Answer my question: ")
      Return Console.ReadLine()
   End Function

Deuxièmement, exécutez une tâche et attendez :

      Dim askTask As Task(Of String) = New TaskFactory().StartNew(Function() AskUser())
      askTask.Wait(TimeSpan.FromSeconds(30))
      If Not askTask.IsCompleted Then
         Console.WriteLine("User failed to respond.")
      Else
         Console.WriteLine(String.Format("You responded, '{0}'.", askTask.Result))
      End If

Il n'est pas nécessaire d'essayer de recréer la fonctionnalité ReadLine ou d'effectuer d'autres hacks périlleux pour que cela fonctionne.Les tâches nous permettent de résoudre la question de manière très naturelle.

Comme s'il n'y avait pas déjà assez de réponses ici :0), ce qui suit résume dans une méthode statique la solution de @kwl ci-dessus (la première).

    public static string ConsoleReadLineWithTimeout(TimeSpan timeout)
    {
        Task<string> task = Task.Factory.StartNew(Console.ReadLine);

        string result = Task.WaitAny(new Task[] { task }, timeout) == 0
            ? task.Result 
            : string.Empty;
        return result;
    }

Usage

    static void Main()
    {
        Console.WriteLine("howdy");
        string result = ConsoleReadLineWithTimeout(TimeSpan.FromSeconds(8.5));
        Console.WriteLine("bye");
    }

Exemple de thread simple pour résoudre ce problème

Thread readKeyThread = new Thread(ReadKeyMethod);
static ConsoleKeyInfo cki = null;

void Main()
{
    readKeyThread.Start();
    bool keyEntered = false;
    for(int ii = 0; ii < 10; ii++)
    {
        Thread.Sleep(1000);
        if(readKeyThread.ThreadState == ThreadState.Stopped)
            keyEntered = true;
    }
    if(keyEntered)
    { //do your stuff for a key entered
    }
}

void ReadKeyMethod()
{
    cki = Console.ReadKey();
}

ou une chaîne statique en haut pour obtenir une ligne entière.

Dans mon cas, ça marche bien :

public static ManualResetEvent evtToWait = new ManualResetEvent(false);

private static void ReadDataFromConsole( object state )
{
    Console.WriteLine("Enter \"x\" to exit or wait for 5 seconds.");

    while (Console.ReadKey().KeyChar != 'x')
    {
        Console.Out.WriteLine("");
        Console.Out.WriteLine("Enter again!");
    }

    evtToWait.Set();
}

static void Main(string[] args)
{
        Thread status = new Thread(ReadDataFromConsole);
        status.Start();

        evtToWait = new ManualResetEvent(false);

        evtToWait.WaitOne(5000); // wait for evtToWait.Set() or timeOut

        status.Abort(); // exit anyway
        return;
}

N'est-ce pas joli et court ?

if (SpinWait.SpinUntil(() => Console.KeyAvailable, millisecondsTimeout))
{
    ConsoleKeyInfo keyInfo = Console.ReadKey();

    // Handle keyInfo value here...
}

Ceci est un exemple plus complet de la solution de Glen Slayden.Il m'est arrivé de faire cela lors de la création d'un scénario de test pour un autre problème.Il utilise des E/S asynchrones et un événement de réinitialisation manuelle.

public static void Main() {
    bool readInProgress = false;
    System.IAsyncResult result = null;
    var stop_waiting = new System.Threading.ManualResetEvent(false);
    byte[] buffer = new byte[256];
    var s = System.Console.OpenStandardInput();
    while (true) {
        if (!readInProgress) {
            readInProgress = true;
            result = s.BeginRead(buffer, 0, buffer.Length
              , ar => stop_waiting.Set(), null);

        }
        bool signaled = true;
        if (!result.IsCompleted) {
            stop_waiting.Reset();
            signaled = stop_waiting.WaitOne(5000);
        }
        else {
            signaled = true;
        }
        if (signaled) {
            readInProgress = false;
            int numBytes = s.EndRead(result);
            string text = System.Text.Encoding.UTF8.GetString(buffer
              , 0, numBytes);
            System.Console.Out.Write(string.Format(
              "Thank you for typing: {0}", text));
        }
        else {
            System.Console.Out.WriteLine("oy, type something!");
        }
    }

Un autre moyen peu coûteux d’obtenir un deuxième fil de discussion consiste à l’envelopper dans un délégué.

Exemple de mise en œuvre du message d'Eric ci-dessus.Cet exemple particulier a été utilisé pour lire les informations transmises à une application console via un canal :

 using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace PipedInfo
{
    class Program
    {
        static void Main(string[] args)
        {
            StreamReader buffer = ReadPipedInfo();

            Console.WriteLine(buffer.ReadToEnd());
        }

        #region ReadPipedInfo
        public static StreamReader ReadPipedInfo()
        {
            //call with a default value of 5 milliseconds
            return ReadPipedInfo(5);
        }

        public static StreamReader ReadPipedInfo(int waitTimeInMilliseconds)
        {
            //allocate the class we're going to callback to
            ReadPipedInfoCallback callbackClass = new ReadPipedInfoCallback();

            //to indicate read complete or timeout
            AutoResetEvent readCompleteEvent = new AutoResetEvent(false);

            //open the StdIn so that we can read against it asynchronously
            Stream stdIn = Console.OpenStandardInput();

            //allocate a one-byte buffer, we're going to read off the stream one byte at a time
            byte[] singleByteBuffer = new byte[1];

            //allocate a list of an arbitary size to store the read bytes
            List<byte> byteStorage = new List<byte>(4096);

            IAsyncResult asyncRead = null;
            int readLength = 0; //the bytes we have successfully read

            do
            {
                //perform the read and wait until it finishes, unless it's already finished
                asyncRead = stdIn.BeginRead(singleByteBuffer, 0, singleByteBuffer.Length, new AsyncCallback(callbackClass.ReadCallback), readCompleteEvent);
                if (!asyncRead.CompletedSynchronously)
                    readCompleteEvent.WaitOne(waitTimeInMilliseconds);

                //end the async call, one way or another

                //if our read succeeded we store the byte we read
                if (asyncRead.IsCompleted)
                {
                    readLength = stdIn.EndRead(asyncRead);
                    if (readLength > 0)
                        byteStorage.Add(singleByteBuffer[0]);
                }

            } while (asyncRead.IsCompleted && readLength > 0);
            //we keep reading until we fail or read nothing

            //return results, if we read zero bytes the buffer will return empty
            return new StreamReader(new MemoryStream(byteStorage.ToArray(), 0, byteStorage.Count));
        }

        private class ReadPipedInfoCallback
        {
            public void ReadCallback(IAsyncResult asyncResult)
            {
                //pull the user-defined variable and strobe the event, the read finished successfully
                AutoResetEvent readCompleteEvent = asyncResult.AsyncState as AutoResetEvent;
                readCompleteEvent.Set();
            }
        }
        #endregion ReadPipedInfo
    }
}
string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    {
        readline = Console.ReadLine();
    }
);
do
{
    Thread.Sleep(100);
} while (readline == "?");

Notez que si vous empruntez la voie « Console.ReadKey », vous perdez certaines des fonctionnalités intéressantes de ReadLine, à savoir :

  • Prise en charge de la suppression, du retour arrière, des touches fléchées, etc.
  • La possibilité d'appuyer sur la touche "haut" et de répéter la dernière commande (cela s'avère très pratique si vous implémentez une console de débogage en arrière-plan qui est très utilisée).

Pour ajouter un délai d'attente, modifiez la boucle while en conséquence.

S'il vous plaît, ne me détestez pas d'avoir ajouté une autre solution à la pléthore de réponses existantes !Cela fonctionne pour Console.ReadKey(), mais pourrait facilement être modifié pour fonctionner avec ReadLine(), etc.

Comme les méthodes "Console.Read" sont bloquantes, il faut "coup de coude" le flux StdIn pour annuler la lecture.

Syntaxe d'appel :

ConsoleKeyInfo keyInfo;
bool keyPressed = AsyncConsole.ReadKey(500, out keyInfo);
// where 500 is the timeout

Code:

public class AsyncConsole // not thread safe
{
    private static readonly Lazy<AsyncConsole> Instance =
        new Lazy<AsyncConsole>();

    private bool _keyPressed;
    private ConsoleKeyInfo _keyInfo;

    private bool DoReadKey(
        int millisecondsTimeout,
        out ConsoleKeyInfo keyInfo)
    {
        _keyPressed = false;
        _keyInfo = new ConsoleKeyInfo();

        Thread readKeyThread = new Thread(ReadKeyThread);
        readKeyThread.IsBackground = false;
        readKeyThread.Start();

        Thread.Sleep(millisecondsTimeout);

        if (readKeyThread.IsAlive)
        {
            try
            {
                IntPtr stdin = GetStdHandle(StdHandle.StdIn);
                CloseHandle(stdin);
                readKeyThread.Join();
            }
            catch { }
        }

        readKeyThread = null;

        keyInfo = _keyInfo;
        return _keyPressed;
    }

    private void ReadKeyThread()
    {
        try
        {
            _keyInfo = Console.ReadKey();
            _keyPressed = true;
        }
        catch (InvalidOperationException) { }
    }

    public static bool ReadKey(
        int millisecondsTimeout,
        out ConsoleKeyInfo keyInfo)
    {
        return Instance.Value.DoReadKey(millisecondsTimeout, out keyInfo);
    }

    private enum StdHandle { StdIn = -10, StdOut = -11, StdErr = -12 };

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetStdHandle(StdHandle std);

    [DllImport("kernel32.dll")]
    private static extern bool CloseHandle(IntPtr hdl);
}

Voici une solution qui utilise Console.KeyAvailable.Ce sont des appels bloquants, mais il devrait être assez simple de les appeler de manière asynchrone via le TPL si vous le souhaitez.J'ai utilisé les mécanismes d'annulation standard pour faciliter la connexion avec le modèle asynchrone de tâches et toutes ces bonnes choses.

public static class ConsoleEx
{
  public static string ReadLine(TimeSpan timeout)
  {
    var cts = new CancellationTokenSource();
    return ReadLine(timeout, cts.Token);
  }

  public static string ReadLine(TimeSpan timeout, CancellationToken cancellation)
  {
    string line = "";
    DateTime latest = DateTime.UtcNow.Add(timeout);
    do
    {
        cancellation.ThrowIfCancellationRequested();
        if (Console.KeyAvailable)
        {
            ConsoleKeyInfo cki = Console.ReadKey();
            if (cki.Key == ConsoleKey.Enter)
            {
                return line;
            }
            else
            {
                line += cki.KeyChar;
            }
        }
        Thread.Sleep(1);
    }
    while (DateTime.UtcNow < latest);
    return null;
  }
}

Cela présente certains inconvénients.

  • Vous ne bénéficiez pas des fonctionnalités de navigation standard qui ReadLine fournit (défilement fléché haut/bas, etc.).
  • Cela injecte des caractères '\0' dans l'entrée si une touche spéciale est enfoncée (F1, PrtScn, etc.).Vous pouvez cependant facilement les filtrer en modifiant le code.

Nous nous sommes retrouvés ici car une question en double a été posée.J'ai trouvé la solution suivante qui semble simple.Je suis sûr qu'il présente certains inconvénients qui m'ont échappé.

static void Main(string[] args)
{
    Console.WriteLine("Hit q to continue or wait 10 seconds.");

    Task task = Task.Factory.StartNew(() => loop());

    Console.WriteLine("Started waiting");
    task.Wait(10000);
    Console.WriteLine("Stopped waiting");
}

static void loop()
{
    while (true)
    {
        if ('q' == Console.ReadKey().KeyChar) break;
    }
}

Je suis arrivé à cette réponse et j'ai fini par faire :

    /// <summary>
    /// Reads Line from console with timeout. 
    /// </summary>
    /// <exception cref="System.TimeoutException">If user does not enter line in the specified time.</exception>
    /// <param name="timeout">Time to wait in milliseconds. Negative value will wait forever.</param>        
    /// <returns></returns>        
    public static string ReadLine(int timeout = -1)
    {
        ConsoleKeyInfo cki = new ConsoleKeyInfo();
        StringBuilder sb = new StringBuilder();

        // if user does not want to spesify a timeout
        if (timeout < 0)
            return Console.ReadLine();

        int counter = 0;

        while (true)
        {
            while (Console.KeyAvailable == false)
            {
                counter++;
                Thread.Sleep(1);
                if (counter > timeout)
                    throw new System.TimeoutException("Line was not entered in timeout specified");
            }

            cki = Console.ReadKey(false);

            if (cki.Key == ConsoleKey.Enter)
            {
                Console.WriteLine();
                return sb.ToString();
            }
            else
                sb.Append(cki.KeyChar);                
        }            
    }

Un exemple simple utilisant Console.KeyAvailable:

Console.WriteLine("Press any key during the next 2 seconds...");
Thread.Sleep(2000);
if (Console.KeyAvailable)
{
    Console.WriteLine("Key pressed");
}
else
{
    Console.WriteLine("You were too slow");
}

Un code beaucoup plus contemporain et basé sur des tâches ressemblerait à ceci :

public string ReadLine(int timeOutMillisecs)
{
    var inputBuilder = new StringBuilder();

    var task = Task.Factory.StartNew(() =>
    {
        while (true)
        {
            var consoleKey = Console.ReadKey(true);
            if (consoleKey.Key == ConsoleKey.Enter)
            {
                return inputBuilder.ToString();
            }

            inputBuilder.Append(consoleKey.KeyChar);
        }
    });


    var success = task.Wait(timeOutMillisecs);
    if (!success)
    {
        throw new TimeoutException("User did not provide input within the timelimit.");
    }

    return inputBuilder.ToString();
}

J'ai eu une situation unique d'avoir une application Windows (service Windows).Lors de l'exécution du programme de manière interactive Environment.IsInteractive (VS Debugger ou depuis cmd.exe), j'ai utilisé AttachConsole/AllocConsole pour obtenir mon stdin/stdout.Pour empêcher le processus de se terminer pendant que le travail était effectué, l'UI Thread appelle Console.ReadKey(false).Je voulais annuler l'attente que le thread d'interface utilisateur faisait à partir d'un autre thread, j'ai donc proposé une modification de la solution par @JSquaredD.

using System;
using System.Diagnostics;

internal class PressAnyKey
{
  private static Thread inputThread;
  private static AutoResetEvent getInput;
  private static AutoResetEvent gotInput;
  private static CancellationTokenSource cancellationtoken;

  static PressAnyKey()
  {
    // Static Constructor called when WaitOne is called (technically Cancel too, but who cares)
    getInput = new AutoResetEvent(false);
    gotInput = new AutoResetEvent(false);
    inputThread = new Thread(ReaderThread);
    inputThread.IsBackground = true;
    inputThread.Name = "PressAnyKey";
    inputThread.Start();
  }

  private static void ReaderThread()
  {
    while (true)
    {
      // ReaderThread waits until PressAnyKey is called
      getInput.WaitOne();
      // Get here 
      // Inner loop used when a caller uses PressAnyKey
      while (!Console.KeyAvailable && !cancellationtoken.IsCancellationRequested)
      {
        Thread.Sleep(50);
      }
      // Release the thread that called PressAnyKey
      gotInput.Set();
    }
  }

  /// <summary>
  /// Signals the thread that called WaitOne should be allowed to continue
  /// </summary>
  public static void Cancel()
  {
    // Trigger the alternate ending condition to the inner loop in ReaderThread
    if(cancellationtoken== null) throw new InvalidOperationException("Must call WaitOne before Cancelling");
    cancellationtoken.Cancel();
  }

  /// <summary>
  /// Wait until a key is pressed or <see cref="Cancel"/> is called by another thread
  /// </summary>
  public static void WaitOne()
  {
    if(cancellationtoken==null || cancellationtoken.IsCancellationRequested) throw new InvalidOperationException("Must cancel a pending wait");
    cancellationtoken = new CancellationTokenSource();
    // Release the reader thread
    getInput.Set();
    // Calling thread will wait here indefiniately 
    // until a key is pressed, or Cancel is called
    gotInput.WaitOne();
  }    
}

Cela semble être la solution la plus simple et la plus efficace, qui n'utilise aucune API native :

    static Task<string> ReadLineAsync(CancellationToken cancellation)
    {
        return Task.Run(() =>
        {
            while (!Console.KeyAvailable)
            {
                if (cancellation.IsCancellationRequested)
                    return null;

                Thread.Sleep(100);
            }
            return Console.ReadLine();
        });
    }

Exemple d'utilisation :

    static void Main(string[] args)
    {
        AsyncContext.Run(async () =>
        {
            CancellationTokenSource cancelSource = new CancellationTokenSource();
            cancelSource.CancelAfter(1000);
            Console.WriteLine(await ReadLineAsync(cancelSource.Token) ?? "null");
        });
    }
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top