C# Wait until condition is true

I am trying to write a code that executes when a condition is met. Currently, I am using while...loop, which I know is not very efficient. I am also looking at AutoResetEvent() but i don't know how to implement it such that it keeps checking until the condition is true.

The code also happens to live inside an async method, so may be some kind of await could work?

private async void btnOk_Click(object sender, EventArgs e)
{
        // Do some work
        Task<string> task = Task.Run(() => GreatBigMethod());
        string GreatBigMethod = await task;

        // Wait until condition is false
        while (!isExcelInteractive())
        {
            Console.WriteLine("Excel is busy");
        }

        // Do work
        Console.WriteLine("YAY");
 }


    private bool isExcelInteractive()
    {
        try
        {
            Globals.ThisWorkbook.Application.Interactive = Globals.ThisWorkbook.Application.Interactive;
            return true; // Excel is free
        }
        catch
        {
            return false; // Excel will throw an exception, meaning its busy
        }
    }

I need to find a way to keep checking isExcelInteractive() without CPU stuck in a loop.

Note: There is no event handler in Excel that would be raised when it is not in edit mode.


At least you can change your loop from a busy-wait to a slow poll. For example:

    while (!isExcelInteractive())
    {
        Console.WriteLine("Excel is busy");
        await Task.Delay(25);
    }

Ended up writing this today and seems to be ok. Your usage could be:

await TaskEx.WaitUntil(isExcelInteractive);

code (including the inverse operation)

public static class TaskEx
{
    /// <summary>
    /// Blocks while condition is true or timeout occurs.
    /// </summary>
    /// <param name="condition">The condition that will perpetuate the block.</param>
    /// <param name="frequency">The frequency at which the condition will be check, in milliseconds.</param>
    /// <param name="timeout">Timeout in milliseconds.</param>
    /// <exception cref="TimeoutException"></exception>
    /// <returns></returns>
    public static async Task WaitWhile(Func<bool> condition, int frequency = 25, int timeout = -1)
    {
        var waitTask = Task.Run(async () =>
        {
            while (condition()) await Task.Delay(frequency);
        });

        if(waitTask != await Task.WhenAny(waitTask, Task.Delay(timeout)))
            throw new TimeoutException();
    }

    /// <summary>
    /// Blocks until condition is true or timeout occurs.
    /// </summary>
    /// <param name="condition">The break condition.</param>
    /// <param name="frequency">The frequency at which the condition will be checked.</param>
    /// <param name="timeout">The timeout in milliseconds.</param>
    /// <returns></returns>
    public static async Task WaitUntil(Func<bool> condition, int frequency = 25, int timeout = -1)
    {
        var waitTask = Task.Run(async () =>
        {
            while (!condition()) await Task.Delay(frequency);
        });

        if (waitTask != await Task.WhenAny(waitTask, 
                Task.Delay(timeout))) 
            throw new TimeoutException();
    }
}

Example usage: https://dotnetfiddle.net/Vy8GbV


You can use thread waiting handler

private readonly System.Threading.EventWaitHandle waitHandle = new System.Threading.AutoResetEvent(false);
private void btnOk_Click(object sender, EventArgs e)
{
    // Do some work
    Task<string> task = Task.Run(() => GreatBigMethod());
    string GreatBigMethod = await task;

    // Wait until condition is false
    waitHandle.WaitOne();
    Console.WriteLine("Excel is busy");
    waitHandle.Reset();

    // Do work
    Console.WriteLine("YAY");
 }

then some other job need to set your handler

void isExcelInteractive()
{
   /// Do your check
   waitHandle.Set()
}

Update: If you want use this solution, you have to call isExcelInteractive() continuously with specific interval:

var actions = new []{isExcelInteractive, () => Thread.Sleep(25)};
foreach (var action in actions)
{                                      
    action();
}

This implementation is totally based on Sinaesthetic's, but adding CancellationToken and keeping the same execution thread and context; that is, delegating the use of Task.Run() up to the caller depending on whether condition needs to be evaluated in the same thread or not.

Also, notice that, if you don't really need to throw a TimeoutException and breaking the loop is enough, you might want to make use of cts.CancelAfter() or new CancellationTokenSource(millisecondsDelay) instead of using timeoutTask with Task.Delay plus Task.WhenAny.

