如何真正避免在Xamarin.Forms中同时点击多个按钮?

时间:2018-04-23 17:39:57

标签: c# xamarin.forms

我在视图中使用多个按钮,每个按钮都会显示自己的弹出页面。在同时单击多个按钮的同时,它会一次转到不同的弹出页面。

我创建了一个包含3个按钮的示例内容页面(每个按钮都转到不同的弹出页面)来演示此问题:

screenshots

XAML页面:

<ContentPage.Content>
    <AbsoluteLayout>

        <!-- button 1 -->
        <Button x:Name="button1" Text="Button 1"
            BackgroundColor="White" Clicked="Button1Clicked"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.3, 0.5, 0.1"/>

        <!-- button 2 -->
        <Button x:Name="button2" Text="Button 2"
            BackgroundColor="White" Clicked="Button2Clicked"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.5, 0.1"/>

        <!-- button 3 -->
        <Button x:Name="button3" Text="Button 3"
            BackgroundColor="White" Clicked="Button3Clicked"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.7, 0.5, 0.1"/>

        <!-- popup page 1 -->
        <AbsoluteLayout x:Name="page1" BackgroundColor="#7f000000" IsVisible="false"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 1.0, 1.0">
            <BoxView Color="Red"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.75, 0.3"/>
            <Label Text="Button 1 clicked" TextColor="White"
                HorizontalTextAlignment="Center"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.45, 0.75, 0.05"/>
            <Button Text="Back" BackgroundColor="White" Clicked="Back1Clicked"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"/>
        </AbsoluteLayout>

        <!-- popup page 2 -->
        <AbsoluteLayout x:Name="page2" BackgroundColor="#7f000000" IsVisible="false"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 1.0, 1.0">
            <BoxView Color="Green"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.75, 0.3"/>
            <Label Text="Button 2 clicked" TextColor="White"
                HorizontalTextAlignment="Center"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.45, 0.75, 0.05"/>
            <Button Text="Back" BackgroundColor="White" Clicked="Back2Clicked"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"/>
        </AbsoluteLayout>

        <!-- popup page 3 -->
        <AbsoluteLayout x:Name="page3" BackgroundColor="#7f000000" IsVisible="false"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 1.0, 1.0">
            <BoxView Color="Blue"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.75, 0.3"/>
            <Label Text="Button 3 clicked" TextColor="White"
                HorizontalTextAlignment="Center"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.45, 0.75, 0.05"/>
            <Button Text="Back" BackgroundColor="White" Clicked="Back3Clicked"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"/>
        </AbsoluteLayout>

    </AbsoluteLayout>
</ContentPage.Content>

C#事件处理程序:

void Button1Clicked(object sender, EventArgs e)
{
    // ... do something first ...
    page1.IsVisible = true;
    Console.WriteLine("Button 1 Clicked!");
}

void Button2Clicked(object sender, EventArgs e)
{
    // ... do something first ...
    page2.IsVisible = true;
    Console.WriteLine("Button 2 Clicked!");
}

void Button3Clicked(object sender, EventArgs e)
{
    // ... do something first ...
    page3.IsVisible = true;
    Console.WriteLine("Button 3 Clicked!");
}

void Back1Clicked(object sender, EventArgs e)
{
    page1.IsVisible = false;
}

void Back2Clicked(object sender, EventArgs e)
{
    page2.IsVisible = false;
}

void Back3Clicked(object sender, EventArgs e)
{
    page3.IsVisible = false;
}

预期:
单击button1打开page1弹出页面,然后单击弹出窗口中的后退按钮会隐藏弹出页面。类似于button2button3的行为。

实际
同时单击多个按钮(例如button1button2)会打开两个弹出页面(page1page2)。快速双击一个按钮也可以激发相同的按钮两次。

关于避免双击的一些研究
通过在stackoverflow中搜索类似的问题(例如thisthis),我得出结论,你应该设置一个外部变量来控制事件是否被执行。这是我在Xamarin.forms中的实现:

C#struct作为外部变量,以便我可以在不同的类中访问此变量:

// struct to avoid multiple button click at the same time
public struct S
{
    // control whether the button events are executed
    public static bool AllowTap = true;

    // wait for 200ms after allowing another button event to be executed
    public static async void ResumeTap() {
        await Task.Delay(200);
        AllowTap = true;
    }
}

然后每个按钮事件处理程序都像这样修改(同样适用于Button2Clicked()Button3Clicked()):

void Button1Clicked(object sender, EventArgs e)
{
    // if some buttons are clicked recently, stop executing the method
    if (!S.AllowTap) return; S.AllowTap = false; //##### * NEW * #####//

    // ... do something first ...
    page1.IsVisible = true;
    Console.WriteLine("Button 1 Clicked!");

    // allow other button's event to be fired after the wait specified in struct S
    S.ResumeTap(); //##### * NEW * #####//
}

这通常很好用。双击同一个按钮可以快速触发按钮事件一次,同时单击多个按钮只打开1个弹出页面。

