Handling Multiple Taps in Xamarin.Forms on Android

Sometimes in this beautiful world of cross-platform mobile development, we come across a problem that makes no sense at first. But after some StackOverflow surfing and Xamarin/MSDN research, we may find a way to tackle that problem. In some scenarios, there are multiple solutions and we have to decide which approach works best given our situation. In this post, I will demonstrate  one of these problems, rather scenarios. The multi-tap monstrosity on Android when using Xamarin.Forms.

Problem

On Android (Xamarin.Forms), if you are quick and tap on a button multiple times, it fires off multiple events causing a page to load twice, or popup to display twice. As you can imagine, this can be a big problem. For example, you have a button that takes you from PageA to PageB. If you double tap the button, it will push two PageB’s on the navigation stack. Imagine the confusion and bad user experience, we don’t what that to happen.

One Solution (Example)

There has been a lot of discussion on the web on what’s the best way to handle this scenario, there are also several good solutions that may work in an enterprise application. I would like to demonstrate the solution that worked for me quite nicely.

In our example, I will be demonstrating how to handle this when using MVVM pattern (as we should be using for any Xamarin.Forms app). I like to integrate things in to apps architectural plumbing so that my developers won’t have to worry about it when consuming.

Application Structure

In our example, we are developing a Xamarin.Forms application using PCL option following MVVM pattern. Our app has two pages, each page has a corresponding ViewModel. We also have a BaseViewModel that will do all the magic for us.

Locking/Unlocking Navigation

In order to lock and unlock the navigation, we will observe a boolean property. Let’s call it CanNavigate. Now, you might be thinking, why use a separate property when you can simply use the IsBusy property to notify that the application is busy and to lock navigation or multiple command executions if need be. We’ll get to it later.

ViewModels

BaseViewModel.cs

Let’s start with our BaseViewModel. Our BaseViewModel will implement INotifyPropertyChanged and have some properties, a Title property to set property for each page, CurrentPage property to use the Page navigation, an IsBusy property that will be used to notify when the application is busy and a CanNavigate property that we will observe when executing a command that has navigation.

public class BaseViewModel : INotifyPropertyChanged
{
#region Properties

string _title;

public string Title {
get { return _title; }
set
{
_title = value;
OnPropertyChanged ();
}
}

bool _isBusy;
public bool IsBusy
{
get { return _isBusy; }
set
{
_isBusy = value;
OnPropertyChanged();
}
}

bool _canNavigate = true;
public bool CanNavigate
{
get { return _canNavigate; }
set
{
_canNavigate = value;
OnPropertyChanged();
}
}

Page _currentPage;
public Page CurrentPage
{
get { return _currentPage; }
set
{
_currentPage = value;
OnPropertyChanged();
}
}

#endregion

#region INotifyPropertyChanged

public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

#endregion
}

The BaseViewModel is ready now. Here’s how the other ViewModels look:

PageAViewModel.cs

public class PageAViewModel : BaseViewModel
{
public PageAViewModel(Page page)
{
Title = "Page A";
CurrentPage = page;
}

#region Commands

ICommand _showAlertCommand;
public ICommand ShowAlertCommand
{
get {
if (_showAlertCommand == null)
_showAlertCommand = new Command (OnShowAlert,(x)=> CanNavigate);

return _showAlertCommand;
}
}

async void OnShowAlert (object obj)
{
CanNavigate = false;
await CurrentPage.DisplayAlert ("Hello", "From intelliAbb", "OK");
CanNavigate = true;
}

ICommand _goToPageBCommand;
public ICommand GoToPageBCommand
{
get
{
if (_goToPageBCommand == null)
_goToPageBCommand = new Command(OnGotoPageB,(x)=> CanNavigate);

return _goToPageBCommand;
}
}

async void OnGotoPageB(object obj)
{
CanNavigate = false;
await NavigateToPage(new PageB());
CanNavigate = true;
}

async Task NavigateToPage(Page page)
{
await CurrentPage.Navigation.PushAsync(page);
}
#endregion
}

Views

Our views are very simple. PageA has two buttons, one to show an alert and another to navigate to PageB.

PageA.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="AndroidMultitap.Views.PageA" Title="{Binding Title}" IsBusy="{Binding IsBusy}">

<StackLayout VerticalOptions="Center">
<Button Text="Show Alert" Command="{Binding ShowAlertCommand}" VerticalOptions="Center" />
<Button Text="Go To Page B" Command="{Binding GoToPageBCommand}" VerticalOptions="Center" />
</StackLayout>

</ContentPage>

At this point, we are done. By toggling and observing the CanNavigate property for your commands, we can avoid multiple pages being loaded or alerts being shown when a command bound items is tapped multiple times.

Better Approach

Even though, the above solution will work, it’s not very elegant. We should try to “black-box” our code as much and as cleanly as possible. It not only helps with maintenance, but also helps in getting new developers on board and not to mention, can help with reducing technical debt.

So, in effort to clean up the solution above, let’s take a page from Xamarin’s Sports app. I like the idea of a TaskRunner which works as a proxy to run tasks through and handle any exception. We can get creative with it and ask it to pass the exceptions back to the ViewModel, lock/unlock navigation, etc.

In our case, let’s move the toggling of IsBusy and CanNavigate to the BaseViewModel. Using a RunTask() method, we can do so quite simply.

protected async Task RunTask(Task task, bool handleException = true, bool lockNavigation = true, CancellationTokenSource token = default(CancellationTokenSource), [CallerMemberName] string caller = "")
{
if (token != null && token.IsCancellationRequested)
return;

Exception exception = null;

try
{
UpdateIsBusy (true, lockNavigation);

await task;

UpdateIsBusy (false);

} catch (TaskCanceledException) {
Debug.WriteLine ($"{caller} Task Cancelled");
} catch (AggregateException e) {
var ex = e.InnerException;
while (ex.InnerException != null)
ex = ex.InnerException;

exception = ex;
} catch (Exception ex) {
exception = ex;
}

if(exception != null)
{
if (handleException)
await CurrentPage.DisplayAlert("Error", (exception.InnerException??exception).Message, "OK");
else
throw exception;

UpdateIsBusy (false);
}
}

void UpdateIsBusy(bool isBusy, bool lockNavigation = true)
{
IsBusy = isBusy;
CanNavigate = !lockNavigation;
}

Let’s break down this particular RunTask() setup,

  • task: The task to run.
  • handleException: If true, use the logic in BaseViewModel to handle the exception. Otherwise, re-throw the exception to be bubbled-up.
  • lockNavigation: For long running tasks that should NOT block other “observing” commands from executing, set this to false. This way, you can still show a loading UI bound to IsBusy without disabling other buttons from tapping. (Like I mentioned in ‘Locking/Unlocking’ section above.)
  • token: self-explanatory
  • caller: The calling method name.

Now, we can run our tasks through RunTask() and control what to do in case of an exception, locking subsequent command, etc. I prefer this approach as it keeps my ViewModels lean and stops the infestation of try/catch blocks.

Wrap up

That’s it folks. You can take the simple approach to avoid multiple taps on Xamarin.Forms Android applications. Or you can centralize the handling of CanNavigate. Give it a try and let me know how it works out for you. Or if you have a better solution, keep us posted in the comments.

Enjoy.

Resources

Source on Github: https://github.com/hnabbasi/androidmultitap

Xamarin’s Sports app: https://github.com/xamarin/Sport

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.