0

the following class below is used to write text to the console but in a more timed way, like the way characters are showed in the movies like this, problem is I just can't get the methods in the class to run as tasks, especially since I'm using the timer and I have multiple inter-linked methods.

    Private NotInheritable Class WriteText
    Private Shared i As Integer = 0
    Private Shared text As String = ""
    Private Shared audio As New Audio
    Private Shared audioFile As String = "C:\Users\jamisco\Documents\Sound for Jamisco\beep-29.wav"
    Shared call_command As Boolean
    Shared timer As New Timer

    Public Shared Sub Text2Write(ByVal Text2Type As String, Optional ByVal call_awaitcommand As Boolean = False)
        text = Text2Type
        call_command = call_awaitcommand
        Timer.Interval = typingInterval
        Timer.AutoReset = True
        Timer.Start()
        writing2console = True
        FinishedTyping = False
        i = 0
        AddHandler Timer.Elapsed, AddressOf Tick
        audio.Play(audioFile, AudioPlayMode.BackgroundLoop)
        While (writing2console)
            Console.ReadKey(True)
        End While

    End Sub

    Private Shared Sub Tick(ByVal sender As Object, ByVal e As ElapsedEventArgs)

        If (text.Length >= 0 And timer.Enabled) Then
            If i < text.Length Then
                Console.Write(text(i))
                i += 1
            Else
                Reset()
            End If
        End If

    End Sub

    Public Shared Sub StopTyping()
        Reset()
    End Sub

    Private Shared Sub Reset()
        Console.WriteLine()
        audio.Stop()
        'timer.Stop()
        timer.Stop()
        i = 0
        writing2console = False
        FinishedTyping = True
        If (call_command) Then
            _jamisco.AwaitCommand()
        End If
    End Sub
End Class
Ňɏssa Pøngjǣrdenlarp
  • 38,411
  • 12
  • 59
  • 178
Jamisco
  • 1,658
  • 3
  • 13
  • 17
  • What part of this do you want to use tasks for? – Bradley Uffner Aug 28 '17 at 15:08
  • what do you mean? ... I want the whole class to function as one tasks since all the methods are inter-linked – Jamisco Aug 28 '17 at 16:02
  • I don't really see how this process could be converted to a task. I'm mean sure, I could make it use a task, but there won't really be any benefit to it. If you just want it to work asynchronously, there are better ways to do it. – Bradley Uffner Aug 28 '17 at 16:13
  • Actually, I think I figured out what you are trying to do now. Give me a bit of time, and I'll try to get an example for you. – Bradley Uffner Aug 28 '17 at 16:15

1 Answers1

1

Is this the effect you are trying to get?

enter image description here

using System;
using System.Threading.Tasks;

namespace asyncConsoleTyping
{
    class Program
    {
        private static Random _rnd = new Random();

        public static void Main(string[] args)
        {
            AsyncMain().Wait();
        }

        public static async Task AsyncMain()
        {
            await Type("This is a test");
            await Type("This is another test");
            await Type("What is your name?");
            var name = Console.ReadLine();
            await Type($"Hello {name}!");

            Console.Read();  
        }

        public static async Task Type(string text)
        {
            var prevColor = Console.ForegroundColor;
            Console.ForegroundColor=ConsoleColor.Green;
            foreach (char c in text)
            {
                Console.Write(c);
                await Task.Delay(10 + _rnd.Next(30));
            }
            Console.WriteLine();
            Console.ForegroundColor = prevColor;
        }
    }
}

This works by asynchronously printing strings a character at a time, with a small random delay between each one. It also changes the text color while printing, and restoring it afterwards. You can call it with await Type(SomeString), and the console won't lock up while printing characters. Execution will resume on the next line after the await once the entire string has been printed. It doesn't actually use a Timer, as there are more appropriate Task based mechanisms for doing this (Task.Delay).

You will have to use a small "trick" to get a console application to use async await properly, since Main can't be a Task. Just create an AsyncMain function, as in my example, and call it from Main with AsyncMain().Wait(). Once C# 7.1 is released, you won't need to do this, as public static Task Main(string[] args); (among others) will be considered a valid entry point.

There are a few other "gotchas" for async console applications that you can read about here.


You can cancel a call to an async Task by using CancellationTokenSource, and the CancellationToken it creates.

