1

I have a problem with the Picker control in .NET MAUI. On the update page, the picker is not showing the value of update model. Here is how is picker defined in the 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"
             x:Class="MauiUI.Pages.AddOrUpdatePlayer"
             xmlns:local="clr-namespace:Backend.Models;assembly=Backend.Models"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit">

    <ContentPage.BindingContext>
        <local:PlayerModel x:Name="ViewModel"/>
    </ContentPage.BindingContext>

    <ContentPage.ToolbarItems>
        <ToolbarItem IconImageSource="save.svg" Clicked="OnSaveClick" Command="{Binding ValidateCommand}">
        </ToolbarItem>
    </ContentPage.ToolbarItems>

    <ScrollView Margin="10">
        <VerticalStackLayout>
            <VerticalStackLayout>
                <Label Text="Name" />
                <Entry x:Name="name" Text="{Binding Name}"
                       ClearButtonVisibility="WhileEditing">
                    <Entry.Behaviors>
                        <toolkit:EventToCommandBehavior
                            EventName="TextChanged"
                            Command="{Binding [Name].HasError}" />
                    </Entry.Behaviors>
                </Entry>
                <Label x:Name="lblValidationErrorName" Text="{Binding [Name].Error}" TextColor="Red" />
            </VerticalStackLayout>
            <VerticalStackLayout Margin="0,10">
                <Label Text="Position" />
                <Picker x:Name="position" Title="Select..."
                        ItemDisplayBinding="{Binding Name}"
                        SelectedItem="{Binding Position}">
                </Picker>
                <Label x:Name="lblValidationErrorPosition" TextColor="red" Text="{Binding [Position].Error}"/>
            </VerticalStackLayout>
            <VerticalStackLayout Margin="0,10">
                <Label Text="Club" />
                <Entry x:Name="club" Text="{Binding Club}" 
                       ClearButtonVisibility="WhileEditing">
                    <Entry.Behaviors>
                        <toolkit:EventToCommandBehavior
                            EventName="TextChanged"
                            Command="{Binding [Club].HasError}" />
                    </Entry.Behaviors>
                </Entry>
                <Label x:Name="lblValidationErrorClub" TextColor="red" Text="{Binding [Club].Error}"/>
            </VerticalStackLayout>
            <VerticalStackLayout Margin="0,10">
                <Label Text="Birthday" />
                <DatePicker  x:Name="birthday" Date="{Binding Birthday}"/>
            </VerticalStackLayout>
            <VerticalStackLayout Margin="0,10">
                <Label Text="Birth place" />
                <Entry x:Name="birthplace" Text="{Binding BirthPlace}" 
                       ClearButtonVisibility="WhileEditing">
                    <Entry.Behaviors>
                        <toolkit:EventToCommandBehavior
                            EventName="TextChanged"
                            Command="{Binding [BirthPlace].HasError}" />
                    </Entry.Behaviors>
                </Entry>
                <Label x:Name="lblValidationErrorBirthPlace" TextColor="red" Text="{Binding [BirthPlace].Error}"/>
            </VerticalStackLayout>
            <VerticalStackLayout Margin="0,10">
                <Label Text="Weight" />
                <Entry x:Name="weight" Text="{Binding Weight}"
                       ClearButtonVisibility="WhileEditing" Keyboard="Numeric">
                    <Entry.Behaviors>
                        <toolkit:EventToCommandBehavior
                            EventName="TextChanged"
                            Command="{Binding [Weight].HasError}" />
                    </Entry.Behaviors>
                </Entry>
                <Label x:Name="lblValidationErrorWeight" TextColor="red" Text="{Binding [Weight].Error}" />
            </VerticalStackLayout>
            <VerticalStackLayout Margin="0,10">
                <Label Text="Height" />
                <Entry x:Name="height" Text="{Binding Height}" 
                       ClearButtonVisibility="WhileEditing" Keyboard="Numeric">
                    <Entry.Behaviors>
                        <toolkit:EventToCommandBehavior
                            EventName="TextChanged"
                            Command="{Binding [Height].HasError}" />
                    </Entry.Behaviors>
                </Entry>
                <Label x:Name="lblValidationErrorHeight" TextColor="red" Text="{Binding [Height].Error}" />
            </VerticalStackLayout>
            <VerticalStackLayout Margin="0,10">
                <Label Text="Image link" />
                <Entry x:Name="webImageLink" Text="{Binding WebImageLink}"
                       ClearButtonVisibility="WhileEditing">
                    <Entry.Behaviors>
                        <toolkit:EventToCommandBehavior
                            EventName="TextChanged"
                            Command="{Binding [WebImageLink].HasError}" />
                    </Entry.Behaviors>
                </Entry>
                <Label x:Name="lblValidationErrorWebImageLink" TextColor="red" Text="{Binding [WebImageLink].Error}"/>
            </VerticalStackLayout>
            <VerticalStackLayout Margin="0,10">
                <Label Text="Description" />
                <Editor x:Name="description" Text="{Binding Description}"
                        AutoSize="TextChanges">
                    <Editor.Behaviors>
                        <toolkit:EventToCommandBehavior
                            EventName="TextChanged"
                            Command="{Binding [Description].HasError}" />
                    </Editor.Behaviors>
                </Editor>
                <Label x:Name="lblValidationErrorDescription" TextColor="red" Text="{Binding [Description].Error}"/>
            </VerticalStackLayout>
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

