Search code examples
c#.netxamlmaui

.NET Maui How to invoke method in VM from referenced view


.NET Maui supports referencing one XAML file in another. For example, multiple <ContentPage> that display a list of related items (e.g, different types of animals) can reference a single <ContentView>that contains the elements needed to manage the list or other content. Likewise, the page VM can use some means (inheritance or library) to consolidate code that is used to manage operation of the list. This all helps with DRY.

But if the <ContentVIew> includes a <CollectionView> that uses RemainingItemsThresholdReachedCommand="{Binding GetItemsCommand}", how can it reference that [RelayCommand] from the VM of the page? I can't put GetItems() into the base VM because each is getting a specific animal type (Monkey or 'Tiger') not just Animal. How do I have that common XAML invoke a command method in the VM of the original page that references the view containing the <CollectionView> since it doesn't know about that page's VM?

Partial sample code follows...

MonkeyListPage.xaml:

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:v="clr-namespace:Sample.Views"
    x:DataType="vm:MonkeyListVM"
    ...
    <v:ListView BindingContext="{Binding .}"/>
</ContentPage>

TigerListPage.xaml:

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:v="clr-namespace:Sample.Views"
    x:DataType="vm:TigerListVM"
    ...
    <v:ListView BindingContext="{Binding .}"/>
</ContentPage>

ListView.xaml:

<ContentView
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:m="clr-namespace:Sample.Models"
    x:DataType="vm:ListVM"
    ...
    <CollectionView ItemsSource="{Binding Items}"
            SelectionMode="None"
            RemainingItemsThreshold="{Binding ItemsRefreshThreshold}"
            RemainingItemsThresholdReachedCommand="{Binding GetItemsCommand}">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="m:Animal">
                <VerticalStackLayout>
                    <Image Source="{Binding MainImage}" Aspect="AspectFill" />
                    <Label Text="{Binding Title}" Style="{StaticResource LabelLargeBold}" />
                    <Label Text="{Binding Description}" Style="{StaticResource LabelMedium}" />
                </VerticalStackLayout>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</ContentView>

MonkeyListVM.cs:

public partial class MonkeyListVM : ListVM
{
    public MonkeyListVM(IConnectivity connectivity, IGeolocation geolocation) : base(connectivity, geolocation)
    {
        BaseTitle = "Monkeys";
    }

    ...

    [RelayCommand]
    public async Task GetTimes()
    {
        if (IsBusy)
            return;

        try
        {
            if (connectivity.NetworkAccess != NetworkAccess.Internet)
            {
                await Shell.Current.DisplayAlert("No connectivity!",
                    $"Please check internet and try again.", "OK");
                return;
            }

            IsBusy = true;

            ResultMonkeys result = await DataService.GetItemsAsync<ResultMonkeys>(ResultMonkey.Term, Items.Count, LimitItems);
            foreach (Monkey item in result.Data)
            {
                Items.Add(item);
            }
            TotalItems = result.Total;
        }
        catch (Exception ex)
        {
            await Shell.Current.DisplayAlert("Error!", $"GetItems failed: {ex.Message}", "OK");
        }
        finally
        {
            IsBusy = false;
        }
    }

    ...
}

TigerListVM.cs:

public partial class TigerListVM : ListVM
{
    public TigerListVM(IConnectivity connectivity, IGeolocation geolocation) : base(connectivity, geolocation)
    {
        BaseTitle = "Tigers";
    }

    ...

    [RelayCommand]
    public async Task GetTimes()
    {
        if (IsBusy)
            return;

        try
        {
            if (connectivity.NetworkAccess != NetworkAccess.Internet)
            {
                await Shell.Current.DisplayAlert("No connectivity!",
                    $"Please check internet and try again.", "OK");
                return;
            }

            IsBusy = true;

            ResultTigers result = await DataService.GetItemsAsync<ResultTigers>(ResultTigers.Term, Items.Count, LimitItems);
            foreach (Tiger item in result.Data)
            {
                Items.Add(item);
            }
            TotalItems = result.Total;
        }
        catch (Exception ex)
        {
            await Shell.Current.DisplayAlert("Error!", $"GetItems failed: {ex.Message}", "OK");
        }
        finally
        {
            IsBusy = false;
        }
    }

    ...

}

ListVM.cs:

public partial class ListVM : BaseVM
{

    [ObservableProperty] ObservableCollection<Animal> items = new();
    [ObservableProperty] int itemsRefreshThreshold = -1;

    public ListVM(IConnectivity connectivity, IGeolocation geolocation)
    {
        IsBusy = false;
        this.geolocation = geolocation;
        this.connectivity = connectivity;
    }

    ...
}

Solution

  • remove x:DataType="vm:ListVM" from the XAML and you should be able to use the command binding.

    Alternately you could try defining the command as an abstract method on the base VM, or define an Interface for the VM and use that for the datatype