Any Task that can be canceled should accept a CancellationToken as an argument. That Token is created by calling the Token function on a CancellationTokenSource. When the source is created, you can specify a timeout value (var cts = new CancellationTokenSource(1000);), or you can call Cancel on it (cts.Cancel) from anywhere that has a reference to the token. Your async method can then pass that token on to anything that it awaits (that supports cancellation). It should also periodically check IsCancellationRequested to see if it should cancel its work.

This is a version of the above example that supports cancellation:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace asyncConsoleTyping
{
    class Program
    {
        private static Random _rnd = new Random();

        public static void Main(string[] args)
        {
            AsyncMain().Wait();
        }

        public static async Task AsyncMain()
        {
            await Type("This is a test");
            await Type("This is another test");
            await Type("What is your name?");
            var name = Console.ReadLine();
            await Type($"Hello {name}!");

            var cts = new CancellationTokenSource(1000); //Auto-cancels after 1 second
            try
            {
                await Type("This String can get canceled via a CancellationToken", cts.Token);
            }
            catch (Exception ex)
            {
                Console.ForegroundColor=ConsoleColor.Red;
                Console.WriteLine($"Canceled: {ex.Message}");
            }
        }

        public static Task Type(string text)
        {
            return Type(text, CancellationToken.None); //This overload doesn't support cancellation, but it calls the one that does.  Passing in CancellationToken.None allows it to work without checking to see if a "real" token was passed.
        }

        public static async Task Type(string text, CancellationToken ct)
        {
            var prevColor = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Green;
            foreach (char c in text)
            {
                Console.Write(c);
                await Task.Delay(10 + _rnd.Next(30), ct); //Pass the Cancellationtoken in to Task.Delay so the delay can be canceled
                if (ct.IsCancellationRequested) //Check to see if the task was canceled, if so, exit the loop through the characters.
                {
                    break;
                }
            }
            Console.WriteLine();
            Console.ForegroundColor = prevColor;
        }
    }
}

The example above that shows cancellation actually has a subtle bug in it that could cause the console color to not get reset. await Task.Delay(10 + _rnd.Next(30), ct); will throw an exception on cancel, and not go in to the color reset code. That can be fixed by changing it to this:

public static async Task Type(string text, CancellationToken ct)
{
    var prevColor = Console.ForegroundColor;
    Console.ForegroundColor = ConsoleColor.Green;
    try
    {
        foreach (char c in text)
        {
            Console.Write(c);
            await Task.Delay(10 + _rnd.Next(30), ct);
        }
    }
    finally
    {
        Console.WriteLine();
        Console.ForegroundColor = prevColor;
    }
    ct.ThrowIfCancellationRequested(); //Omit this line if you don't want an exception thrown on cancellation (but be aware of the consequences!)
}
Bradley Uffner
  • 16,641
  • 3
  • 39
  • 76
  • just one more question, but how would I stop/pause the task? – Jamisco Aug 28 '17 at 19:05
  • I've updated my answer to show how you can support cancellation. Cancellation in a console application can be tricky, because it isn't event based like WinForms, there is no easy way for the user to invoke the cancellation since there are no UI buttons to press. My example will cancel printing one of the lines after 1000ms automatically, but you could do without that and cancel from another thread if required, as long as the other thread has a reference to the `CancellationTokenSource`. – Bradley Uffner Aug 28 '17 at 19:30
  • I'm sorry, I just realized your question is in VB.NET, and I answered you in C#. Do you need me to convert the code? – Bradley Uffner Aug 28 '17 at 19:34
  • its fine, I know both languages, thanks I really appreciate the help – Jamisco Aug 28 '17 at 19:40
  • Just some more notes. By default, most functions that can be canceled will throw a `TaskCanceledException` when they detect the cancellation. You can read more about this, how to stop it from doing that, and what to watch out for if you don't throw the exaction [here](https://stackoverflow.com/questions/7343211/cancelling-a-task-is-throwing-an-exception). – Bradley Uffner Aug 28 '17 at 19:51
  • 1
    Just a useful tip, instead of doing `AsyncMain().Wait();` do `AsyncMain().GetAwaiter().GetResult();`, if a exception is thrown you get the real exception raised from that line instead of a `AggragateException` with the real exception as the inner exception. – Scott Chamberlain Aug 28 '17 at 21:33