1

Here I have simple code written in C#.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Reactive.Subjects;

namespace ReactiveProgramming
{
    class Program
    {

        static void Main(string[] args)
        {
            var generateSeq = new GenerateSequence();
            Console.WriteLine("Hello World!");

            generateSeq.Sequence.Subscribe(val =>
            {
                Console.WriteLine(val);

                // it works if I remove below two lines ...
                Console.SetCursorPosition(0, Console.CursorTop - 1);   
                Console.Write("\r" + new string(' ', Console.WindowWidth) + "\r");
            });

            generateSeq.Run();
        }
    }

    class GenerateSequence
    {
        public Subject<int> Sequence = new Subject<int>();

        public void Run(int runTimes = 10)
        {
            ConsoleKeyInfo cki;

            Task.Run(() => runForTimes(10));

            do
            {
                cki = Console.ReadKey();
            } while (cki.Key != ConsoleKey.Escape);
        }

        public void runForTimes(int runTimes = 10)
        {
            for (var i = 0; i < 10; i++)
            {
                Sequence.OnNext(i);
                Thread.Sleep(1000);
            }
        }
    }
}

But instead of printing sequence on top of each other, it just freeze the output after first emit.

And tested in Linux too ... same output.

Screenshot

If I remote these lines Console.SetCursorPosition and Console.Write("\r" + new string(' ', Console.WindowWidth) + "\r") from subscribe ... it works and print all numbers on screen one after another but I want to print on top of each other ...

But if I change my Main function like this:

    static void Main(string[] args)
    {
        var generateSeq = new GenerateSequence();
        Console.WriteLine("Hello World!");

        generateSeq.Sequence.Subscribe(val =>
        {
            Console.WriteLine(val);
            // Console.SetCursorPosition(0, Console.CursorTop - 1);
            // Console.Write("\r" + new string(' ', Console.WindowWidth) + "\r");
        });

        generateSeq.Run();
    }

Where I have commented those two lines ... output is as follows ...

Screenshot

But instead of output in sequence like second image, I want to print the output at the same position. Just over write the new output over the old one

Note: I am running it on Macbook Pro (Big Sur), it happens with .net core 3.1 or .net 5.0 and using iTerm as console emulator

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
microchip78
  • 2,040
  • 2
  • 28
  • 39
  • 1
    You may want to re-read [MCVE]... Did you figured if it is issue with threading or just SetCursorPosition? No need for all that fancy code in either case... – Alexei Levenkov Sep 15 '21 at 04:57
  • 2
    This is unrelated to your question, but in general Task.Delay() is a better option than Thread.Sleep(). await Task.Delay causes a logical pause in your code but keeps the thread free for other activities, thread.sleep causes the thread to be unusable by anyone. – The Lemon Sep 15 '21 at 05:14
  • @AlexeiLevenkov it is minimal production for this particular scenario ... – microchip78 Sep 15 '21 at 05:18
  • @microchip78 totally fair. At least whoever will try to answer would know that SetCursorPosition works perfectly fine for you when not called from another thread. Thanks for clarification. – Alexei Levenkov Sep 15 '21 at 05:20
  • @AliReza I have updated the question, I believe it is not the `Console.ReadKey()`. I want my app to wait for key press and thats the reason I have started new thread for `RunForTimes` function – microchip78 Sep 15 '21 at 05:24
  • @AlexeiLevenkov thats exactly right ... because in normal scenario, `Console.SetCursorPostion` works like a charm, but it in this particular scenario, its not working ... i tried using `lock()` around `Console.*` calls in subscribe, but no luck ... – microchip78 Sep 15 '21 at 05:30
  • I finally understand what you trying to do, also updated the answer. @microchip78 – AliReza Sabouri Sep 15 '21 at 06:31
  • 1
    @AliReza thats excellent ... That works perfectly fine ... Thank you so much for your time and looking into it ... – microchip78 Sep 15 '21 at 06:36
  • 1
    Why aren't you using `Observable.Interval(TimeSpan.FromSeconds(1.0)).Take(10)` to generate your sequence? – Enigmativity Sep 15 '21 at 06:51
  • @Enigmativity Thanks for your input, but that example was for **minimal reproducible example** for the bigger problem I have ... But its true that `Observable.Interval(TimeSpan.FromSeconds(1.0)).Take(10)` is a better way to produce a sequence ... – microchip78 Sep 15 '21 at 07:02
  • @microchip78 - Given the complexity of this example, perhaps it would be useful to post about your bigger example? Any time you use subject like this it's generally saying you're doing something wrong. – Enigmativity Sep 16 '21 at 22:22

