2

I'm trying to create an app with C# WPF to simulate Windows' command prompt, but with more flexibility and output options (like displaying images or forms). I recently got stuck on trying to simulate Console.ReadLine(). I need to keep the GUI fully responsive, allowing the user to type input. At the same time, I need to be able to return the answer from the same method.

I tried to solve this problem already by using events, but I can't figure out how to use them in a way that won't return void. I looked into async / await and a question about it, but couldn't quite figure out how to use that information. I considered an event-driven solution where the result would be stored in a permanent list variable for all of the inputs, which I could read the last of to get the latest input, but decided it wasn't good enough for what I'm simulating.

I plan on creating the console GUI in the main thread as soon as the application starts. However, I will be using logic from it in another thread which will be the meat of my code (I know it's not a professional way to program, but this is a personal project / learning experience, after all.) Then, I want to use some sort of custom ReadLine() method to wait until the user submits text, then return it. If this is possible, how can it be done in WPF?

Community
  • 1
  • 1
  • So, you want another thread to handle the console GUI, what about the main thread? what will it be doing? Displaying some WPF window? – Yacoub Massad Sep 14 '15 at 00:05
  • @YacoubMassad oh, I wrote that wrong. Thanks for noticing so quickly. Fixed. –  Sep 14 '15 at 00:06
  • So, ReadLine will be called on a background thread? And when you call it, do expect that it should block (wait) until the user writes something and hits enter? – Yacoub Massad Sep 14 '15 at 00:09
  • Yes, that's the goal. I know that it means using the console requires the user to create a new thread in order to call `Read` the way it's intended, but I don't actually know a cleaner way to handle that. (It's another issue for another time.) –  Sep 14 '15 at 00:10

2 Answers2

0

Use the BlockingCollection class.

Here is how I see it could be done:

public class MyConsole
{
    private readonly BlockingCollection<string> m_Lines = new BlockingCollection<string>();

    public string ReadLine()
    {
        return m_Lines.Take();
    }

    private void AddNewLine(string new_line)
    {
        m_Lines.Add(new_line);
    }

    //...
}

The AddNewLine method is a private method that you must call when the user writes something and hits enter. This happens in the GUI thread.

The ReadLine method is a public method that your other thread will call to get a new line.

Please note that this call will return a stored item if there is any, or it will wait until a new item is added by the AddNewLine method. This blocking is a feature of the BlockingCollection class.

Yacoub Massad
  • 27,509
  • 2
  • 36
  • 62
  • I'm trying to write to the console, then read from it, and write what I read. When I write to the console (currently in App.xaml.cs), it appears. If I try to read from App.xaml.cs, it blocks the application since it's on the GUI thread. Threads, ThreadPool, Dispatcher / BackgroundWorkers don't appear to let me circumvent this: Either the Read() never gets called or, when the second Write() is on the GUI thread, it simply goes to that because Read() is asynchronous. See my code: http://pastebin.com/BJdxh8Gk I can't debug it either because once the code hits Read(), the debugger can't follow. –  Sep 14 '15 at 02:18
  • Why in your Read method, you reinitialize the blocking collection to a new one? – Yacoub Massad Sep 14 '15 at 09:46
  • If it only blocks when the list is empty, I only want to store up to 1 element so I can use its blocking feature. Otherwise, Read would only work once correctly, right? Since there's no `.Clear()` method, I just overwrote the list. –  Sep 14 '15 at 13:31
  • Thanks for the clarification. I've implemented Evk's solution at the moment, and it seems to be working fine. It's always nice to learn new things, though :) –  Sep 15 '15 at 00:48
  • You're welcome. Good to hear your problem is solved. – Yacoub Massad Sep 15 '15 at 00:53
0

The following quick and dirty code should give you an idea how to achieve what you want:

public partial class MainWindow : Window {
    public MainWindow() {
        InitializeComponent();
        var console = new MyConsole();
        this.Content = console.Gui;
        Task.Factory.StartNew(() => {
            var read = console.ReadLine();
            console.WriteLine(read);
        });
    }
}

public class MyConsole {
    private readonly ManualResetEvent _readLineSignal;
    private string _lastLine;        
    public MyConsole() {
        _readLineSignal = new ManualResetEvent(false);
        Gui = new TextBox();
        Gui.AcceptsReturn = true;
        Gui.KeyUp += OnKeyUp;
    }

    private void OnKeyUp(object sender, KeyEventArgs e) {
        // this is always fired on UI thread
        if (e.Key == Key.Enter) {
            // quick and dirty, but that is not relevant to your question
            _lastLine = Gui.Text.Split(new string[] { "\r\n"}, StringSplitOptions.RemoveEmptyEntries).Last();
            // now, when you detected that user typed a line, set signal
            _readLineSignal.Set();
        }
    }        

    public TextBox Gui { get; private set;}

    public string ReadLine() {
        // that should always be called from non-ui thread
        if (Gui.Dispatcher.CheckAccess())
            throw new  Exception("Cannot be called on UI thread");
        // reset signal
        _readLineSignal.Reset();
        // wait until signal is set. This call is blocking, but since we are on non-ui thread - there is no problem with that
        _readLineSignal.WaitOne();
        // we got signalled - return line user typed.
        return _lastLine;
    }

    public void WriteLine(string line) {
        if (!Gui.Dispatcher.CheckAccess()) {
            Gui.Dispatcher.Invoke(new Action(() => WriteLine(line)));
            return;
        }

        Gui.Text += line + Environment.NewLine;
    }
}
Evk
  • 98,527
  • 8
  • 141
  • 191