I'm developing a blazor web app and I'm facing a weird behaviour. Almost all pages are a copy/paste, they have CRUD operations and a previous list of differents elements (seasons, player, fixtures, etc...) but only in one of them I'm having problems. When I click on the 'Edit' button of a player I'm getting a 'Cannot access a disposed object' referring to the _dbContext.
Adding a breakpoint to the DisposeAsync() method I've discovered that it's been fired when I navigate to the edit page. But only happens in player's page, not in any of the other pages.
Here's the players page:
@page "/players"
@attribute [Authorize]
@using Models = Biwenger.Models
@using Biwenger.Services
@inject PlayersService service;
@inject NavigationManager navigationManager;
<h3>Jugadores</h3>
<a class="btn btn-primary" href="/player" role="button">Añadir</a>
<div class="mb-3 lg-6">
<label for="search" class="form-label">Buscar Jugador:</label>
<input type="text" id="search" class="form-control" @bind="searchTerm" @oninput="FilterPlayers" placeholder="Escribe el nombre del jugador..." />
</div>
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Nombre</th>
<th scope="col">Equipo</th>
<th scope="col">Acciones</th>
</tr>
</thead>
<tbody>
@if (filteredList.Count > 0)
{
foreach (var item in filteredList)
{
<tr>
<th scope="row">@item.Id</th>
<td>@item.Name</td>
<td>@item.Team.Name</td>
<td>
<button type="button" class="btn btn-primary" @onclick="() => EditPlayer(item.Id, item.TeamId)">Editar</button>
</td>
</tr>
}
}
else
{
<tr>
<td colspan="4" class="text-center">No hay registros.</td>
</tr>
}
</tbody>
</table>
@code {
List<Models.Player> fullList = new List<Models.Player>();
List<Models.Player> filteredList = new List<Models.Player>();
string searchTerm = "";
protected override async Task OnInitializedAsync()
{
fullList = await service.GetAllPlayers();
filteredList = fullList;
}
private void FilterPlayers()
{
if (string.IsNullOrEmpty(searchTerm))
{
filteredList = fullList;
} else
{
filteredList = fullList.Where(p => p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
.ToList();
}
StateHasChanged();
}
private void EditPlayer(int id, int teamId)
{
navigationManager.NavigateTo($"/player/{id}/{teamId}"); <-- Here calls for dispose
}
}
This is the page I'm navigating to:
@page "/player/{id:int?}/{teamId:int?}"
@attribute [Authorize]
@using Biwenger.Models.ViewModels
@using Biwenger.Services
@inject ILogger<Player> Logger
@inject PlayersService service
@inject TeamsService teamsService
@inject SeasonsService seasonsService;
@inject NavigationManager navigationManager
@inject IJSRuntime JS
<PageTitle>Equipo</PageTitle>
<h3>@(model?.PlayerId == 0 ? "Nuevo Jugador" : "Editar Jugador")</h3>
<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="form">
<DataAnnotationsValidator />
<div class="col-mb-3 col-lg-6 col-md-6">
<label for="name" class="form-label">Nombre</label>
<input id="name" class="form-control" @bind="model!.Name" @onblur="CheckIfNameExists" @ref="nameInput"></input>
</div>
<div class="col-mb-3 col-lg-6">
<ValidationMessage For="() => model!.Name"></ValidationMessage>
</div>
<div class="col-mb-3 col-lg-6 col-md-6">
<label for="team" class="form-label">Equipo</label>
<InputSelect id="team" class="form-control" @bind-Value="model!.TeamId">
<option value="">Seleccione un equipo</option>
@foreach (Models.Team team in listTeams)
{
<option value="@(team.Id)">@team.Name</option>
}
</InputSelect>
</div>
<div class="col-mb-3 col-lg-6">
<ValidationMessage For="() => model!.TeamId"></ValidationMessage>
</div>
<div class="col-mb-3 col-lg-6 col-md-6">
<label for="team" class="form-label">Posición</label>
@if (positionsItems != null)
{
<BootstrapSelect
TItem="Models.DropdownItem<String>"
Data="@positionsItems"
@bind-Value="model!.Position"
TextField="@((item) => item.Value)"
ValueField="@((item) => item.Key.ToString())"
TType="Biwenger.Enums.Positions">
</BootstrapSelect>
}
</div>
<div class="col-mb-3 col-lg-6">
<ValidationMessage For="() => model!.TeamId"></ValidationMessage>
</div>
<div class="col-mb-3 col-lg-6 col-md-6">
<label for="cost" class="form-label">Coste @currentSeason!.Name</label>
<input type="number" id="cost" @bind="currentPlayerCost!.Cost" min="1" class="form-control text-end" @ref="costInput" @onfocus="selectAllText" @onblur="addMillions" />
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="@model!.Black" id="checkBlack" @bind="model!.Black" />
<label class="form-check-label" for="checkBlack"> Es negro</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="@model!.Active" id="checkActive" @bind="model!.Active" />
<label class="form-check-label" for="checkActive"> Activo</label>
</div>
<div class="col-mb-3 col-lg-6">
<button type="button" class="btn btn-secondary ms-2" @onclick="GoBack">Volver</button>
<button type="submit" class="btn @buttonClass" disabled="@(!editContext?.Validate() ?? false || isLoading)">
@if (isLoading)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="visually-hidden">Enviando...</span>
}
else if (showSuccess)
{
<span>Guardado...</span>
}
else if (showError)
{
<span>Error...</span>
}
else
{
<span>Enviar</span>
}
</button>
</div>
</EditForm>
@code {
private EditContext? editContext;
private ElementReference nameInput;
private ElementReference costInput;
private Biwenger.Models.Season? currentSeason;
private Biwenger.Models.PlayerSeasonCost? currentPlayerCost;
[Parameter]
public int? id { get; set; }
[Parameter]
public int? teamId { get; set; }
[SupplyParameterFromForm]
private PlayerWithCostViewModel? model { get; set; }
private List<Models.Team> listTeams = new List<Models.Team>();
private ValidationMessageStore? messageStore;
private bool isLoading = false;
private bool showSuccess = false;
private bool showError = false;
private string buttonClassSuccess = "btn-success";
private string buttonClassError = "btn-danger";
private string buttonClassIdle = "btn-primary";
private string buttonClass = "btn-primary";
IList<Models.DropdownItem<String>> positionsItems;
protected override async Task OnInitializedAsync()
{
positionsItems = new List<Models.DropdownItem<String>>
{
new Models.DropdownItem<String> { Key = 0, Value = "Posición" },
new Models.DropdownItem<String> { Key = 1, Value = "PT" },
new Models.DropdownItem<String> { Key = 2, Value = "DF" },
new Models.DropdownItem<String> { Key = 3, Value = "MC" },
new Models.DropdownItem<String> { Key = 4, Value = "DL" },
};
editContext = new EditContext(model ??= new PlayerWithCostViewModel());
currentSeason = seasonsService.GetCurrentSeason();
currentPlayerCost = new Models.PlayerSeasonCost();
listTeams = await teamsService.GetAllTeams();
if (id.HasValue && id.Value > 0)
{
model = await service.GetPlayerWithCostById(id.Value, teamId!.Value);
if (model != null)
{
editContext = new EditContext(model);
}
}
messageStore = new ValidationMessageStore(editContext);
}
private async void CheckIfNameExists()
{
if (!string.IsNullOrEmpty(model!.Name))
{
bool exists = await service!.NameExists(model!.Name, id, model!.TeamId);
messageStore?.Clear(() => model.Name);
if (exists)
{
messageStore?.Clear();
messageStore?.Add(() => model.Name, "El nombre ya existe");
}
editContext?.NotifyValidationStateChanged();
}
}
private void GoBack()
{
navigationManager.NavigateTo("/players");
}
private async Task Submit()
{
isLoading = true;
Logger.LogInformation("Se ha llamado a submit");
bool success = false;
if (editContext!.Validate())
{
if (id.HasValue && id.Value > 0)
{
success = await service.UpdatePlayer(model!, currentSeason!.Id);
}
else
{
if (model!.Costs!.Count == 0)
{
model!.Costs.Add(new Models.PlayerSeasonCost()
{
Cost = currentPlayerCost!.Cost,
SeasonId = currentSeason!.Id,
TeamId = model!.TeamId
});
}
success = await service!.AddPlayer(model!);
}
isLoading = false;
if (success)
{
showSuccess = true;
buttonClass = buttonClassSuccess;
StateHasChanged();
await Task.Delay(3000);
showSuccess = false;
buttonClass = buttonClassIdle;
StateHasChanged();
if (!id.HasValue)
{
model = new PlayerWithCostViewModel();
editContext = new EditContext(model);
messageStore = new ValidationMessageStore(editContext);
messageStore.Clear();
}
model!.Name = "";
model.TeamId = 0;
currentPlayerCost = new Models.PlayerSeasonCost();
model!.Black = false;
model!.Active = true;
StateHasChanged();
await JS.InvokeVoidAsync("focusElement", nameInput);
}
else
{
showSuccess = true;
buttonClass = buttonClassError;
await Task.Delay(1000);
showSuccess = false;
buttonClass = buttonClassIdle;
}
}
}
private async Task selectAllText()
{
await JS.InvokeVoidAsync("selectElementText", costInput);
}
private void addMillions()
{
if (currentPlayerCost!.Cost < 100)
{
currentPlayerCost.Cost = currentPlayerCost.Cost * 1000000;
}
}
}
And the playersService:
using Biwenger.Data;
using Biwenger.Models;
using Biwenger.Models.ViewModels;
using Microsoft.EntityFrameworkCore;
namespace Biwenger.Services
{
public class PlayersService
{
private readonly ApplicationDbContext _dbContext;
public PlayersService(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<List<Player>> GetAllPlayers()
{
return await _dbContext.Players.Include(p => p.Team).AsNoTracking().ToListAsync();
}
public async Task<List<Player>> GetAllActivePlayers()
{
return await _dbContext.Players.Where(p => p.Active == true).AsNoTracking().ToListAsync();
}
public async Task<bool> AddPlayer(PlayerWithCostViewModel player)
{
var strategy = _dbContext.Database.CreateExecutionStrategy();
bool result = false;
await strategy.ExecuteAsync(async () =>
{
var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
var newPlayer = new Player
{
Active = player.Active,
Black = player.Black,
TeamId = player.TeamId,
Name = player.Name,
Position = player.Position,
};
await _dbContext.Players.AddAsync(newPlayer);
var playerSeasonCost = new PlayerSeasonCost
{
Cost = player.Costs!.First().Cost,
TeamId = player.TeamId,
PlayerId = newPlayer.Id,
SeasonId = player.Costs!.First().SeasonId,
};
await _dbContext.PlayersSeasonsCost.AddAsync(playerSeasonCost);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
result = true;
}
catch (Exception ex)
{
await transaction.RollbackAsync();
result = false;
}
});
return result;
}
public async Task<bool> UpdatePlayer(PlayerWithCostViewModel player, int seasonId)
{
Player? currentPlayer = await _dbContext.Players.FindAsync(player.PlayerId);
if (currentPlayer == null)
{
return false;
}
var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
currentPlayer.Name = player.Name;
currentPlayer.Black = player.Black;
currentPlayer.Position = player.Position;
currentPlayer.Active = player.Active;
currentPlayer.TeamId = player.TeamId;
_dbContext.Players.Update(currentPlayer);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return true;
}
catch (Exception ex)
{
await transaction.RollbackAsync();
return false;
}
}
public async Task<Player?> GetPlayerByid(int id)
{
return await _dbContext.Players.FindAsync(id);
}
public async Task<PlayerWithCostViewModel?> GetPlayerWithCostById(int playerId, int teamId)
{
try
{
Player? player = await _dbContext.Players
.Include(p => p.PlayersSeasonsCost)
.Where(p => p.Id == playerId && p.TeamId == teamId)
.FirstOrDefaultAsync(); <-- Already _dbContext Disposed
if (player == null)
{
return null;
}
List<PlayerSeasonCost>? pscList = await _dbContext.PlayersSeasonsCost.Where(psc => psc.PlayerId == playerId).AsNoTracking().ToListAsync();
PlayerWithCostViewModel pwcvm = new PlayerWithCostViewModel
{
PlayerId = playerId,
TeamId = teamId,
Active = player.Active,
Black = player.Black,
Position = player.Position,
Name = player.Name,
Costs = pscList ??= new List<PlayerSeasonCost>()
};
return pwcvm;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return null;
}
}
public async Task<bool> NameExists(string name, int? id, int teamId)
{
if (id.HasValue && id.Value > 0)
{
return await _dbContext.Players.AnyAsync(t => t.Name == name && t.TeamId == teamId && t.Id != id);
}
return await _dbContext.Players.AnyAsync(t => t.Name == name && t.TeamId == teamId);
}
public async Task<List<PlayerWithCurrentCostVM>> GetPlayersWithCurrentCost()
{
List<PlayerWithCurrentCostVM> pwcc = new List<PlayerWithCurrentCostVM>();
pwcc = await _dbContext.Players.Include(p => p.PlayersSeasonsCost)
.ThenInclude(psc => psc.Season)
.Where(p => p.Active == true)
.Select(p => new PlayerWithCurrentCostVM
{
Player = p,
Name = p.Name,
Cost = p.PlayersSeasonsCost == null ? 0 : p.PlayersSeasonsCost.Where(psc => psc.Season.Active == true).First().Cost
})
.AsNoTracking()
.ToListAsync();
return pwcc;
}
}
}
Finally, the problem was in this part of the code:
<BootstrapSelect
TItem="Models.DropdownItem<String>"
Data="@positionsItems"
@bind-Value="model!.Position"
TextField="@((item) => item.Value)"
ValueField="@((item) => item.Key.ToString())"
TType="Biwenger.Enums.Positions">
</BootstrapSelect>
It seems that BootsatrpSelect doesn't work well with Generic types or Enums. Binded it to a int variable and the problem dissapears.
Thank you all for your feedback