Home > Software design >  MAUI Button staying pressed after awaiting Shell.Current.GoToAsync()
MAUI Button staying pressed after awaiting Shell.Current.GoToAsync()

Time:01-13

In my MAUI app I am navigating between two pages, the main page and add page.

I am using await Shell.Current.GoToAsync() in a Command on the ViewModel that the Button calls. However, when I click the Button, it stays grayed out. It navigates to the correct page, but when I return to the page it is still grayed out. If I right click the Button it fixes this and is no longer grayed out. If I take the await away or make it not async both fix the issue too.

MainPage before button press AddPage before return button press Main page after button press and returned from add page

<?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"
             xmlns:viewmodel="clr-namespace:ButtonTest"
             x:DataType="viewmodel:MainPageViewModel"
             x:Class="ButtonTest.MainPage">

    <ScrollView>
        <VerticalStackLayout
            Spacing="25"
            Padding="30,0"
            VerticalOptions="Center">

            <Image
                Source="dotnet_bot.png"
                SemanticProperties.Description="Cute dot net bot waving hi to you!"
                HeightRequest="200"
                HorizontalOptions="Center" />

            <Label
                Text="Hello, World!"
                SemanticProperties.HeadingLevel="Level1"
                FontSize="32"
                HorizontalOptions="Center" />

            <Label
                Text="Welcome to .NET Multi-platform App UI"
                SemanticProperties.HeadingLevel="Level2"
                SemanticProperties.Description="Welcome to dot net Multi platform App U I"
                FontSize="18"
                HorizontalOptions="Center" />

            <Button
                x:Name="CounterBtn"
                Text="AddPage"
                Command="{Binding GoAddPageCommand}"
                HorizontalOptions="Center" />

        </VerticalStackLayout>
    </ScrollView>

</ContentPage>
namespace ButtonTest;

public partial class MainPage : ContentPage
{
    int count = 0;

    public MainPage(MainPageViewModel vm)
    {
        InitializeComponent();
        BindingContext= vm;
    }

    private void OnCounterClicked(object sender, EventArgs e)
    {
        count  ;

        if (count == 1)
            CounterBtn.Text = $"Clicked {count} time";
        else
            CounterBtn.Text = $"Clicked {count} times";

        SemanticScreenReader.Announce(CounterBtn.Text);
    }
}
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ButtonTest
{
    public partial class MainPageViewModel : ObservableObject
    {
        [RelayCommand]
        public async Task GoAddPage()
        {
            await Shell.Current.GoToAsync("//AddPage");
        }
    } 
}
<?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"
             xmlns:viewmodel="clr-namespace:ButtonTest"
             x:DataType="viewmodel:AddPageViewModel"
             x:Class="ButtonTest.AddPage"
             Title="AddPage">
    <VerticalStackLayout>
        <Button
                x:Name="CounterBtn"
                Text="MainPage"
                Command="{Binding GoMainPageCommand}"
                HorizontalOptions="Center" />
    </VerticalStackLayout>
</ContentPage>
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ButtonTest
{
    public partial class AddPageViewModel : ObservableObject
    {
        [RelayCommand]
        public async Task GoMainPage()
        {
            await Shell.Current.GoToAsync("//MainPage");
        }
    }
}

I would think that after Button press it would return to the normal color. I tried removing async or not returning a Task and that works fine as expected. How do you deal with asynchronous Commands and Buttons?

CodePudding user response:

By default, an async RelayCommand will disable the button until the command's Task completes. The behaviour you've described here indicates that await Shell.Current.GoToAsync() does not complete, which is likely a bug. You can verify this by placing a breakpoint right after GoToAsync() and checking whether it is hit after the AddPage is shown.

CodePudding user response:

You can change the method to the codes below.

 public partial class MainPageViewModel : ObservableObject
    {
        [RelayCommand]
        public static void GoAddPage()
        {
            Shell.Current.GoToAsync("add");
        }
    }

Using the async method make the button must wait, so it can not finish the task until you turn it back to the main thread. So this is why the color can not be changed back. You can use the static method.

CodePudding user response:

When you use the [RelayCommand] attribute to auto-generate a Command for a method, it depends on the method's return type and signature what type of Command will be generated.

1. RelayCommand

When your method has a void return type, then a RelayCommand is generated for you under the hood:

[RelayCommand]
private void DoSomething()
{
    //e.g.
    Shell.Current.GoToAsync("//AddPage");
}

Here, a RelayCommand will be generated and since the method is void, its execution can't be awaited and it runs synchronously. Therefore, the Command returns immediately and the Button is enabled again right-away.

2. AsyncRelayCommand

When your method is of return type async Task on the other hand, it will create an AsyncRelayCommand under the hood:

[RelayCommand]
private async Task DoSomethingAsync()
{
    //e.g.
    await Shell.Current.GoToAsync("//MainPage");
}

There are a few key differences here:

  1. An AsyncRelayCommand awaits the execution of the Task
  2. While awaiting the execution, the IsRunning flag of the AsyncRelayCommand will be set to true
  3. Buttons in MAUI recognize AsyncRelayCommands and get disabled while the IsRunning flag is true, which is not the case for the synchronous RelayCommand

In this case, your Button stays disabled until the Shell.Current.GoToAsync(); call finishes execution. That may take a long time, depending on what's going on in your app.

Quick fix

Unless you need to specifically wait for the navigation call, you could just change the method signature to return void instead of a Task like in section 1 above. Note that this means your method won't be awaitable when called directly.

General remarks

I recommend not performing navigation directly inside ViewModels. Personally, I think it's better to hide navigation behind an interface or delegate it to a View's code behind. That way, the ViewModel stays clean and testable.

As to why the Button never returns to its active color, there may be a bug, although I've yet to encounter that. For me, it worked fine so far in my MAUI applications. However, I always delegate navigation to my Views or to a helper class outside of my ViewModels.

  • Related