Logan B. Lehman
Logan B. Lehman

Reputation: 5007

Tell a Task to wait until Timer has stopped

Explanation & Background: Sorry if this question sucks, but I am trying to wrap my head around Tasks. I currently have a set of classes that will take a control into their constructors, and allow for me to automate user interaction (typing into textbox, clicking button, etc.).

An example of one of these is my class called TextboxTester. Here is a snippet of how I bring in the textbox:

public class TextboxTester
{
    private int _currentTextLength = 0;
    private string _text;
    private TextBox _textBox;
    private Timer timer;

    #region Constructor
    public TextboxTester(TextBox TextBox)
    {
        _textBox = TextBox;
    }
    #endregion

I want to be able to chain actions(tasks) together in order - one after another so I can automate a series of user input events. I read up and found out about TPL tasks, and decided to give it a spin.

I created my TextboxTester class and my ButtonTester class, and passed in the controls. I want to be able to type into the textbox, and then click the button - so I call this:

Task.Factory.StartNew(() => txtTester.WriteText("Hello World")).Wait();
Task.Factory.StartNew(() => btnTester.Click(1));

From my knowledge of the Task.Factory calls, this is what I want to do - perform the first action, wait until it is done - and then perform the next. The problem is that TextboxTester.WriteText() uses a timer to emulate typing into a TextBox (1 character per second):

public void WriteText(string Text)
    {
        if (timer == null)
        {
            State.Working = true;
            timer = new Timer();

            try
            {
                _text = Text;
                timer.Elapsed += new ElapsedEventHandler(timer_ElapsedWrite);
                timer.Interval = 1000;
                timer.Enabled = true;
                timer.Start();
            }
            catch
            {
                MessageBox.Show("WriteText timer could not be started.");
            }
        }
    }
void timer_ElapsedWrite(object sender, ElapsedEventArgs e)
    {
        _textBox.Dispatcher.BeginInvoke(new Action(() =>
        {
            TextBoxAutomationPeer peer = new TextBoxAutomationPeer(_textBox);
            IValueProvider valueProvider = peer.GetPattern(PatternInterface.Value) as IValueProvider;
            valueProvider.SetValue(_text.Substring(0, _currentTextLength));

            if (_currentTextLength == _text.Length)
            {
                timer.Stop();
                State.Working = false;
                timer = null;
                return;
            }

            _currentTextLength++;

        }));
    }

**The _textBox.Dispatcher call in the elapsed event was to prevent a "thread already owns this object" message that was occurring.

Finally the problem: The Task.Factory.StartNew() call does not seem to take into account the time_elapsed event that is still occurring. I want to be able to finish out the events before the Task considers "WriteText" is complete.

and Question: Is there a way to tell a task to be able to take these types of things into account? Am I not understanding Tasks?

EDIT - I am using 3.5 and a 3.5 implementation of TPL

Upvotes: 3

Views: 1470

Answers (1)

max
max

Reputation: 34427

Right now you are getting no benefits from using tasks. Making WriteText an async method can make things much easier:

public async Task WriteText(string text)
{
    TextBoxAutomationPeer peer = new TextBoxAutomationPeer(_textBox);
    IValueProvider valueProvider = peer.GetPattern(PatternInterface.Value) as IValueProvider;
    for(int i = 1; i < text.Length; ++i)
    {
        await Task.Delay(1000); // no need for timer
        valueProvider.SetValue(text.Substring(0, i));
    }
}

So you can write

async void TestMethod()
{
   await txtTester.WriteText("Hello World"));
   Task.Factory.StartNew(() => btnTester.Click(1)); // probably sould be refactored too
}

Edit

Version without async/await with minimal changes to existing code:

public class TextboxTester
{
    /* unchanged code */

    // unfortunately, there is no non-generic version of this class
    // basically it allows us to 'manually' complete Task
    private TaskCompletionSource<object> _tcs;

    public Task WriteText(string Text)
    {
        if (timer == null)
        {
            _tcs = new TaskCompletionSource<object>();
            /* unchanged code */
        }
        return _tcs.Task; // return task which tracks work completion
    }

    /* unchanged code */
            if (_currentTextLength == _text.Length)
            {
                timer.Stop();
                State.Working = false;
                timer = null;
                _tcs.TrySetResult(null); // notify completion
                return;
            }
    /* unchanged code */
}

Now you can write

// well, you could simply make this a blocking method instead
txtTester.WriteText("Hello World").Wait();
btnTester.Click(1);

or better

txtTester.WriteText("Hello World")
         .ContinueWith(t => btnTester.Click(1)); // does not block calling thread

Main idea is to design your async method in a way which allows reporting completion. There are 3 common patterns for that:

  • Begin/End method pairs (BeginWriteText, which returns IAsyncResult and EndWriteText, which accepts IAsyncResult)
  • Event which is invoked when work is completed (public event EventHandler WriteTextCompleted)
  • Task-based approach (method returns Task)

These approaches are well described in Task-based Asynchronous Pattern

Upvotes: 2

Related Questions