0

I'm coding the backend portion of a software and at some point I need my user to choose some things, it can be a convoluted process, the user can even cancel the selection at any point.

From the back end I'd like to do something like:

private async void StartAction()
{
    //some code
    var SelectedItem = await UI.RequestUserToChooseItem();
    // some final code using the selected item
}

Here I don't know how to handle cancellation, but I can send null and assume that if the SelectedItem is null it was canceled.

But what about the UI portion of it? How do I handle it to return the call when the thing is selected by the user?

I need to perform some steps here: (this is pseudocode, I don't even know where to start)

public List<Item> RequestUserToChooseItem()
{
    PrepareItemsInList();
    ShowSelectionPanel();
    List<Items> SelectedItemsFromPanel = WaitForUserToChose(); //???????
    return SelectedItemsFromPanel;

}

And then we have the cancel button:

private void CancelButtonClicked(object sender, EventArgs e)
{
    CancelWaitedSelectionProcessAndReturnNull(); //????
}
javirs
  • 1,049
  • 26
  • 52
  • 1
    https://stackoverflow.com/q/499294/1136211 – Clemens Mar 04 '22 at 08:23
  • 1
    You don't need asynchronous operations and threads to display a modal dialog box. Have you [checked the dialog box docs?](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/windows/dialog-boxes-overview?view=netdesktop-6.0) ? – Panagiotis Kanavos Mar 04 '22 at 08:33
  • 3
    `async void StartAction` is a bug. `async void` methods are meant only for asynchronous event handlers. You can't modify the UI from a background thread anyway, so whatever `UI.RequestUserToChooseItem();` does won't work unless it somehow gets back to the UI thread. Since you want to *wait* for the user, there's no reason to use asynchronous code – Panagiotis Kanavos Mar 04 '22 at 08:35
  • just for the record, as I stated, the process of selecting stuff is not about showing one modal, there are plenty of steps and actions the user has to do in the UI. I need, from the backend, for him to finish doing all those steps that, eventually, will provide one or various selected item/s – javirs Mar 04 '22 at 12:40
  • Regarding the `UI.RequestUserToChooseItem` method, how does the user choose an item? What are the actions involved in choosing an item? Do they choose by clicking a `Button` for example? – Theodor Zoulias Mar 04 '22 at 14:25
  • Sure, they click in some places, they drag and drop some elements, and at the end they click one button to confirm the selection. The thing is that from the backend I'd like to abstract that all to a call to the UI manager asking him to let the user chose and tell me what he choose – javirs Mar 04 '22 at 16:24
  • I think that you are searching for this question: [Is it possible to await an event instead of another async method?](https://stackoverflow.com/questions/12858501/is-it-possible-to-await-an-event-instead-of-another-async-method) – Theodor Zoulias Mar 04 '22 at 16:38
  • So the backend code should wait until one of two buttons gets clicked, either a button for positive action or a button for cancellation, correct? – Theodor Zoulias Mar 04 '22 at 16:53
  • correct, That's it :) – javirs Mar 07 '22 at 07:18

2 Answers2

1

You can use a TaskCompletionSource to signal the choices. Something like

private TaskCompletionSource<MyOptions> tcs;
public Task<MyOptions> ShowPanelAndWaitForSelection(){
    // show panel and do other initialization
    tcs = new TaskCompletionSource<MyOptions>();
    return  tcs.Task;
}
public void OnOptionSelection(MyOptions value) => tcs.SetResult(value);
public void OnCanceled() => tcs.SetCanceled();

When if the task is canceled, any awaiter will get a OperationCanceledException, so your code would normally look something like:

try{
    ...
    var selectedOption = await ShowPanelAndWaitForSelection();
    ...
}
catch(OperationCanceledException){
    // Handle cancellation
}
catch(Exception e){
   // Handle actual errors
}

This assumes your UI is non-modal, like showing and hiding panels in the same form. If you use modal dialogs for each step you do not need any async code.

This style essentially uses the compiler to generate a state machine, instead of writing such a state machine by hand. I think this can be a useful style to handle specific cases, since you can make a decision tree using regular constructs like if/while etc. But it may not always be a net positive, and it may trip up developers that do not expect it.

JonasH
  • 28,608
  • 2
  • 10
  • 23
0

Here is an asynchronous method that waits asynchronously for the first click on one or more buttons, and returns the clicked button:

public static Task<Button> OnClickAsync(params Button[] buttons)
{
    var tcs = new TaskCompletionSource<Button>();
    foreach (var button in buttons) button.Click += OnClick;
    return tcs.Task;

    void OnClick(object sender, RoutedEventArgs e)
    {
        foreach (var button in buttons) button.Click -= OnClick;
        tcs.SetResult((Button)sender);
    }
}

It could be used like this:

public async Task<List<Item>> RequestUserToChooseItemAsync()
{
    PrepareItemsInList();
    ShowSelectionPanel();
    var selectedButton = await OnClickAsync(btnOK, btnCancel);
    if (selectedButton == btnCancel) return null;
    return SelectedItemsFromPanel;
}

This method should be called exclusively on the UI thread, not on background threads, because it interacts with UI elements.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • This looks totally what I need but I'm not following something. From the back end I just need the List with the selected items. But I don't have access to it from the backend. If RequestIserToChoseItemAsync has to be run in the back end.. Then It cannot access SelectedItemsFromPanel. I need the OnClickAsync to return them. Am... Am I right ? – javirs Mar 07 '22 at 07:30
  • @javirs to be honest I just copy-pasted your code, without giving much thought about what the `SelectedItemsFromPanel` might be. I assumed that it might be a public property of the UI layer, being as accessible as the `PrepareItemsInList` and `ShowSelectionPanel` methods. You might want to edit the question, and include any missing information about these members. – Theodor Zoulias Mar 07 '22 at 07:37
  • so both of your methods run on the UI class ? Why do you need two separared methods? And how do you call the RequestuserToChooseItem and unwrap the returned list of items ? – javirs Mar 07 '22 at 08:00
  • Long story short, I create the `TaskCompletionSource>` and return `tsc.Task` Then whenever I happen to fill in the tcs.SetResult it will actually finish the task and whoever was awaiting will receive the result. Is that how it works ? – javirs Mar 07 '22 at 08:05
  • @javirs yeap, exactly. The `TaskCompletionSource` is generic, and the `TResult` can be anything you like. Be aware that the code that awaits the `.Task` will start running immediately after you call `SetResult`. The code that follows the `SetResult` will have to wait until the code after the `await` completes. If you find this confusing, you can pass the `TaskCreationOptions.RunContinuationsAsynchronously` in the constructor. – Theodor Zoulias Mar 07 '22 at 08:24