MAUI CollectionView selected item error

Lukas Hubacek 0 Reputation points
2025-01-03T23:01:00.3966667+00:00

Hello.

I starts to learn MAUI Enterprise apps patterns and I found en error (or unexpected behavior) in CollectionView.

In my example, I have TrainingsViewModel whitch have ObservableCollection of TrainingViewModel property named as AllTrainings. This collection is binded in TrainingsView as CollectionView. Selected item of this CollectionView is binded as SelectedTraining in TrainingsViewModel.

Insert and Update of this Collection works fine, but when I remove an item, there is an UI error.

Binding to SelectedTraining works fine too.

But when I remove an item from Collection, I cannot select any item bellow by removed item position properly. In fact binding to selected item works after click, but UI doesn't show item as selected.

For example:

In pic. 1 I have 5 items in CollectionView and selected item no 5. UI shows properly selected item.

1

In pic. 2 I delete third item and then click to second item. UI shows selected item properly.

2

But when I click on 4. or 5. item after 3. item was delete (pic. 3), UI not shows selected item properly. It seems, that no item is selected. (But binding to selected items work well, when I click to edit, it works, only UI not show selected item).

3

Can you tell me, what I am doing wrong, please?

Here is TrainingsViewModel:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.Windows.Input;
using IntervalStopwatch.Services;
//using static Android.Provider.ContactsContract.CommonDataKinds;
//using Java.Time;
using IntervalStopwatch.Models;
namespace IntervalStopwatch.ViewModels
{
    public class TrainingsViewModel : IQueryAttributable
    {
        private readonly IDbService _dbService;
        private readonly INavigationService _navigationService;
        public ObservableCollection<ViewModels.TrainingViewModel> AllTrainings { get; }
        public TrainingViewModel? SelectedTraining { get; set; }
        public ICommand NewCommand { get; }
        public ICommand LoadCommand { get; }
        public ICommand EditCommand { get; }
        public ICommand DeleteCommand { get; }
        public ICommand ImportCommand { get; }
        public ICommand ExportCommand { get; }
        public async void ApplyQueryAttributes(IDictionary<string, object> query)
        {
            if (query.ContainsKey("deleted"))
            {
                int id;
                string idString = query["deleted"].ToString();
                if (int.TryParse(idString, out id))
                {
                    TrainingViewModel training = AllTrainings.Where(x => x.Id == id).FirstOrDefault();
                    // If note exists, delete it
                    if (training != null)
                    {
                        // when I remove an item, it will already removed, but UI not works well.
                        AllTrainings.Remove(training);
                        
                        // this works fine, but it not "fine" for me. For example can have lot of items.
                        //AllTrainings.Clear();
                        //LoadTrainings();
                    }
                }
                
            }
            else if (query.ContainsKey("saved"))
            {
                int id;
                string idString = query["saved"].ToString();
                if (int.TryParse(idString, out id))
                {
                    TrainingViewModel training = AllTrainings.Where(x => x.Id == id).FirstOrDefault();
                    // If note is found, update it
                    if (training != null)
                    {
                        training.Reload();
                    }
                    // If note isn't found, it's new; add it.
                    else
                    {
                        Training newTraining = await _dbService.GetTrainingAsync(id);
                        AllTrainings.Add(new TrainingViewModel(_dbService, _navigationService, newTraining));
                    }
                } 
            }
        }
        public TrainingsViewModel(IDbService dbService, INavigationService navigationService)
        {
            _dbService = dbService;
            _navigationService = navigationService;
            AllTrainings = new ObservableCollection<TrainingViewModel>();
            NewCommand = new AsyncRelayCommand(NewTrainingAsync);
            EditCommand = new AsyncRelayCommand(EditTrainingAsync);
            LoadTrainings();
        }
        private async void LoadTrainings()
        {
            var trainings = await _dbService.GetTrainingListAsync();
            foreach (var training in trainings)
            {
                TrainingViewModel model = new TrainingViewModel(_dbService, _navigationService, training);
                AllTrainings.Add(model);
            }
        }
        private async Task NewTrainingAsync()
        {
            await _navigationService.NavigateToAsync("//TrainingAdd?new=x");
            SelectedTraining = null;
        }
        private async Task EditTrainingAsync()
        {
            if (SelectedTraining != null)
            {
                await _navigationService.NavigateToAsync($"//TrainingEdit?load={SelectedTraining.Id}");
                SelectedTraining = null;
            }
        }
    }
}

