9

C# noob here, coming from experience in other languages. (Most notably Java).

I'm looking at this question's code. It's a standard WinForms C# project in VS 2013:

drop a button and a textbox on the form and use this code:

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
        .ContinueWith(t => DelayedAdd(t.Result, 20))
        .ContinueWith(t => DelayedAdd(t.Result, 30))
        .ContinueWith(t => DelayedAdd(t.Result, 50))
        .ContinueWith(t => textBox1.Text = t.Result.ToString(),
            TaskScheduler.FromCurrentSynchronizationContext());
}

private int DelayedAdd(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Press the button, wait 2 seconds for the four DelayedAdd calls to complete, and the result (115) is displayed in the text box. How can I get the result displayed on the text box after every DelayedAdd call?

I tried to shove the final continuation between each call,

Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
    .ContinueWith(t => textBox1.Text = t.Result.ToString(),
        TaskScheduler.FromCurrentSynchronizationContext())

    .ContinueWith(t => DelayedAdd(t.Result, 20))
    .ContinueWith(t => textBox1.Text = t.Result.ToString(),
        TaskScheduler.FromCurrentSynchronizationContext())

    .ContinueWith(t => DelayedAdd(t.Result, 30))
    .ContinueWith(t => textBox1.Text = t.Result.ToString(),
        TaskScheduler.FromCurrentSynchronizationContext())

    .ContinueWith(t => DelayedAdd(t.Result, 50))
    .ContinueWith(t => textBox1.Text = t.Result.ToString(),
        TaskScheduler.FromCurrentSynchronizationContext());

but that fails, I'm guessing because the continuations I inserted don't return the integer result t. I'm such a C# noob that I don't even know how to fix that, let alone do this in an idiomatic way.

Community
  • 1
  • 1
kdbanman
  • 10,161
  • 10
  • 46
  • 78

2 Answers2

5

I'd just change DelayedAdd method:

    private void button1_Click(object sender, EventArgs e)
    {
        Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
            .ContinueWith(t => DelayedAdd(t.Result, 20))
            .ContinueWith(t => DelayedAdd(t.Result, 30))
            .ContinueWith(t => DelayedAdd(t.Result, 50));
    }

    private int DelayedAdd(int a, int b)
    {
        if (textBox1.InvokeRequired)
        {
            textBox1.BeginInvoke((Action)(() => textBox1.Text = (a + b).ToString()));
        }
        Thread.Sleep(500);
        return a + b;
    }
voytek
  • 2,202
  • 3
  • 28
  • 44
  • Interesting. Why the `InvokeRequired` guard? I have no idea what that is. – kdbanman Jun 25 '15 at 20:27
  • When you want to update your UI from different thread, that's the way to go – voytek Jun 25 '15 at 20:29
  • I thought that was already covered by the second `ContinueWith` parameter, `TaskScheduler.FromCurrentSynchronizationContext()`? – kdbanman Jun 25 '15 at 20:31
  • 1
    Well, you're right. `FromCurrentSynchronizationContext` means that you will run your task in UI thread. So you have to use two functions here: one for update UI and another for sleeping. I just wanted to point out that you can use only one function. You can use you very first example from the question, just change `DelayedAdd` method. Look at my edited answer – voytek Jun 25 '15 at 20:57
  • I like how little code your solution is. My solution is more verbose because I tried to decouple the blocking method from UI code. Thanks very much for the crash course! – kdbanman Jun 26 '15 at 16:49
  • You'll definitely want to use and Invoke process to allow other threads to manipulate what you need. – Andrew Grinder Jun 26 '15 at 17:02
  • @AndrewGrinder, why is that? Do you mean *instead* of `FromCurrentSynchronizationContext`, or *in addition* to it? – kdbanman Jun 26 '15 at 18:48
  • @kdbanman What I usually need is to update the user interface so the user can see the status of the item that is processing. So, you'll need to invoke each control in the Form's `.cs`. That can be done with one method. The one I have tells the form to look for a certain control, specify the string representation of the properties (ie. `ForeGround`), and set that property to an `object` value. Linking another question that answers your basic need to change a form's control from another thread: http://stackoverflow.com/questions/2562946/cross-thread-winforms-control-editing `WPF is much simpler` – Andrew Grinder Jun 30 '15 at 04:24
1

Got it! If anyone has better/more interesting alternatives, please answer.

First I tried a multiline lambda in place of the erroneous continuations from my question:

    .ContinueWith(t => {
        textBox1.Text = t.Result.ToString();
        return t.Result;
    },
        TaskScheduler.FromCurrentSynchronizationContext()); 
    }

That's a tad verbose and repetitive, so I DRYed it up:

    private void button1_Click(object sender, EventArgs e)
    {
        // SCHEDULING LOGIC
        Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
            .ContinueWith(t => UpdateText(t.Result),
                TaskScheduler.FromCurrentSynchronizationContext())

            .ContinueWith(t => DelayedAdd(t.Result, 20))
            .ContinueWith(t => UpdateText(t.Result),
                TaskScheduler.FromCurrentSynchronizationContext())

            .ContinueWith(t => DelayedAdd(t.Result, 30))
            .ContinueWith(t => UpdateText(t.Result),
                TaskScheduler.FromCurrentSynchronizationContext())

            .ContinueWith(t => DelayedAdd(t.Result, 50))
            .ContinueWith(t => UpdateText(t.Result),
                TaskScheduler.FromCurrentSynchronizationContext()); 
    }

    private int UpdateText(int i)
    {
        // UI LOGIC
        textBox1.Text = i.ToString();
        return i;
    }

    private int DelayedAdd(int a, int b)
    {
        // PROCESS LOGIC
        Thread.Sleep(500);
        return a + b;
    }
kdbanman
  • 10,161
  • 10
  • 46
  • 78