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.
<?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:
- An
AsyncRelayCommand
awaits the execution of the Task - While awaiting the execution, the
IsRunning
flag of theAsyncRelayCommand
will be set totrue
- Buttons in MAUI recognize
AsyncRelayCommands
and get disabled while theIsRunning
flag istrue
, which is not the case for the synchronousRelayCommand
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.