mauipickerselectedindex

.net MAUI Picker SelectedIndex does not cause item to display


I cannot get the expected behaviour of SelectedIndex to work. The Item is not shown. The ItemSource, ItemDisplayBinding and SelectedItem are working when the picker is selected, But when the view is first displayed the Pickers are not showing the objects from the List that they are bound to.

I have created a test .Maui APP as follows;

The View MainPage.xml:

<?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:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
            xmlns:models="clr-namespace:MauiPicker;assembly=MauiPicker"
            xmlns:viewModels="clr-namespace:MauiPicker"
             x:Class="MauiPicker.MainPage"
             x:DataType="viewModels:MainViewModel">

    <Grid
        ColumnDefinitions="*"
        RowDefinitions="*,*">
        <CollectionView
            Grid.Row="0"
            Grid.Column="0"
            ItemsSource="{Binding PartAResultLists}"
            SelectionMode="None">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="models:PartAResultList">
                        <Grid Padding="5">
                            <Border>
                                <Grid Padding="10"
                                    ColumnDefinitions="Auto,*"
                                    RowDefinitions="Auto"
                                    RowSpacing="7">
                                    <Label Text="Outlet Type:" 
                                            Grid.Column="0" Grid.Row="0"
                                            HorizontalOptions="End"
                                            VerticalOptions="Center"
                                            Margin="0,0,0,0"/>
                                    <Border
                                        Grid.Column="1"
                                        Grid.Row="0"
                                        Grid.ColumnSpan="2">
                                        <Picker
                                            Title="Select an Outlet"
                                            ItemsSource="{Binding Source={RelativeSource AncestorType={x:Type viewModels:MainViewModel}},                                   Path=Outlets}"
                                            ItemDisplayBinding="{Binding Name}"
                                            SelectedIndex="{Binding OutletIndex}"
                                            SelectedItem="{Binding OutletName}">
                                        </Picker>
                                    </Border>
                                </Grid>
                            </Border>
                        </Grid>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
        <Button
            Grid.Row="1"
            Grid.Column="0"
            Text="Reload List"
            HorizontalOptions="Center"
            VerticalOptions="Center"
            Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:MainViewModel}}, Path=LoadResultsCommand}">
        </Button>
    </Grid>

</ContentPage>

The code behind MainPage.xml.cs

        namespace MauiPicker;

public partial class MainPage : ContentPage
{
    public MainPage(MainViewModel vm)
    {
        InitializeComponent();
        BindingContext = vm;
    }
}

The ViewModel MainViewModel.cs

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MvvmHelpers;

namespace MauiPicker
{
    public partial class MainViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
    {

        public MainViewModel()
        {
            LoadResults();
        }

        [RelayCommand]
        async Task LoadResults()
        {

            Outlets = new ObservableRangeCollection<Outlet>
            {
                new Outlet(){Name="Outlet0"},
                new Outlet(){Name="Outlet1"},
                new Outlet(){Name="Outlet2"},

            };


            PartAResultLists = new ObservableRangeCollection<PartAResultList>
            {
                new PartAResultList(){OutletIndex = 0, OutletName= new Outlet(){Name="Outlet0" } },
                new PartAResultList(){OutletIndex=1, OutletName= new Outlet(){Name="Outlet1" }},
                new PartAResultList(){OutletIndex = 2, OutletName= new Outlet(){Name="Outlet2" }},
                new PartAResultList(){OutletIndex = 0, OutletName= new Outlet(){Name="Outlet0" }},
                new PartAResultList(){OutletIndex = 2, OutletName= new Outlet(){Name="Outlet2" }}
            };
        }


        [ObservableProperty]
        ObservableRangeCollection<Outlet> outlets;

        [ObservableProperty]
        ObservableRangeCollection<PartAResultList> partAResultLists;

    }
}

The models;

using CommunityToolkit.Mvvm.ComponentModel;

namespace MauiPicker
{
    public partial class Outlet : ObservableObject
    {
        [ObservableProperty]
        public string name;
    }
}
using CommunityToolkit.Mvvm.ComponentModel;

namespace MauiPicker
{
    public partial class PartAResultList : ObservableObject
    {

        [ObservableProperty]
        public Outlet outletName;

        [ObservableProperty]
        public int outletIndex;

    }
}

MauiProgram.cs

using CommunityToolkit.Maui;
using Microsoft.Extensions.Logging;

namespace MauiPicker;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiCommunityToolkit()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        builder.Services.AddSingleton<MainPage>();
        builder.Services.AddSingleton<MainViewModel>();

#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

Solution

  • First I want to point out, for anyone who is reading this, that the code in the question is bound correctly.

    Second, I want to say that new Outlet(Name="1") and new Outlet(Name="1") are not equal. Just because some property has the same value, does not mean that the objects will be the same. They are saved at two different places, and they are not the same. Not unless you make sure that only "Name" matters, when checking if they are equal.

    Third, If you check the community toolkit MVVM autogenerated property code, you will see something like:

    if(!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(name, value))
    

    And calling OnChanged in that IF block. This is optimization to help only waste resources, when something has to be re-rendered.

    This is why, when you set:

    new PartAResultList(){OutletIndex = 0...
    

    And then if you call, for example:

    PartAResultLists[0].OutletIndex = 0;
    

    The picker will remain blank. But if you call this:

    PartAResultLists[0].OutletIndex = 1;
    

    Surprisingly it will change. (Again, giving example with the index, because the object will not work for the reasons I pointed out earlier).

    There is no need to bind to Object and to Index at the same time. In your situation this can only lead to more bugs.

    Your bindings are setup well. You have to fix your code around them here and there.

    Edit: You do not have to actually change the value. Just trying to explain the problem that OnPropertyChanged("OutletIndex") is not called.

    Theoretical example: Lets say that in the:

    public partial class PartAResultList : ObservableObject
    

    We place some method:

    NotifyWorkaround(){
          OnPropertyChanged("OutletIndex");
    }
    

    We can now call PartAResultList[n].NotifyWorkaround(), for every n-th element of the list. And the interface will respond to the change.

    I have never used this in production however. It worked for your example (I added 10 elements), but I have no idea what impact it will have on 1000, 10k, or more elements in the list.