여러 번 클릭을 한번 클릭한 것처럼 동작하게 하는 방법

<VerticalStackLayout>
	<VerticalStackLayout.GestureRecognizers>
		<TapGestureRecognizer Command="{Binding ABCCommand}" />
	</VerticalStackLayout.GestureRecognizers>
</VerticalStackLayout>

Button을 사용 할 때는 이런 문제가 없었던 것 같습니다.
Tap Gesture를 사용하니 이러한 경우에서 문제가 발생합니다.
Tap Gesture의 경우 탭을 여러번 할 경우 전부 커맨드를 통해 들어오게 됩니다.
짧은 시간 안에 많은 입력이 들어와서 앱이 죽는 경우가 생깁니다.

타이머를 이용하여 일정 시간동안 들어오는 입력을 처리해서 한번 만 동작하게 한다던가,
Event를 활용하여 구독해지를 통해 여러번 입력이 들어와도 한번만 동작하게하는 방법 등등…
여러가지 방법이 있는 것 같습니다.
이러한 디자인 패턴 이름이 있는 것 같은데;;; 검색 키워드를 잘 모르는 상황입니다.
이러한 문제를 해결하기위해 주로 사용하는 방법은 무엇인가요?


Tap Gesture를 사용하고 짧은 시간 안에 입력이 여러번 들어오면 문제가 생긴다.
여러 번 들어오는 입력을 하나의 입력으로 처리하고 싶습니다.

2개의 좋아요

먼저 재현하기 전에 궁금한점 물어봅니다.

왜 짧은 시간안에 여러번 커맨드가 호출될 때 문제가 생기나요?

2개의 좋아요

@dimohy 재현 가능한 샘플을 제가 제공 해드려야하는건데… 죄송합니다.
(Shell Navigation 페이지에서 Modal 페이지를 열고 await Navigation.PushAsync(new DetailsPage());)
Modal 페이지에서 tap을하면 pop되서 닫히는 구조입니다. (await Navigation.PopAsync();)
이 때 tap이 여러번 들어오면 에러가 발생합니다. ( pop할 stack이 없다라는 느낌이였습니다.)
이런 에러는 Stack이 있을 때 pop해라 라고 해도 되긴 합니다…

TapGesture()
{
   await Navigation.PopAsync();
}
2개의 좋아요

State 패턴을 참고하시면 도움이 될 수도 있으실 거 같아요… 객체가 상태체크하지 않고 상태 자체를 객체화 시켜서 알아서 행동할 수 있게 하는 방법은 어떠신가요…

3개의 좋아요

가장 쉽게 접근하는 방법은 CommunityToolkit MVVM을 이용하는 것입니다.

MAUI 템플릿 프로젝트로 시작 한 후 MainPage.xaml을 다음 처럼 변경합니다. (코드도 공유 드릴테니 그것으로 동작성을 확인하세요.)

        <VerticalStackLayout
            Padding="30,0"
            Spacing="25"
            VerticalOptions="Center">
            <VerticalStackLayout.GestureRecognizers>
                <TapGestureRecognizer Command="{Binding GestureCommand}" />
            </VerticalStackLayout.GestureRecognizers>
...

그런 후 유사한 상황을 재현하기 위해 MainPage.xaml.cs를 다음처럼 구현합니다.

        public MainPage()
        {
            InitializeComponent();

            BindingContext = this;
        }
...

        [RelayCommand]
        private async void OnGesture()
        {
            if (cts is null)
                cts = new CancellationTokenSource();
            else
                cts.Cancel();

            await Task.Delay(1000, cts.Token);

            OnCounterClicked(this, EventArgs.Empty);

            cts.Dispose();
            cts = null;
        }

이 코드는 1초 안에 커맨드가 2회 이상 호출될 경우 취소 예외가 발생하도록 의도한 것입니다.

실행하고 영역을 빠르게 두번 누르면 다음 처럼 예외가 발생합니다.

image

이 문제를 CommunityToolkit MVVM를 이용해서 수정하는 것은 매우 간단한 편입니다. 위의 코드를 아래처럼 변경합니다.

        [RelayCommand]
        private async Task OnGestureAsync()
        {
            if (cts is null)
                cts = new CancellationTokenSource();
            else
                cts.Cancel();

            await Task.Delay(1000, cts.Token);

            OnCounterClicked(this, EventArgs.Empty);

            cts.Dispose();
            cts = null;
        }

다시 실행하면 이제 이 명령어는 처리가 완료될 때까지 호출되지 않게 됩니다. 이는 RelayCommand의 AllowConcurrentExecutions 속성에 의한 것으로 기본 값은 false입니다. 이를 true로 변경하면 예외가 발생하게 됩니다.

