Using Shell navigation in dotNET MAUI, what is the best (most reliable) way to ensure that page transitions are always animating and doing this as smooth as possible, across different devices?
I did find the simplest solution but I'm wondering if there is a better way to do it. The way I'm currently doing it does not seem all too reliable, especially across different (slower and faster) devices. Is this really the best / most acceptable way?
Update for accepted answer:
The accepted answer has made a huge difference for me! Many lists of objects could be loaded into the GlobalViewModel, throughout my entire application. I never knew I was making so many copies. This has sped up my entire application on every page!
The actual problem:
I noticed one of my pages having slightly more data to load. Although in my opinion still a pretty acceptable amount, the page transition doesn't show because loading data causes a lag.
My current solution:
I'm navigating to a page using Shell await Shell.Current.GoToAsync($"{nameof(PageName)}", true, Params);
and this is a very primitive/simple example of a destination page that I would have:
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"
xmlns:m="clr-namespace:MauiApp1.Models"
xmlns:tk="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:vm="clr-namespace:MauiApp1.ViewModels"
x:DataType="vm:PageNameViewModel">
<ContentPage.Behaviors>
<tk:EventToCommandBehavior Command="{Binding OnAppearingCommand}" EventName="Appearing"/>
</ContentPage.Behaviors>
<StackLayout>
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="m:Item">
<VerticalStackLayout>
<!-- Using item data here -->
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</ContentPage>
ViewModel:
I am basically delaying all the stuff in OnAppearing()
to animate first, before actually populating the page:
public class PageNameViewModel : BaseViewModel, IQueryAttributable
{
private List<Item> items { get; set; } = new();
public ObservableCollection<Item> Items { get; set; } = new();
public Command OnAppearingCommand { get; private set; }
public PageNameViewModel()
{
OnAppearingCommand = new Command(OnAppearing);
}
public void ApplyQueryAttributes(IDictionary<string, object> Params)
{
items = Params["Items"] as items;
}
private async void OnAppearing()
{
// Delay here to wait for page animation, before populating the page:
await Task.Delay(500);
foreach (var item in items)
{
Items.Add(item);
}
}
}
Please note that, in my case, I have to load the data inside the Appearing
event in my ViewModel because of the IQueryAttributable
interface. Properties are set from query parameters after the constructor but before the appearing event in which I populate the view. I am using MAUI Community Toolkit to convert the page appearing event into a command.
I think this may not be very reliable because on older and slower devices, the delay time might be way too short. On high end devices the delay time might even become obvious and annoying.
Is this an acceptable way of doing it?
It appears that the items are known before you move to the page. Why does the page need to have a copy of the items? Instead, let's refer to the original instance.
Let's assume we have a GlobalViewModel
singleton, and, in it, let's place the ObservableCollection
. The intention is that the ObservableCollection
is available for your entire application.
public GlobalViewModel
{
public ObservableCollection<Item> Items { get; } = new();
}
Note that for collections that you initialize either inline (as depicted above) or in the constructor, you only need to have get;
.
Register this as a singleton in your MauiProgram.cs
for dependency injection:
builder.Services.AddSingleton<GlobalViewModel>();
builder.Services.AddTransient<PageNameViewModel>();
Then for your PageNameViewModel
you can pass in the GlobalViewModel
singleton via constructor injection parameters:
public PageViewModel
{
public GlobalViewModel Global { get; }
public PageViewModel(GlobalViewModel Global)
{
this.Global = Global;
}
}
As mentioned previously, since the property is defined during the constructor, we only need to use get;
permissions for the property.
In your XAML we can access the collection via Global.Items
:
<CollectionView ItemsSource="{Binding Global.Items}">