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();