결국에는 비동기 처리가 완료되기전에 다시 커맨드를 실행할 것인가의 일반 문제 인 것 같네요.


3개의 좋아요

재현 소스가 없어서 정확히 어떤 문젠지는 모르겠지만 중복페이지 문제라면 아래와같이 처리해보세요

MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiApp3.MainPage"
             BindingContext="{Binding Source={RelativeSource Mode=Self}}">

    <Grid Padding="30,0"
          VerticalOptions="Fill"
          Background="{StaticResource Gray100}">
        <Grid.GestureRecognizers>
            <TapGestureRecognizer Command="{Binding PagePushCommand}"/>
        </Grid.GestureRecognizers>

        <Label Text="Main Page!"
               VerticalOptions="Center" 
               HorizontalOptions="Center" 
               TextColor="#0000ff"/>
    </Grid>

</ContentPage>

MainPage.cs

using CommunityToolkit.Mvvm.Input;

namespace MauiApp3;

public partial class MainPage : ContentPage
{
	public MainPage()
	{
		InitializeComponent();
	}

	[RelayCommand]
	private async void PagePush()
	{
		if (Shell.Current.CurrentPage.GetType() != typeof(DetailPage))
		{
            await Navigation.PushAsync(new DetailPage());
        }
    }
}

DetailPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiApp3.DetailPage"
             Title="DetailPage"
             BindingContext="{Binding Source={RelativeSource Mode=Self}}">
    
    <Grid Padding="30,0"
          VerticalOptions="Fill"
          Background="{StaticResource Gray100}">
        <Grid.GestureRecognizers>
            <TapGestureRecognizer Command="{Binding PagePopCommand}"/>
        </Grid.GestureRecognizers>

        <Label Text="Detail Page!"
               VerticalOptions="Center" 
               HorizontalOptions="Center" 
               TextColor="#0000ff"/>
    </Grid>
</ContentPage>

DetailPage.cs

using CommunityToolkit.Mvvm.Input;

namespace MauiApp3;

public partial class DetailPage : ContentPage
{
	public DetailPage()
	{
		InitializeComponent();
	}

    [RelayCommand]
    private async void PagePop()
    {
        await Navigation.PopAsync();
    }
}

페이지가 중복처리 안되게 막는 소스 입니다.
핵심 처리 부분…
if (Shell.Current.CurrentPage.GetType() != typeof(DetailPage))

2개의 좋아요

Xamarin.Form 뿐만 아니라 WInform, WPF, Blazor, Javascript에서도 마찬가지로 사용자가 버튼을 여러 번 누를 수 있기에 저는 다음 방법 중 하나를 사용합니다.

  1. MVVM을 사용할 경우에는 ViewModel에 IsBusy라는 상태 속성을 추가하고 버튼 클릭 커맨드 실행 시 IsBusy를 true로 지정하고 기능이 다 실행된 후 IsBusy를 false로 바꿉니다.
    버튼쪽에서는 IsBusy를 Enabled 속성에 바인딩해놓으면 버튼 커맨드가 끝나기 전 까지는 사용자가 버튼을 연속적으로 누를 수 없습니다.

  2. MVVM을 사용하지 않고 CodeBehind로 할 경우에는 버튼 클릭 이벤트(Full MVVM이 아닌 Command만 사용하는 경우도 마찬가지)에서 버튼의 Enabled 속성을 false로 했다가 다 끝나고 True로 바꿉니다.

  3. 각 버튼 클릭 이벤트 또는 커맨드마다 이렇게 하기 싫으면 해당 화면 최상단 컨테이너의 Enabled 속성에 IsBusy를 바인딩해놓으면 한 화면의 모든 버튼마다 IsBusy를 바인당하지 않아도 되고 어차피 사용자가 버튼을 누를 때 다른 입력도 하면 안되는 경우가 대부분이므로 이런 방법이 더 좋을 수 있습니다.

  4. 버튼 클릭 이벤트에서 시간이 좀 걸리는 작업을 하는 경우에 화면 전체가 Disable되면 보기 좋지 않을 수 있는데, 이런 경우에는 화면 전체를 가리는 Full Loading Splash 같은 것을 올려서 IsBusy=true일 때 Full Loading Splash를 보여주고, IsBusy=false일 때 숨겨주면 됩니다. 예전 WPF/Silverlight 시절에 BusyIndicator라는 컨트롤이 이런 기능을 제공해서 잘 써먹었었네요.

3개의 좋아요