The code behind:

[QueryProperty(nameof(Player), "player")]
public partial class AddOrUpdatePlayer : ContentPage
{
    private PlayerModel player;
    public PlayerModel Player
    {
        get => player;
        set
        {
            player = value;
            OnPropertyChanged("player");
        }
    }

    private readonly IMemoryCache memoryCache;
    private readonly IPlayerClient playerClient;


    private delegate Task Action();
    private Action asyncAction;


    public AddOrUpdatePlayer(IMemoryCache memoryCache, IPlayerClient playerClient)
    {
        this.memoryCache = memoryCache;
        this.playerClient = playerClient;

        InitializeComponent();
        SetUpPositionPicker();
    }

    protected override void OnAppearing()
    {
        player ??= new PlayerModel();
        player.ValidationCompleted += OnValidationHandler;

        BindingContext = player;

        SetUpControls();
        SetTitle();
        SetActionPointer();
    }

    private void SetUpControls()
    {
        birthday.MinimumDate = new DateTime(1900, 1, 1);
        birthday.MaximumDate = DateTime.Now.Date;

        memoryCache.TryGetValue(CacheKeys.Positions, out List<PositionModel> positions);
        var selectedPosition = positions.FirstOrDefault(x => x.Id == player?.Position?.Id);
        var index = positions.IndexOf(selectedPosition);
        position.SelectedIndex = index;
    }

    private void SetUpPositionPicker()
    {
        memoryCache.TryGetValue(CacheKeys.Positions, out List<PositionModel> positions);
        position.ItemsSource = positions;
    }

    private void SetTitle()
    {
        Title = this.player?.Id == 0 ?
                "Add new player" :
                $"Update {player?.Name}";
    }

    private void SetActionPointer()
    {
        asyncAction = this.player?.Id == 0 ?
                      AddNewPlayer :
                      UpdatePlayer;
    }

    private async Task AddNewPlayer()
    {
        var result = await playerClient.CreateAsync(player);

        if (!result)
            return;
    }

    private async Task UpdatePlayer()
    {
        var result = await playerClient.UpdateAsync(player);

        if (!result)
            return;
    }

    private async void OnSaveClick(object sender, EventArgs e)
    {
        if (player?.HasErrors ?? true)
            return;

        await asyncAction();
    }

    private void OnValidationHandler(Dictionary<string, string?> validationMessages)
    {
        if (validationMessages is null)
            return;

        lblValidationErrorName.Text = validationMessages.GetValueOrDefault("name");
        lblValidationErrorPosition.Text = validationMessages.GetValueOrDefault("positionid");
        lblValidationErrorClub.Text = validationMessages.GetValueOrDefault("club");
        lblValidationErrorWebImageLink.Text = validationMessages.GetValueOrDefault("webimagelink");
        lblValidationErrorBirthPlace.Text = validationMessages.GetValueOrDefault("birthplace");
        lblValidationErrorWeight.Text = validationMessages.GetValueOrDefault("weight");
        lblValidationErrorHeight.Text = validationMessages.GetValueOrDefault("height");
        lblValidationErrorDescription.Text = validationMessages.GetValueOrDefault("description");
    }
}

public partial class PlayerModel : BaseViewModel
{
    private int id;
    private string name;
    private string webImageLink;
    private string club;
    private string birthday;
    private string birthPlace;
    private int? weight;
    private double? height;
    private string description;
    private PositionModel position;
    
    public int Id
    {
        get => this.id;
        set => SetProperty(ref this.id, value, true);
    }

    [Required]
    [StringLength(255)]
    [MinLength(2)]
    public string Name
    {
        get => this.name;
        set
        {
            SetProperty(ref this.name, value, true);

            ClearErrors();
            SetProperty(ref this.name, value);
            ValidateAllProperties();
            OnPropertyChanged("ErrorDictionary[Name]");
        }
    }

