Home > OS >  Displaying duplicate when adding a new item to an ObservableCollection<> bound to a Collection
Displaying duplicate when adding a new item to an ObservableCollection<> bound to a Collection

Time:11-18

I currently have a Collection View that displays a list of records being pulled from a local database in my device. The database is working fine, adding the records, deleting them is working fine. The problem is that when I add or delete a record, when the collection view gets refreshed it is displaying a duplicate of each existing record. The weird part is that if I refresh again, it goes back to normal and only shows the records of the table in the database that is pulling from.

Here is my view Model:

 [QueryProperty(nameof(Players), "Players")]
    public partial class ManagePlayersPageViewModel : ObservableObject
    {
        /// <summary>
        /// List of players being displayed 
        /// </summary>
        private ObservableCollection<Player> _players = new();
        public ObservableCollection<Player> Players
        {
            get => _players;
            set => SetProperty(ref _players, value);
        }

        [ObservableProperty] private bool isRefreshing;

        /// <summary>   
        /// Options for selection modes
        /// </summary>
        public SelectionOptions SelectionOptions { get; } = new();

        /// <summary>
        /// Adds player to list
        /// </summary>
        /// <returns></returns>
        [RelayCommand]
        async Task AddPlayer()
        {
          var task =  await Shell.Current.ShowPopupAsync(new AddPlayerPopup());
          var player = task as Player;

          if (task == null)
              return;

          if (await PlayerService.RecordExists(player))
          {
              await Shell.Current.DisplaySnackbar("Jugador ya existe");
              return;
          }
          
          await PlayerService.AddAsync(player);
          
         
          await Refresh();
        } 

Here is the refresh() method:

        /// <summary>
        /// Refreshs and updates UI after each database query 
        /// </summary>
        /// <returns></returns>
        [RelayCommand]
        async Task Refresh()
        {
            IsRefreshing = true;
            await Task.Delay(TimeSpan.FromSeconds(1));
            Players.Clear();

            var playersList = await PlayerService.GetPlayersAsync();
           
            foreach (Player player in playersList)
                Players.Add(player);

            IsRefreshing = false;
        }
        

Here is my xaml where the control sits:

<RefreshView Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3"
               IsRefreshing="{Binding IsRefreshing}"
               Command="{Binding RefreshCommand}">
                <CollectionView
                    ItemsSource="{Binding Players}"
                    SelectionMode="{Binding SelectionOptions.SelectionMode}">
                    <CollectionView.ItemTemplate>
                        <DataTemplate>
                            <SwipeView>
                                <SwipeView.RightItems>
                                    <SwipeItems>
                                        <SwipeItemView 
                                            Padding="0, 2.5"
                                            Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:ManagePlayersPageViewModel}}, Path= DeletePlayerCommand}"
                                            CommandParameter="{Binding .}">
                                            <Border 
                                                StrokeShape="RoundRectangle 10"
                                                Stroke="{StaticResource DimBlackSolidColorBrush}"
                                                Background="{StaticResource DimBlackSolidColorBrush}">
                                                <Grid>
                                                    <Image
                                                        Source="Resources/Images/delete.svg"
                                                        WidthRequest="35"
                                                        HeightRequest="35"
                                                        Aspect="AspectFill"/>
                                                </Grid>
                                            </Border>
                                        </SwipeItemView>
                                    </SwipeItems>
                                </SwipeView.RightItems>
                                <Grid>
                                <Border Grid.Column="0"
                                        StrokeShape="RoundRectangle 10"
                                        Stroke="{StaticResource DimBlackSolidColorBrush}"
                                        StrokeThickness="3">
                                    <Grid
                                        RowDefinitions="auto, auto, auto"
                                        Background="{StaticResource DimBlackSolidColorBrush}">
                                        <Label Grid.Row="0"
                                               Text="{Binding Name}"
                                               VerticalTextAlignment="Center"
                                               Margin="10, 2.5" 
                                               TextColor="White"/>
                                        <Label Grid.Row="1"
                                               Text="{Binding Alias}"
                                               VerticalTextAlignment="Center"
                                               Margin="10, 2.5" />
                                        <Label Grid.Row="2"
                                               Text="{Binding Team, TargetNullValue=Ninguno}"
                                               VerticalTextAlignment="Center"
                                               FontAttributes="Italic"
                                               Margin="10, 2.5" />
                                    </Grid>
                                </Border>
                                <Grid.GestureRecognizers>
                                    <TapGestureRecognizer 
                                        Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:ManagePlayersPageViewModel}}, Path=ItemTappedCommand}"
                                        CommandParameter="{Binding .}"/>
                                </Grid.GestureRecognizers>
                            </Grid>
                            </SwipeView>
                        </DataTemplate>
                    </CollectionView.ItemTemplate>
                </CollectionView>
           </RefreshView>

Any idea why this might be happening? Note: The database is being queried in a previous page, and is being passed as an argument to the page where the collection view sits, do not know if that has anything to do with it. Note: It used to work fine with the List View control, but I dont have as much flexibility for customization with that control which is why im going the route of using a Collection View.

When I debug, it is showing me that the value in the setter is already the duplicate but I have no clue on why or where is getting duplicated. It only happens when I add or delete a record. Any help is appreciated thanks!

CodePudding user response:

The symptom could happen if Refresh is called again, before it finishes. Specifically, if it is called again during await PlayerService.GetPlayersAsync(), the sequence of actions on Players would be:

  • call#1: Players.Clear();
  • call#2: Players.Clear();
  • call#1: add all (existing) players
  • call#2: add all players (including new one).

To find out if this is happening, add a check:

async Task Refresh()
{
    if (IsRefreshing)
        throw new InvalidProgramException("Nested call to Refresh");
    IsRefreshing = true;
    try
    {
        ... your existing code ...
    }
    finally
    {
        IsRefreshing = false;
    }
}

Does this ever throw that exception?


If so, then move Players.Clear() later in code. This might fix it:

var playersList = await PlayerService.GetPlayersAsync();
Players.Clear();

If that doesn't fix it, then add a lock around the statements that touch Players, to ensure that one call can't touch it until previous one is done:

...
lock (PlayersLock)
{
    Players.Clear();
    foreach (Player player in playersList)
       Players.Add(player);
}
...

// OUTSIDE of the method, define a member to act as a lock:
private object PlayersLock = new object();
  • Related