0

I would like to implement an async version of the Device.StartTimer method. The reason is that I need the callback to wait for the boolean result of an async method to be returned.

I can't figure out how to do this, because I can't see how to await the TaskCompletionSource's task. The result is that the boolean value is returned immediately.

Device.StartTimer(TimeSpan.FromMilliseconds(500), () =>
{
    var retVal = false;

    Device.BeginInvokeOnMainThread(async () =>
    {
        TaskCompletionSource<bool> tcs = new();
        retVal = await this.RefreshAsync().ConfigureAwait(false);
        tcs.TrySetResult(retVal);
    });

    // How to await the TaskCompletionSource's Task ?

    return retVal;
});

EDIT: The business problem that I'm trying to solve, is in iOS specific code, displaying a toast and waiting for its animation to complete (alpha = 0).

Because the animation is completely asynchronous, I'm using a TaskCompletionSource and set its result in the completion handler to await the task on the outside.

Here is the code:

private static Task ShowAlertAsync(string message, double seconds, Alignements alignements)
{
    var preferredStyle = UIAlertControllerStyle.Alert;

    if ((alignements & Alignements.Bottom) != 0)
    {
        preferredStyle = UIAlertControllerStyle.ActionSheet;
    }

    TaskCompletionSource<bool> tcs = new();

    var toast = UIAlertController.Create("", message, preferredStyle);
    UIApplication.SharedApplication.KeyWindow.RootViewController.PresentViewController(toast, true, () =>
    {
        UICubicTimingParameters timing = new(UIViewAnimationCurve.EaseIn);
        UIViewPropertyAnimator animator = new(seconds, timing);
        animator.AddAnimations(() => { toast.View.Alpha = 0; });
        animator.AddCompletion((pos) =>
        {
            toast.DismissViewController(true, null);
            toast.Dispose();
            toast = null;
            tcs.TrySetResult(true);
        });
        animator.StartAnimation();
    });
    
    return tcs.Task;
}

Basicaly, RefreshAsync in the previous bloc of code is awaiting ShowAlertAsync.

  • 1
    What problem are you actually trying to solve? – Stephen Cleary May 18 '23 at 10:32
  • The callback is synchronous, but the bool value returned should be obtained through an async method. –  May 18 '23 at 10:53
  • ...on the UI thread, without blocking –  May 18 '23 at 11:15
  • 2
    That's not possible. What business problem are you trying to solve? – Stephen Cleary May 18 '23 at 12:58
  • @StephenCleary I've updated my question with more informations. Hope that helps. –  May 18 '23 at 14:42
  • How is the `Device.StartTimer` code snippet related to the second code snippet? – ToolmakerSteve May 18 '23 at 22:17
  • 1
    I’m voting to close this question because, according to Olivier’s comment on my answer, they have solved their need in a different way, since it is not possible to do what they ask here. – ToolmakerSteve May 19 '23 at 18:03
  • I'm about to respond to my own question, because I found an equivalent for an async timer. –  May 22 '23 at 07:48
  • Define "async timer". What's that supposed to be? The idea of a timer is to execute some functionality or send a signal in a fixed interval without introducing an offset or delay. – Julian May 28 '23 at 08:44

2 Answers2

0

This part of code:

Device.BeginInvokeOnMainThread(async () =>
    {
        TaskCompletionSource<bool> tcs = new();
        retVal = await this.RefreshAsync().ConfigureAwait(false);
        tcs.TrySetResult(retVal);
    });

    // How to await the TaskCompletionSource's Task ?

change to:

await MainThread.InvokeOnMainThreadAsync( async () =>
    {
        TaskCompletionSource<bool> tcs = new();
        retVal = await this.RefreshAsync().ConfigureAwait(false);
        tcs.TrySetResult(retVal);
        await tcs.Task();   // await TaskCompletionSource's Task
    });

or maybe simply:

await MainThread.InvokeOnMainThreadAsync( async () =>
    {
        await this.RefreshAsync();
    });
ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • Because it is indeed not possible, and because I have no idea what business problem Device.StartTimer is solving, I decided to get rid of it. I also duplicated synchronous methods in async ones. Now it works like a charm. –  May 19 '23 at 06:40
0

I found a way to use an async timer:

var stop = false;
while (!stop)
{
    stop = await this.RefreshAsync().ConfigureAwait(true);
    if (!stop)
    {
        await Task.Delay(TimeSpan.FromMilliseconds(500)).ConfigureAwait(true);
    }
}

The inspiration comes from this answer .

EDIT:

There is a pull request in the GitHub repo of Xamarin.Forms to provide an overload of Device.StartTimer that takes an async callback.

EDIT 2: The proposed overload has a bug, because it does not await the async callback. So I needed to modify it to await the callback.

Here is the code. This is iOS only:

public void StartTimer(TimeSpan interval, Func<Task<bool>> callback)
{
#pragma warning disable CA2000 // Dispose objects before losing scope
    NSTimer timer = NSTimer.CreateRepeatingTimer(interval, async t =>
    {
        await callback().ContinueWith(
            completedTask =>
        {
            if (completedTask.IsFaulted
                || (completedTask.IsCompleted && !completedTask.Result))
            {
                t.Invalidate();
                t.Dispose();
            }
        }, CancellationToken.None,
            TaskContinuationOptions.None,
            TaskScheduler.Default).ConfigureAwait(true);
    });
#pragma warning restore CA2000 // Dispose objects before losing scope

    NSRunLoop.Main.AddTimer(timer, NSRunLoopMode.Common);
}
  • 1
    Look at [PeriodicTimer](https://learn.microsoft.com/en-us/dotnet/api/system.threading.periodictimer?view=net-7.0) – Alexander Petrov May 22 '23 at 08:09
  • That PR is closed and won't be merged since Xamarin.Forms has a successor with .NET MAUI. Are you aware that this solution introduces an offset? Your `RefreshAsync()` method will **not** be called every 500 milliseconds. Instead, it will be called every 500 milliseconds + the execution time of the `RefreshAsync()` method. – Julian May 28 '23 at 08:41
  • I suppose the synchronous version [Device.StartTimer](https://learn.microsoft.com/en-us/dotnet/api/xamarin.forms.device.starttimer?view=xamarin-forms) has the same problem. –  Jun 02 '23 at 08:59