    [Required]
    [StringLength(4096)]
    public string WebImageLink
    {
        get => this.webImageLink;
        set
        {
            SetProperty(ref this.webImageLink, value, true);

            ClearErrors();
            ValidateAllProperties();
            OnPropertyChanged("ErrorDictionary[WebImageLink]");
        }
    }

    [Required]
    [StringLength(255)]
    [MinLength(2)]
    public string Club
    {
        get => this.club;
        set
        {
            SetProperty(ref this.club, value, true);

            ClearErrors();
            ValidateAllProperties();
            OnPropertyChanged("ErrorDictionary[Club]");
        }
    }

    [Required]
    [StringLength(32)]
    public string Birthday
    {
        get => this.birthday;
        set
        {
            SetProperty(ref this.birthday, value, true);

            ClearErrors();
            ValidateAllProperties();
            OnPropertyChanged("ErrorDictionary[Birthday]");
        }
    }

    [Required]
    [StringLength(255)]
    public string BirthPlace
    {
        get => this.birthPlace;
        set
        {
            SetProperty(ref this.birthPlace, value, true);

            ClearErrors();
            ValidateAllProperties();
            OnPropertyChanged("ErrorDictionary[BirthPlace]");
        }
    }

    [Required]
    [Range(0, 100)]
    public int? Weight
    {
        get => this.weight;
        set
        {
            SetProperty(ref this.weight, value, true);

            ClearErrors();
            ValidateAllProperties();
            OnPropertyChanged("ErrorDictionary[Weight]");
        }
    }

    [Required]
    [Range(0, 2.5)]
    public double? Height
    {
        get => this.height;
        set
        {
            SetProperty(ref this.height, value, true);

            ClearErrors();
            ValidateAllProperties();
            OnPropertyChanged("ErrorDictionary[Height]");
        }
    }

    [Required]
    public string Description
    {
        get => this.description;
        set
        {
            SetProperty(ref this.description, value, true);

            ClearErrors();
            ValidateAllProperties();
            OnPropertyChanged("ErrorDictionary[Description]");
        }
    }

    [Required]
    public PositionModel Position
    {
        get => this.position;
        set
        {
            SetProperty(ref this.position, value, true);

            ClearErrors();
            ValidateAllProperties();
            OnPropertyChanged("ErrorDictionary[Name]");
        }
    }

    public PlayerModel() : base()
    {}

    public PlayerModel(int id, string name, string webImageLink, string club, string birthday, string birthPlace, int weight, double height, string description, string positionName, int positionId) : base()
    {
        Id = id;
        Name = name;
        WebImageLink = webImageLink;
        Club = club;
        Birthday = birthday;
        BirthPlace = birthPlace;
        Weight = weight;
        Height = height;
        Description = description;
        Position = new PositionModel(positionId, positionName);
    }

    public PlayerModel(int id, string name, string webImageLink, string club, string birthday, string birthPlace, int weight, double height, string description, PositionModel position) : base()
    {
        Id = id;
        Name = name;
        WebImageLink = webImageLink;
        Club = club;
        Birthday = birthday;
        BirthPlace = birthPlace;
        Weight = weight;
        Height = height;
        Description = description;
        Position = position;
    }

    public PlayerModel(PlayerEntity player)
    {
        Id = player.Id;
        Name = player.Name;
        WebImageLink = player.WebImageLink;
        Club = player.Club;
        Birthday = player.Birthday;
        BirthPlace = player.BirthPlace;
        Weight = player.Weight;
        Height = player.Height;
        Description = player.Description;
        Position = new PositionModel(player.Position);
    }

    public PlayerEntity ToEntity()
    {
        return new PlayerEntity
        {
            Id = Id,
            Name = Name,
            WebImageLink = WebImageLink,
            Club = Club,
            Birthday = Birthday,
            BirthPlace = BirthPlace,
            Weight = Weight.Value,
            Height = Height.Value,
            Description = Description,
            PositionId = Position.Id
        };
    }

    public void ToEntity(PlayerEntity player)
    {
        player.Id = Id;
        player.Name = Name;
        player.WebImageLink = WebImageLink;
        player.Club = Club;
        player.Birthday = Birthday;
        player.BirthPlace = BirthPlace;
        player.Weight = Weight.Value;
        player.Height = Height.Value;
        player.Description = Description;

        player.PositionId = Position.Id;
    }
}

public delegate void NotifyWithValidationMessages(Dictionary<string, string?> validationDictionary);

public class BaseViewModel : ObservableValidator
{
    public event NotifyWithValidationMessages? ValidationCompleted;

