Home > database >  How can I await for a button click in an async method?
How can I await for a button click in an async method?

Time:11-30

I try to write a code to read a JSON File and allows user to input all the parametes for the objects in the JSON File one by one.
I try to write something like an "awaitable Button", but I failed to write a "GetAwaiter" extension for the button, although I found informations about how to do it.

https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-inherit-from-existing-windows-forms-controls?view=netframeworkdesktop-4.8

how can I combine await.WhenAny() with GetAwaiter extension method

http://blog.roboblob.com/2014/10/23/awaiting-for-that-button-click/

So here is my code after clicking a button "loadJSON":

for (int i = 0; i<templist_net.Count; i  )
{
    GeneratorFunctions.GetNetworkParameterList(templist_net[i].Type, templist_net[i], treeViewPath.SelectedPath, SolutionFolder);
    cBoxPouItem.Text = templist_net[i].Type;

    ListViewParam2.ItemsSource = GeneratorFunctions.TempList;   // Parameter list binding
    temp = GeneratorFunctions.TempList;
    ListViewParam2.Visibility = Visibility.Visible;             // Set list 2 visible
    ListViewParam.Visibility = Visibility.Collapsed;            // Set list 1 invisible

    //something stop loop, and wait user to type parameters in Listview, and click Button, Then the loop move on. 
}

And Here is code trying to write a Button with extension. I add a new class for custom control, and write the extension.

public partial class CustomControl2 : System.Windows.Forms.Button
{
    static CustomControl2()
    {

    }
    public static TaskAwaiter GetAwaiter(this Button self)
    {
        ArgumentNullException.ThrowIfNull(self);
        TaskCompletionSource tcs = new();
        self.Click  = OnClick;
        return tcs.Task.GetAwaiter();

        void OnClick(object sender, EventArgs args)
        {
            self.Click -= OnClick;

            tcs.SetResult();
        }
    }
}

But I can't write a extension, which inherit System.Windows.Forms.Button. What should I do?

UPDATE: here is what i tried.

 private async Task Btn_loadJsonAsync(object sender, RoutedEventArgs e) {
        // Initialize an open file dialog, whose filter has a extend name ".json"
        OpenFileDialog openFileDialog = new OpenFileDialog();
        openFileDialog.Filter = "(*.json)|*.json";
        TextBoxInformation.Text  = "Opening project ...\n";
        if (openFileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
        { 
            networks = GeneratorFunctions.ReadjsonNetwork(openFileDialog.FileName);
            for (int i = 0; i < networks.Count; i  )
            {
                if (temp != null)
                {
                    if (networks[i].Type == "Network")
                    {
                        templist_net.Add(networks[i]);
                        i = 1;
                    }
                    if (networks[i].Type == "Subsystem")
                    {
                        templist_sub.Add(networks[i]);
                        i = 1;
                    }
                    if (networks[i].Type == "Component: Data Point Based Control")
                    {
                        templist_com.Add(networks[i]);
                        i = 1;
                    }
                }
            }
            using (SemaphoreSlim semaphore = new SemaphoreSlim(0, 1))
            {
                void OnClick(object sender, RoutedEventArgs e) => semaphore.Release();
                btn.Click  = OnClick;

                for (int i = 0; i < templist_net.Count; i  )
                {
                    //...

                    //wait here until [btn] is clicked...
                    await semaphore.WaitAsync();
                }

                btn.Click -= OnClick;
            }}}

CodePudding user response:

Although you may want to redesign the way you are doing things, a quick an dirty solution would be to use a dialog box in modal mode and upon the dialog box closing, capture the data that got input and continue looping. The loop will block until the dialog box is closed.

CodePudding user response:

You can wait for a button click asynchronously using a SemaphoreSlim, e.g.:

using (SemaphoreSlim semaphore = new SemaphoreSlim(0, 1))
{
    void OnClick(object sender, RoutedEventArgs e) => semaphore.Release();
    btn.Click  = OnClick;

    for (int i = 0; i < templist_net.Count; i  )
    {
        //...

        //wait here until [btn] is clicked...
        await semaphore.WaitAsync();
    }

    btn.Click -= OnClick;
}

CodePudding user response:

First of all I must insist that your request goes against the principles of the MVVM pattern which is based on events.
Your logic should be in a separate class and expose an OnNext method which should be called from the model through an ActionCommand

Anyway, to conform (as much as possible) to the MVVM pattern, you don't want to await on the button but more on the command bound to the button.
So let's build an awaitable command :

public class AwaitableCommand : ICommand
{
    private readonly object _lock = new();
    
    private TaskCompletionSource? _taskCompletionSource;

    /// <summary>
    /// null-event since it's never raised
    /// </summary>
    public event EventHandler? CanExecuteChanged
    {
        add { }
        remove { }
    }

    /// <summary>
    /// Always executable
    /// </summary>
    public bool CanExecute(object? parameter) => true;
    

    public void Execute(object? parameter)
    {
        lock (_lock)
        {
            if (_taskCompletionSource is null)
                return;

            _taskCompletionSource.SetResult();

            // reset the cycle
            _taskCompletionSource = null;
        }
    }

    public Task WaitAsync()
    {
        lock (_lock)
        {
            // start a new cycle if needed
            _taskCompletionSource ??= new TaskCompletionSource();
            return _taskCompletionSource.Task;
        }
    }
}

Then you can create your logic with it (I put it in the model, wich is a bad practice):

public class Model : NotifyPropertyChangedBase
{
    private int _count;

    public Model()
    {
        RunLogicAsync();
    }

    public int Count
    {
        get => _count;
        private set => Update(ref _count, value);
    }

    public AwaitableCommand OnNextCommand { get; } = new();
    
    /// <summary>
    /// I know, I know, we should avoid async void
    /// </summary>
    private async void RunLogicAsync()
    {
        try
        {
            for (;;)
            {
                await OnNextCommand.WaitAsync();
                Count  ;
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
}

And your view:

<Window ...>
    <Window.DataContext>
        <viewModel:Model />
    </Window.DataContext>

    <Window.Resources>
        <system:String x:Key="StringFormat">You clicked it {0} times</system:String>
    </Window.Resources>

    <Grid>
        <Button Content="{Binding Count}"
                ContentStringFormat="{StaticResource StringFormat}"
                Command="{Binding OnNextCommand}"
                Padding="10 5"
                HorizontalAlignment="Center"
                VerticalAlignment="Center" />
    </Grid>
</Window>

Working demo available here.

  • Related