TrainingViewModel:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.Windows.Input;
using IntervalStopwatch.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using static System.Net.Mime.MediaTypeNames;
using static System.Runtime.InteropServices.JavaScript.JSType;
using IntervalStopwatch.Services;
namespace IntervalStopwatch.ViewModels
{
    public class TrainingViewModel : ObservableObject, IQueryAttributable
    {
        private Training _training;
        private readonly IDbService _dbService;
        private readonly INavigationService _navigationService;
        public int Id
        {
            get => _training.Id;
            set
            {
                if (_training.Id != value)
                {
                    _training.Id = value;
                    OnPropertyChanged();
                }
            }
        }
        public string Name
        {
            get => _training.Name;
            set
            {
                if (_training.Name != value)
                {
                    _training.Name = value;
                    OnPropertyChanged();
                }
            }
        }
        public int TotalRoundCount
        {
            get => _training.TotalRoundCount;
            set
            {
                if (_training.TotalRoundCount != value)
                {
                    _training.TotalRoundCount = value;
                    OnPropertyChanged();
                }
            }
        }
        public ICommand AddCommand { get; private set; }
        public ICommand SaveCommand { get; private set; }
        public ICommand DeleteCommand { get; private set; }
        public TrainingViewModel(IDbService dbService, INavigationService navigationService)
        {
            _training = new Models.Training();
            _dbService = dbService;
            AddCommand = new AsyncRelayCommand(AddAsync);
            SaveCommand = new AsyncRelayCommand(SaveAsync);
            DeleteCommand = new AsyncRelayCommand(DeleteAsync);
            _navigationService = navigationService;
        }
        public TrainingViewModel(IDbService dbService, INavigationService navigationService, Models.Training training)
        {
            _training = training;
            _dbService = dbService;
            SaveCommand = new AsyncRelayCommand(SaveAsync);
            DeleteCommand = new AsyncRelayCommand(DeleteAsync);
            _navigationService = navigationService;
        }
        private async Task AddAsync()
        {
            var result = await _dbService.InsertTrainingAsync(_training);
            _navigationService.NavigateToAsync($"//Trainings?saved={result.Id}");
            //await Shell.Current.GoToAsync($"..?saved={_note.Filename}");
        }
        private async Task SaveAsync()
        {
            _dbService.UpdateTrainingAsync(_training);
            _navigationService.NavigateToAsync($"//Trainings?saved={_training.Id}");
            //await Shell.Current.GoToAsync($"..?saved={_note.Filename}");
        }
        private async Task DeleteAsync()
        {
            _dbService.DeleteTrainingAsync(_training);
            _navigationService.NavigateToAsync($"//Trainings?deleted={_training.Id}");
            //_note.Delete();
            //await Shell.Current.GoToAsync($"..?deleted={_note.Filename}");
        }
        async void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
        {
            if (query.ContainsKey("load"))
            {
                int id;
                if (int.TryParse(query["load"].ToString(), out id))
                {
                    _training = await _dbService.GetTrainingAsync(id);
                }
                RefreshProperties();
            }
            else
            {
                _training = new Training();
                RefreshProperties();
            }
        }
        public async void Reload()
        {
            _training = await _dbService.GetTrainingAsync(_training.Id);
            RefreshProperties();
        }
        private void RefreshProperties()
        {
            OnPropertyChanged(nameof(Id));
            OnPropertyChanged(nameof(Name));
            OnPropertyChanged(nameof(TotalRoundCount));
        }
    }
}

TrainingsView

<?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:viewModels="clr-namespace:IntervalStopwatch.ViewModels"
             x:Class="IntervalStopwatch.Views.TrainingsView"
             Title="TrainingsView">
    <!-- Add an item to the toolbar -->
    <ContentPage.ToolbarItems>
        <ToolbarItem Text="Add" Command="{Binding NewCommand}" IconImageSource="{FontImage Glyph='Add', Color=Black, Size=22}" />
        <ToolbarItem Text="Edit" Command="{Binding EditCommand}" IconImageSource="{FontImage Glyph='Edit', Color=Black, Size=22}" />
    </ContentPage.ToolbarItems>
    <!-- Display notes in a list -->
    <CollectionView x:Name="trainingsCollection"
                    ItemsSource="{Binding AllTrainings}"
                    Margin="20"
                    SelectionMode="Single"
                    SelectedItem="{Binding SelectedTraining}">
        <!-- Designate how the collection of items are laid out -->
        <CollectionView.ItemsLayout>
            <LinearItemsLayout Orientation="Vertical" ItemSpacing="10" />
        </CollectionView.ItemsLayout>
        <!-- Define the appearance of each item in the list -->
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <StackLayout>
                    <Label Text="{Binding Name}" FontSize="22"/>
                    <Label Text="{Binding TotalRoundCount}" FontSize="14" TextColor="Silver"/>
                </StackLayout>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</ContentPage>

Thank you for answer.

.NET MAUI
.NET MAUI
A Microsoft open-source framework for building native device applications spanning mobile, tablet, and desktop.
3,916 questions
{count} votes

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.