    public virtual ICommand ValidateCommand => new RelayCommand(() =>
    {
        ClearErrors();

        ValidateAllProperties();

        var validationMessages = this.GetErrors()
                                     .ToDictionary(k => k.MemberNames.First().ToLower(), v => v.ErrorMessage);

        ValidationCompleted?.Invoke(validationMessages);
    });


    [IndexerName("ErrorDictionary")]
    public ValidationStatus this[string propertyName]
    {
        get
        {
            var errors = this.GetErrors()
                             .ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>();

            var hasErrors = errors.TryGetValue(propertyName, out var error);
            return new ValidationStatus(hasErrors, error ?? string.Empty);
        }
    }

    public BaseViewModel() : base()
    {}
}

public class ValidationStatus : ObservableObject
{
    private bool hasError;
    private string error;

    public bool HasError
    {
        get => this.hasError;
        set => SetProperty(ref this.hasError, value);
    }

    public string Error
    {
        get => this.error;
        set => SetProperty(ref this.error, value);
    }

    public ValidationStatus()
    {
    }

    public ValidationStatus(bool hasError, string error)
    {
        this.hasError = hasError;
        this.error = error;
    }
}

public class PositionModel
{
    [Required]
    [Range(1, 7)]
    public int Id { get; set; }

    [Required]
    [StringLength(255)]
    public string Name { get; set; }

    public PositionModel()
    {
    }

    public PositionModel(int id, string name)
    {
        Id = id;
        Name = name;
    }

    public PositionModel(PositionEntity entity)
    { 
        Id = entity.Id;
        Name = entity.Name;
    }

    public PositionEntity ToEntity()
    {
        return new PositionEntity
        {
            Id = Id,
            Name = Name
        };
    }

    public void ToEntity(PositionEntity entity)
    {
        entity.Id = Id;
        entity.Name = Name;
    }

What is very interesting, if I edit the first object everything works as it, but if I navigate on the second object and try to edit it, the position is not set. And so on, the third object will have set the position, the 4th won't, and so on. Any idea?

Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
Wasyster
  • 2,279
  • 4
  • 26
  • 58
  • 1
    Where's `player` defined and why are you setting the `BindingContext` in `OnAppearing()`? Also, where's the "Update Page" you're referring to? Please show all relevant parts of your code. – Julian Jan 07 '23 at 15:30
  • 2
    You are also manually setting the picker values and then setting the BindingContext, which will likely override the values you just set. If you are using data binding you should not also set values manually – Jason Jan 07 '23 at 15:39
  • @Jason I tried without setting the picker selected index, but not worked. I saw a post where this was the suggested way, but not working for me. – Wasyster Jan 07 '23 at 16:02
  • @ewerspej I updated the question, and posted all the code, as it was originaly – Wasyster Jan 07 '23 at 16:04
  • 1
    first, set the BindingContext in the constructor and see if that helps – Jason Jan 07 '23 at 16:26
  • You should consider using the MVVM pattern. You have a lot of logic in your View code. Business logic belongs into separate, maintainable classes, which makes debugging, testing and development a lot easier and less error-prone. – Julian Jan 07 '23 at 16:44
  • @Jason not helped, that way the controls are empty. – Wasyster Jan 07 '23 at 16:48
  • you are also setting the BindingContext in BOTH the XAML and the code behind. This is a problem. – Jason Jan 07 '23 at 16:52
  • @Jason I disagree. – Wasyster Jan 07 '23 at 18:51
  • Show declaration of `PlayerModel` class, and its `Name` property. I assume you've verified ItemsSource is being set to a list that contains at least one item? Consider a test where you set ItemsSource **in the constructor** (after InitializeComponent) to a "hardcoded" list with a single Player, whose Name is set. Reason: I'm suspecting there is some problem with the order in which things are done. Want to know if the picker behaves correctly if the information is available much earlier. – ToolmakerSteve Jan 08 '23 at 03:22
  • `SelectedItem="{Binding Position}">`. Show declaration of `Position`. When are you expecting the "previous value" to be shown? You mean after user picks one? Any warnings in VS Output Pane, that mention `Position` or `Name`? – ToolmakerSteve Jan 08 '23 at 03:34
  • @ToolmakerSteve The previous value for position is coming as a part of the query property, and the picker is not showing the value for it. – Wasyster Jan 08 '23 at 07:36
  • @ToolmakerSteve I added the asked models – Wasyster Jan 08 '23 at 07:38
  • @ToolmakerSteve The picker ItemSource is populated as it should be. – Wasyster Jan 08 '23 at 07:39
  • *"The picker ItemSource is populated as it should be"* I understand. What I'm speculating is that maybe there is a Maui bug where the value is not displayed unless the itemSource is populated EARLIER. I could be totally off-base, but populating it **in the constructor** would tell us whether this is an issue or not. – ToolmakerSteve Jan 09 '23 at 01:17
  • *"And so on, the third object will have set the position, the 4th won't, and so on"* - **1)** Does the position not get **set** in the item, OR is it just not **displayed** in the picker? **2)** If you edit them in a different order, does that change which ones "work"? Is it always "works, then not work, works, then not work", regardless of which you pick? **3)** Anyway, sounds like confirmation that there is some Maui bug re updating the displayed value. – ToolmakerSteve Jan 09 '23 at 01:23
  • `OnPropertyChanged("ErrorDictionary[WebImageLink]");`. I've never seen a syntax like that string `"ErrorDictionary[WebImageLink]"`. **1)** **Please give link to relevant doc.** All I've ever seen is`"WebImageLink"` or `nameof(WebImageLink)`. OR simply leave off the parameter: it is not needed unless one property is dependent on another property. **2)** **Does it work better if** you change all those to simply `OnPropertyChanged();`? – ToolmakerSteve Jan 09 '23 at 01:28
  • What's the code of `ObservableValidator`,`PositionEntity `, `ClearErrors();` and `ValidateAllProperties();`? If it is convenient for you, could you please post a basic demo so that we can test on our side? – Jessie Zhang -MSFT Jan 09 '23 at 08:05
  • @ToolmakerSteve it was interesting while I do the debug. The Position in the player are received, and when setting the BindingContext, the Position property is set to null. Editing in the different order, doesn't change anything. My suggestion is that the Picker cant find the SelectedItem int he ItemSource for some reason. – Wasyster Jan 09 '23 at 12:10
  • 1
    @ToolmakerSteve Please visit the following answers https://stackoverflow.com/questions/74956610/net-maui-entry-behaviors-and-triggers-on-validation-not-triggers/74961564#74961564 – Wasyster Jan 09 '23 at 12:15
  • @ToolmakerSteve ObservableValidator cames form CommunityToolkit.Mvvm package – Wasyster Jan 09 '23 at 12:34