public static class AsyncUtils
{
    /// <summary>
    ///     Blocks while condition is true or task is canceled.
    /// </summary>
    /// <param name="ct">
    ///     Cancellation token.
    /// </param>
    /// <param name="condition">
    ///     The condition that will perpetuate the block.
    /// </param>
    /// <param name="pollDelay">
    ///     The delay at which the condition will be polled, in milliseconds.
    /// </param>
    /// <returns>
    ///     <see cref="Task" />.
    /// </returns>
    public static async Task WaitWhileAsync(CancellationToken ct, Func<bool> condition, int pollDelay = 25)
    {
        try
        {
            while (condition())
            {
                await Task.Delay(pollDelay, ct).ConfigureAwait(true);
            }
        }
        catch (TaskCanceledException)
        {
            // ignore: Task.Delay throws this exception when ct.IsCancellationRequested = true
            // In this case, we only want to stop polling and finish this async Task.
        }
    }

    /// <summary>
    ///     Blocks until condition is true or task is canceled.
    /// </summary>
    /// <param name="ct">
    ///     Cancellation token.
    /// </param>
    /// <param name="condition">
    ///     The condition that will perpetuate the block.
    /// </param>
    /// <param name="pollDelay">
    ///     The delay at which the condition will be polled, in milliseconds.
    /// </param>
    /// <returns>
    ///     <see cref="Task" />.
    /// </returns>
    public static async Task WaitUntilAsync(CancellationToken ct, Func<bool> condition, int pollDelay = 25)
    {
        try
        {
            while (!condition())
            {
                await Task.Delay(pollDelay, ct).ConfigureAwait(true);
            }
        }
        catch (TaskCanceledException)
        {
            // ignore: Task.Delay throws this exception when ct.IsCancellationRequested = true
            // In this case, we only want to stop polling and finish this async Task.
        }
    }

    /// <summary>
    ///     Blocks while condition is true or timeout occurs.
    /// </summary>
    /// <param name="ct">
    ///     The cancellation token.
    /// </param>
    /// <param name="condition">
    ///     The condition that will perpetuate the block.
    /// </param>
    /// <param name="pollDelay">
    ///     The delay at which the condition will be polled, in milliseconds.
    /// </param>
    /// <param name="timeout">
    ///     Timeout in milliseconds.
    /// </param>
    /// <exception cref="TimeoutException">
    ///     Thrown after timeout milliseconds
    /// </exception>
    /// <returns>
    ///     <see cref="Task" />.
    /// </returns>
    public static async Task WaitWhileAsync(CancellationToken ct, Func<bool> condition, int pollDelay, int timeout)
    {
        if (ct.IsCancellationRequested)
        {
            return;
        }

        using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
        {
            Task waitTask     = WaitWhileAsync(cts.Token, condition, pollDelay);
            Task timeoutTask  = Task.Delay(timeout, cts.Token);
            Task finishedTask = await Task.WhenAny(waitTask, timeoutTask).ConfigureAwait(true);

            if (!ct.IsCancellationRequested)
            {
                cts.Cancel();                            // Cancel unfinished task
                await finishedTask.ConfigureAwait(true); // Propagate exceptions
                if (finishedTask == timeoutTask)
                {
                    throw new TimeoutException();
                }
            }
        }
    }

    /// <summary>
    ///     Blocks until condition is true or timeout occurs.
    /// </summary>
    /// <param name="ct">
    ///     Cancellation token
    /// </param>
    /// <param name="condition">
    ///     The condition that will perpetuate the block.
    /// </param>
    /// <param name="pollDelay">
    ///     The delay at which the condition will be polled, in milliseconds.
    /// </param>
    /// <param name="timeout">
    ///     Timeout in milliseconds.
    /// </param>
    /// <exception cref="TimeoutException">
    ///     Thrown after timeout milliseconds
    /// </exception>
    /// <returns>
    ///     <see cref="Task" />.
    /// </returns>
    public static async Task WaitUntilAsync(CancellationToken ct, Func<bool> condition, int pollDelay, int timeout)
    {
        if (ct.IsCancellationRequested)
        {
            return;
        }

        using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
        {
            Task waitTask     = WaitUntilAsync(cts.Token, condition, pollDelay);
            Task timeoutTask  = Task.Delay(timeout, cts.Token);
            Task finishedTask = await Task.WhenAny(waitTask, timeoutTask).ConfigureAwait(true);

            if (!ct.IsCancellationRequested)
            {
                cts.Cancel();                            // Cancel unfinished task
                await finishedTask.ConfigureAwait(true); // Propagate exceptions
                if (finishedTask == timeoutTask)
                {
                    throw new TimeoutException();
                }
            }
        }
    }
}