真正的问题
如上所述,在修改代码(在AllowTap中添加共享状态变量struct S)之后,仍然可以打开多个弹出页面。例如,如果用户使用2个手指按住button1button2,请释放button1,等待大约一秒钟,然后释放button2,两个弹出页面{{1 }}和page1将被打开。

尝试修复此问题失败
如果点击page2button1button2,我会尝试停用所有按钮,如果点击后退按钮,则会启用所有按钮。

button3

然后每个按钮事件处理程序都像这样修改(同样适用于void disableAllButtons() { button1.IsEnabled = false; button2.IsEnabled = false; button3.IsEnabled = false; } void enableAllButtons() { button1.IsEnabled = true; button2.IsEnabled = true; button3.IsEnabled = true; } Button2Clicked()):

Button3Clicked()

每个后退按钮事件处理程序都是这样修改的(同样适用于void Button1Clicked(object sender, EventArgs e) { if (!S.AllowTap) return; S.AllowTap = false; // ... do something first ... disableAllButtons(); //##### * NEW * #####// page1.IsVisible = true; Console.WriteLine("Button 1 Clicked!"); S.ResumeTap(); } Back2Clicked()):

Back3Clicked()

但是,同样的问题仍然存在(能够按住另一个按钮并稍后释放它们同时触发2个按钮)。

在我的应用中禁用多点触控不会是一个选项,因为我在我的应用中的其他页面中需要它。此外,弹出页面也可能包含多个按钮,这些按钮也可以指向其他页面,因此只需使用弹出页面中的后退按钮在void Back1Clicked(object sender, EventArgs e) { page1.IsVisible = false; enableAllButtons(); //##### * NEW * #####// } 中设置变量AllowTap即可。选项也是。

任何帮助将不胜感激。感谢。

修改
&#34; 真正的问题&#34;影响Android和iOS。在Android上,当用户按住按钮时,一旦按钮被禁用,就无法激活按钮。这种保持禁用按钮问题不会影响iOS中的按钮。

6 个答案:

答案 0 :(得分:5)

将您的停用呼叫(disableAllButtons())移至按钮的Pressed事件。

一旦检测到第一次触摸,这将禁用其他按钮。

修改
要防止意外禁用所有内容,请创建自定义按钮渲染器,并挂钩到本机事件以取消禁用拖出: iOS:UIControlEventTouchDragOutside

修改
Android在此方案中已经过测试,它与此问题中描述的问题相同。

答案 1 :(得分:4)

我认为以下代码适合您。

void Button1Clicked(object sender, EventArgs e)
{
    disableAllButtons();

    // ... do something first ...
    page1.IsVisible = true;
    Console.WriteLine("Button 1 Clicked!");
}

void Button2Clicked(object sender, EventArgs e)
{
    disableAllButtons();

    // ... do something first ...
    page2.IsVisible = true;
    Console.WriteLine("Button 2 Clicked!");
}

void Button3Clicked(object sender, EventArgs e)
{
    disableAllButtons();

    // ... do something first ...
    page3.IsVisible = true;
    Console.WriteLine("Button 3 Clicked!");
}

void Back1Clicked(object sender, EventArgs e)
{
    enableAllButtons();
}

void Back2Clicked(object sender, EventArgs e)
{
    enableAllButtons();
}

void Back3Clicked(object sender, EventArgs e)
{
    enableAllButtons();
}



void disableAllButtons()
{
    button1.IsEnabled = false;
    button2.IsEnabled = false;
    button3.IsEnabled = false;

    disableAllPages();
}

void enableAllButtons()
{
    button1.IsEnabled = true;
    button2.IsEnabled = true;
    button3.IsEnabled = true;

    disableAllPages();
}

void disableAllPages()
{
    page1.IsVisible = false;
    page2.IsVisible = false;
    page3.IsVisible = false;
}

答案 2 :(得分:4)

我建议使用MVVM方法,并将每个按钮的IsEnabled属性绑定到视图模型中的同一属性,例如AreButtonsEnabled

<强> MyViewModel.cs

private bool _areButtonsEnabled = true;

public bool AreButtonsEnabled
{
    get => _areButtonsEnabled;
    set
    {
        if (_areButtonsEnabled != value)
        {
            _areButtonsEnabled = value;
            OnPropertyChanged(nameof(AreButtonsEnabled)); // assuming your view model implements INotifyPropertyChanged
        }
    }
}

MyPage.xaml (仅显示一个按钮的代码):

...
<Button 
    Text="Back" 
    BackgroundColor="White" 
    Clicked="HandleButton1Clicked"
    AbsoluteLayout.LayoutFlags="All"
    AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"
    IsEnabled={Binding AreButtonsEnabled} />
...

然后,对于每个按钮的事件处理程序,您可以将AreButtonsEnabled属性设置为false以禁用所有按钮。请注意,您应首先检查AreButtonsEnabled的值是否为真,因为用户可能会在调用PropertyChanged事件之前单击两次。但是,因为按钮的click事件处理程序在主线程上运行,所以AreButtonsEnabled的值将在调用下一个HandleButtonXClicked之前设置为false。换句话说,即使UI尚未更新,AreButtonsEnabled的值也会更新。

<强> MyPage.xaml.cs

HandleButton1Clicked(object sender, EventArgs e)
{
    if (viewModel.AreButtonsEnabled)
    {
        viewModel.AreButtonsEnabled = false;
        // button 1 code...
    }
}

HandleButton2Clicked(object sender, EventArgs e)
{
    if (viewModel.AreButtonsEnabled)
    {
        viewModel.AreButtonsEnabled = false;
        // button 2 code...
    }
}

HandleButton3Clicked(object sender, EventArgs e)
{
    if (viewModel.AreButtonsEnabled)
    {
        viewModel.AreButtonsEnabled = false;
        // button 3 code...
    }
}

然后在您想要重新启用按钮时调用viewModel.AreButtonsEnabled = true;

如果你想要一个“真正的”MVVM模式,你可以将命令绑定到按钮而不是收听他们的Clicked事件。

<强> MyViewModel.cs

private bool _areButtonsEnabled = true;

public bool AreButtonsEnabled
{
    get => _areButtonsEnabled;
    set
    {
        if (_areButtonsEnabled != value)
        {
            _areButtonsEnabled = value;
            OnPropertyChanged(nameof(AreButtonsEnabled)); // assuming your view model implements INotifyPropertyChanged
        }
    }
}

public ICommand Button1Command { get; protected set; }

public MyViewModel()
{
    Button1Command = new Command(HandleButton1Tapped);
}

private void HandleButton1Tapped()
{
    // Run on the main thread, to make sure that it is getting/setting the proper value for AreButtonsEnabled
    // And note that calls to Device.BeginInvokeOnMainThread are queued, therefore
    // you can be assured that AreButtonsEnabled will be set to false by one button's command
    // before the value of AreButtonsEnabled is checked by another button's command.
    // (Assuming you don't change the value of AreButtonsEnabled on another thread)
    Device.BeginInvokeOnMainThread(() => 
    {
        if (AreButtonsEnabled)
        {
            AreButtonsEnabled = false;
            // button 1 code...
        }
    });
}

// don't forget to add Commands for Button 2 and 3

MyPage.xaml (仅显示一个按钮):

<Button 
    Text="Back" 
    BackgroundColor="White"
    AbsoluteLayout.LayoutFlags="All"
    AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"
    Command={Binding Button1Command}
    IsEnabled={Binding AreButtonsEnabled} />

现在您无需在MyPage.xaml.cs代码隐藏文件中添加任何代码。

答案 3 :(得分:4)

共享状态就是您所需要的。最少侵入性和最通用的方法就是这样 - 只需包装你的代码:

    private bool isClicked;
    private void AllowOne(Action a)
    {
        if (!isClicked)
        {
            try
            {
                isClicked = true;
                a();
            }
            finally
            {
                isClicked = false;
            }               
        }
    }

    void Button1Clicked(object sender, EventArgs e)
    {
        AllowOne(() =>
        {
            // ... do something first ...
            page1.IsVisible = true;
            Console.WriteLine("Button 1 Clicked!");
        });
    }

try..finally模式对于保证安全非常重要。如果a()抛出异常,则状态不会受到损害。

答案 4 :(得分:1)

我的基本视图模型中有这个

public bool IsBusy { get; set; }

protected async Task RunIsBusyTaskAsync(Func<Task> awaitableTask)
{
    if (IsBusy)
    {
        // prevent accidental double-tap calls
        return;
    }
    IsBusy = true;
    try
    {
        await awaitableTask();
    }
    finally
    {
        IsBusy = false;
    }
}

然后,命令委托如下所示:

    private async Task LoginAsync()
    {
        await RunIsBusyTaskAsync(Login);
    }

...或者如果您有参数:

    private async Task LoginAsync()
    {
        await RunIsBusyTaskAsync(async () => await LoginAsync(Username, Password));
    }

登录方法将包含您的实际逻辑

    private async Task Login()
    {
        var result = await _authenticationService.AuthenticateAsync(Username, Password);

        ...
    }

另外,您可以使用内联委托:

    private async Task LoginAsync()
    {
        await RunIsBusyTaskAsync(async () =>
        {
            // logic here
        });
    }

不需要IsEnabled设置。如果您要执行的实际逻辑不是异步的,则可以用Action替换Func。

您还可以将IsBusy属性绑定到诸如ActivityIndi​​cator之类的东西

答案 5 :(得分:0)

您可以通过Command进行操作:

    bool CanNavigate = true;

    public ICommand OpenPageCommand
    {
        get
        {
            return new Command<View>(async (v) => await 
             GotoPage2ndPage(v), (v) => CanNavigate);
        }
    }


    private async Task GotoPage2ndPage(View view)
    {
        CanNavigate = false;

        // Logic for goto 2nd page

        CanNavigate = true;
    }


    Note : Where v indicates CommandParameter.