2 Answers2

1

At the end, I finally come up with a solution. The Position class has to implement IEquatable interface, then the Picker finds the binded Position object from the ViewModel.

public bool Equals(PositionModel? position) => this.Id == position?.Id &&
                                               this.Name == position?.Name;

Another solution was, as I mentioned in my comment: ,, If we want the Picker to bind to a property of object type, it needs to come from the same collection as the Pickers ItemSource. I have a feeling, that it looks equality on a ,,reference,, type. ,,

Wasyster
  • 2,279
  • 4
  • 26
  • 58
  • FYI: `OnPropertyChanged("player");` is a bug. xamarin is NOT being told when `Player property` changes. Should be `OnPropertyChanged("Player");` OR `OnPropertyChanged();` OR `OnPropertyChanged(nameof(Player));` I don't know whether fixing that would have helped or not, but you should make that change in case it affects something else in the future. – ToolmakerSteve Jan 12 '23 at 00:29
0

UPDATE

Ok, I misunderstood the binding. I think x:Name ViewModel is interfering with BindingContext = player.

  • Remove BindingContext from xaml.
  • Do that in constructor instead: BindingContext = new PlayerModel().
  • Then the later set of BindingContext should work as expected.

———————

Original answer; Wrong explanation

  • Position is a property on each Player.
  • Picker is on the page.
  • <Picker x:Name="position" SelectedItem="{Binding Position}"> is looking for a property Position on the page. Looks like there is no such property.

It won't magically find Position on a player.

I see page has Player property. Assuming that is always the correct player to look at, then bind to Player.Position. I'm not sure what happens if Player is null.

Try:
<Picker x:Name="position" SelectedItem="{Binding Player.Position}">.

ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • Ok, I will try. But the BindingContext is set to a player variable, but I think you got the point, have sense what you wrote. Let you know.; – Wasyster Jan 10 '23 at 10:06
  • Ok, I misunderstood the binding. My explanation is wrong. x:Name ViewModel is interfering with BindingContext = player. Remove BindingContext from xaml. Do that in constructor instead. – ToolmakerSteve Jan 10 '23 at 17:16
  • I tried, because I was out of idea, but not worked. – Wasyster Jan 11 '23 at 07:45
  • At the end I figured out. I think the MAUI Picker has a bug. If we want Picker to bind to a property of object type, it needs to come from the same collection as the Pickers ItemSource. I have a feeling, that it looks equality on a ,,reference,, type. Another idea is to implement the IEquatable Interface on the Position class. – Wasyster Jan 11 '23 at 19:37