user10455609
user10455609

Reputation:

Trying to prevent multiple rapid button presses, but the buttons aren't disabling properly

I have a situation where I am trying to prevent the user from spamming a button, and to do this I have my command taking two functions as parameters. The first is the command that figures out which button was pressed and then performs the appropriate navigation operation. As it stands now when you press the button there is a short delay, I assume while data is loaded, and then the buttons disable a millisecond before the new page is loaded and presented. I would like them to disable as soon as a button press has occurred so they can't be spammed and load multiple pages of the same type.

This specific button press I am trying to resolve has the Page1 ViewModel retrieving an SQL table from a web service. The call to this is in the Page1ViewModel's constructor.

    NavigateAsyncCommand = new RelayCommandAsync<object>(NavigateAsync, CanClickButton);

    public async Task NavigateAsync(object parameter)
    {
        IsBusy = true;

        Xamarin.Forms.Button b = parameter as Xamarin.Forms.Button;
        string page = b.Text;

        switch (page)
        {
            case "Page1":
                await App.MainNavigation.PushAsync(new Views.Page1(), true);
                IsBusy = false;
                return;

            //More cases here
        }
    }

The second function just checks the status of IsBusy and returns the inverse.

    public bool CanClickButton(object parameter)
    {
        return !IsBusy;
    }

In my XAML my buttons are implemented like this

        <Button x:Name="StartButton" 
             Command="{Binding NavigateAsyncCommand}" 
             CommandParameter="{Binding Source={x:Reference StartButton}}"
             Text="{Binding StartText}"
             Grid.Row="1"/>

Upvotes: 1

Views: 802

Answers (2)

TaiT&#39;s
TaiT&#39;s

Reputation: 3216

I ran into this issue once. The way I solved it was the following:

1) I created a "reverse boolean converter"

public class ReverseBooleanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        try
        {
            bool myValue = (bool)value;
            return !myValue ; 
        }
        catch { return true; } // or false
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return Convert(value, targetType, parameter, culture);
    }
}

2) I referenced the converter into xaml header

xmlns:converters="clr-namespace:MyNamespace.Converters"

3) I added the converter into a resource dictionary

<ResourceDictionary>
     <converters:ReverseBooleanConverter x:Key="ReverseBool" />
</ResourceDictionary>

4) Last step, I binded IsEnabled to the IsBusy property of the viewmodel, using the above converter.

<Button x:Name="StartButton" 
    IsEnabled="{Binding IsBusy, Converter={StaticResource ReverseBool}}" 
    ...
    Grid.Row="1"/>

Hope this helps!

Upvotes: 2

foxanna
foxanna

Reputation: 1570

Here is the closest to your initial code solution:

    NavigateCommand = new RelayCommandAsync<object>(NavigateAsync, CanNavigate);  

    ...
    private async Task NavigateAsync(object parameter)
    {
        if (IsBusy)
            return Task.CompletedTask;

        IsBusy = true;
        NavigateCommand.OnCanExecuteChanged();

        var page = (string) parameter;

        switch (page)
        {
            case "Page1":
                await App.MainNavigation.PushAsync(new Views.Page1(), true);

            //More cases here
        }

        IsBusy = false;
        NavigateCommand.OnCanExecuteChanged();
    }

    private bool CanNavigate(object parameter) => !IsBusy;

    ...
    <Button Command="{Binding NavigateCommand}" 
            CommandParameter="{Binding StartText}"
            Text="{Binding StartText}"
            Grid.Row="1"/>

It will inform Button that CanExecute has changed each time IsBusy value changes. And it will ignore clicks that pass through.

In general, you should not init any long-running operation in ViewModel ctor keeping it as simple as possible. Instead, use Page.OnAppearing to inform ViewModel it can start loading.

Upvotes: 0

Related Questions