2 Answers2

3

If I were writing this, I'd go with this implementation:

    static async Task Main(string[] args)
    {
        Console.WriteLine("Hello World!");

        IObservable<System.ConsoleKeyInfo> keys =
            Observable
                .Start(() => Console.ReadKey());

        await
            Observable
                .Interval(TimeSpan.FromSeconds(1.0))
                .Take(10)
                .TakeUntil(keys)
                .Do(x =>
                {
                    Console.WriteLine(x);
                    Console.SetCursorPosition(0, Console.CursorTop - 1);
                },
                () => Console.SetCursorPosition(0, Console.CursorTop + 1));

        Console.WriteLine("Bye World!");
    }

Wherever possible you should avoid using subjects.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Genuinely curious about why one should avoid using subjects where possible. Maybe you could help with my question [here](https://stackoverflow.com/questions/69239022/is-there-a-subject-implementation-in-rx-net-that-functionally-resembles-behavior). Thanks! – noseratio Sep 18 '21 at 23:20
  • 1
    Observables should behave consistently. When you use subjects you risk creating an observable that may die because somewhere your subject got an `OnCompleted` or `OnError`. They are a form of state in a functional world and that should be avoided. – Enigmativity Sep 19 '21 at 00:16
  • 1
    https://stackoverflow.com/questions/14396449/why-are-subjects-not-recommended-in-net-reactive-extensions – Enigmativity Sep 19 '21 at 00:16
  • Thanks for the insights! OT, what's up with Rx .NET low adoption rate? After all, It was the original Rx implementation back in the day. Today: [RxJava repo](https://github.com/ReactiveX/RxJava) - 45.3K stars, [rxjs repo](https://github.com/ReactiveX/rxjs) - 25.6K stars, [Rx .NET repo](https://github.com/dotnet/reactive) - 5.1K stars. – noseratio Sep 19 '21 at 01:00
-1

SetCursorPosition works perfectly fine when not called from another thread. you can use an asynchronous approach to solve the problem instead of using Task.Run

  class Program
    {
        static void Main(string[] args)
        {
            var generateSeq = new GenerateSequence();
            Console.WriteLine("Hello World!");

            generateSeq.Sequence.Subscribe(val =>
            {
                Console.WriteLine(val);
                // move cursor back to previous line
                Console.SetCursorPosition(0 ,Console.CursorTop - 1);
            });
            
            // start background operation
            generateSeq.Run();
        }
    }
    
    class GenerateSequence
    {
        public readonly Subject<int> Sequence = new();

        public void Run(int runTimes = 10)
        {
            ConsoleKeyInfo cki;

            // create a cancelation token, because if the user presses
            // Escape key we don't need to run our background task 
            // anymore and the task should be stopped.
            var tokenSource = new CancellationTokenSource();
            var token = tokenSource.Token;

            // we can not use await keyword here because we need to 
            // listen to ReadKey functions in case the user wants to 
            // stop the execution. without the await, task will run in 
            // the background asynchronously
            var task = RunForTimes(runTimes,token);

            // wait for the Escape key to cancel the execution or stop it 
            // if it's already running
            do
            {
                cki = Console.ReadKey();
            } while (cki.Key != ConsoleKey.Escape && !task.IsCompleted);

            // cancel the background task if it's not compeleted.
            if (!task.IsCompleted)
                tokenSource.Cancel();    
            
            // Revert CursorPosition to the original state
            Console.SetCursorPosition(0, Console.CursorTop + 1); 

            Console.WriteLine("Execution ends");
        }

        // we use an async task instead of a void to run our background 
        // job Asynchronously.
        // the main difference is, we should not use a separate thread 
        // because we need to be on the main thread to safely access the Console (to read or write)
        private async Task RunForTimes(int runTimes, CancellationToken token)
        {
            for (var i = 0; i < runTimes; i++)
            {
                Sequence.OnNext(i);
                await Task.Delay(1000, token);
             // exit the operation if it is requested
                if (token.IsCancellationRequested) return;
            }
        }
    }

enter image description here

AliReza Sabouri
  • 4,355
  • 2
  • 25
  • 37
  • Yes it works on Windows terminal ... I can also confirm that ... I have tested it now too on Windows VM ... but same code doesn't work on Mac ... And but again its not a `Console.SetCursorPosition ` ... if I use `Console. SetCursorPosition` without threading, it works on Mac terminal too ... its with threading its not working ... – microchip78 Sep 15 '21 at 05:46
  • have you tried **Linux Machine/VM** or **WSL**. I haven't tried WSL but yes, its not working on Linux machine ... – microchip78 Sep 15 '21 at 06:02
  • That change from `Task.Run(() => runForTimes(10));` to `await RunForTimes(10);` defeats the whole purpose. Purpose of starting a new task at that point is, I can run that task in parallel ... while it is waiting for that `RunForTimes` function execute, if user hit the `escape` key, that will exit the application. If I change from `Task.Run(() => runForTimes(10));` to `await runForTimes(10)`, basically `do ... while` will never get executed till `RunForTimes` finish its execution. – microchip78 Sep 15 '21 at 06:21
  • Oh, I didn't see your comments till now xD. yes, you were right I hope the latest answer update fixes your problem. @microchip78 – AliReza Sabouri Sep 15 '21 at 06:43
  • thats excellent ... That works perfectly fine ... Thank you so much for your time and looking into it ... I am sorry, I didn't make myself clear in my question or in my previous comment – microchip78 Sep 15 '21 at 06:44
  • Happy to hear that. your welcome and good luck. – AliReza Sabouri Sep 15 '21 at 06:46
  • Just to complete the answer, if you can add bit of explanation why it was happening and how this solution fixed the problem. It will help someone who will have similar problem in future ... – microchip78 Sep 15 '21 at 07:43
  • the only important thing is changing the code from a multithreaded to asynchronous version. I've added also some comments maybe it helps. also this you can check this post. https://stackoverflow.com/questions/32726294/is-async-await-using-task-run-starting-a-new-thread-asynchronously – AliReza Sabouri Sep 15 '21 at 08:17
  • *Try this* is an extremely poor answer. Instead, you should epxlain what changes you've made to the code and how those changes fix the poster's issue. Just dumping code and saying *Try this* does not impart any information that allows the poster (and others reading this question) to learn. – Ken White Sep 15 '21 at 12:33
  • @KenWhite I agree with you. but before this answer, we didn't even know what is the OP's problem. now it is simple and you are right but it costs about 1 hour of dealing with different solutions to understand the problem. feel free to post a better answer. – AliReza Sabouri Sep 15 '21 at 13:19
  • This is not a wiki site. It's supposed to be a repository of knowledge, which means that answers should provide an answer as well as education to increase knowledge and understanding. Just dumping code and saying *Try this* is not actually an answer at all - *try this* sounds more like a guess that what you posted *might or might not work*, so the person should *try it* to find out. That's not an effort to share knowledge. If you go to [meta.se] and search for *Is try this an answer*, you'll find that the general consensus of site users say it's not. – Ken White Sep 15 '21 at 14:30
  • The `await Task.Delay(1000, token);` will throw if the token is canceled. The `if (token.IsCancellationRequested) return;` line will not throw if the token is canceled. So you have two completion strategies for the `RunForTimes` task, where the same signal may result in two different outcomes, in a non-deterministic fashion. That's not good. Neither is good that this task is not awaited, so this inconsistency, or any unexpected exception that may occur, will pass unobserved. Hence my downvote. – Theodor Zoulias Sep 16 '21 at 05:51
  • @TheodorZoulias you are right this line `if (token.IsCancellationRequested) return;` should be bellow `await Task.Delay(1000, token);` but in this example, it doesn't matter. but I updated the answer – AliReza Sabouri Sep 21 '21 at 11:02
  • How is the inconsistency resolved by changed the order of the lines? The cancellation of the token can still produce two different outcomes, in a non-deterministic fashion. And the task returned by the `RunForTimes` method is still not awaited, so nobody will ever know if it failed and for what reason. – Theodor Zoulias Sep 21 '21 at 11:47
  • the problem is you mentioned a different outcome that doesn't exist in this example or at least I'm not able to find it. in this scenario, because we have an infinite loop after the task(kind of) there is always one outcome. but I should say I agree the entire implementation is not ideal but I fixed only the OPs problem by his implementation. (for example, before this I never faced the Subjects at all, I think using the subjects causing these problem at the first place) – AliReza Sabouri Sep 21 '21 at 13:04