I've used this pattern very successfully. But you are right about the giant switch-case, it does require a bit of boilerplate. But then again, it's not actually any shorter with callbacks.
Example:
Button pressedButton = await WaitForButtonClickAsync(ct, button1, button2, button3);
if (pressedButton == button1) await DoStuff1(ct);
if (pressedButton == button2) await DoStuff2(ct);
if (pressedButton == button3) await DoStuff3(ct);
Or with callbacks (Note that we aren't removing event handlers here, so this solution isn't at all shorter in reality), though here we don't get the advantage of chaining async code:
button1.onClick += () => DoStuff1();
button2.onClick += () => DoStuff2();
button3.onClick += () => DoStuff3();
You could also make some kind of helper to avoid the if-elses, something like this:
await WaitForButtonHandlersAsync(ct,
(button1, DoStuff1),
(button2, DoStuff2),
(button3, DoStuff3));
But as you start mixing in other awaits in addition to buttons, this can get messy, and I've always just fallen back to if-else listings.
EDIT:
One thing to ad is that you can also group things. For example if you have a commonly used set of navigation buttons/gestures in addition to a few context specific buttons, you can bundle the navigation buttons into one task. This example also contains the usage of linked tokens presented in the video.
using (var cts = CreateLinkedTokenSource(ct))
{
var navigationTask = WaitForNavigationEventAsync(cts.Token);
var buttonTask1 = WaitForButtonClickAsync(cts.Token, button1);
var buttonTask2 = WaitForButtonClickAsync(cts.Token, button2);
var finishedTask = await Task.WhenAny(navigationTask, buttonTask1, buttonTask2);
await finishedTask; // "Reveal" and propagate exceptions
cts.Cancel();
if (finishedTask == navigationTask) await Navigate(navigationTask.Result, ct);
if (finishedTask == buttonTask1) await DoStuff1(ct);
if (finishedTask == buttonTask2) await DoStuff2(ct);
}
With this kind of constructs, the listing never gets out of hands and you avoid some repeating code.