I have a task list that users can check off from the list, which disappears because Show Completed Tasks is not checked. However, when the task disappears, the next task item is checked, smh which is undesired behavior. Do you see anything in the code below?
@page "/task-list"
@inject IJSRuntime JS
@rendermode InteractiveServer
@using ClassLibrary1
@inject TaskRepository TaskRepo
@inject NavigationManager _navMgr
<h3 class="my-0">Task List</h3>
<div class="mt-1">Active: @ActiveTaskCount</div>
<div class="mb-2">Completed: @CompletedTaskCount</div>
<button class="btn btn-success mb-3" @onclick="AddTask"><span class="me-2 oi oi-plus"></span>Add Task</button>
<div class="form-check form-switch mb-3 float-end">
<input class="form-check-input" type="checkbox" id="showCompletedTasksCheckbox" @bind="showCompletedTasks">
<label class="form-check-label" for="showCompletedTasksCheckbox">Show Completed Tasks</label>
</div>
@if (tasks == null)
{
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
}
else
{
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th></th> <!-- Drag handle column -->
<th>Task</th>
<th></th>
</tr>
</thead>
<tbody id="task-list">
@foreach (var task in tasks)
{
if (showCompletedTasks || !task.TaskCompleted)
{
<tr data-id="@task.Id">
<td><i class="bi bi-grip-vertical" style="cursor: grab;"></i></td> <!-- Drag handle -->
<td>
<input type="checkbox" checked="@task.TaskCompleted" @onchange="@(async (e) => await ToggleTaskCompletion(task, (bool)e.Value))" />
@task.TaskName
</td>
<td>
<div class="d-flex">
<button class="me-1 border-0" style="background-color: transparent;" @onclick="() => EditTask(task.Id)">
<span style="color: dimgrey;" class="bi bi-pencil"></span>
</button>
<button class="btn-sm border-0" style="background-color: transparent;" @onclick="() => DeleteTask(task.Id)">
<span style="color: red;" class="bi bi-x-circle-fill"></span>
</button>
</div>
</td>
</tr>
}
}
</tbody>
</table>
}
@code {
private IEnumerable<ArcTask> tasks;
private DotNetObjectReference<TaskList> dotNetRef;
private bool showCompletedTasks = false;
protected override async Task OnInitializedAsync()
{
tasks = await TaskRepo.GetAllAsync("TaskCompleted DESC, TaskPriority ASC");
dotNetRef = DotNetObjectReference.Create(this);
UpdateTaskCounts();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Task.Delay(100);
await JS.InvokeVoidAsync("initializeSortable", dotNetRef);
}
}
[JSInvokable]
public async Task UpdateTaskPriorities(string[] reorderedIds)
{
for (int i = 0; i < reorderedIds.Length; i++)
{
var taskId = Guid.Parse(reorderedIds[i]);
var task = tasks.First(t => t.Id == taskId);
task.TaskPriority = i + 1; // Assign new priority based on position
await TaskRepo.UpdateAsync(task);
}
tasks = await TaskRepo.GetAllAsync("TaskCompleted DESC, TaskPriority ASC");
UpdateTaskCounts();
await InvokeAsync(StateHasChanged);
}
private void AddTask()
{
_navMgr.NavigateTo($"/task-detail/{Guid.Empty}");
}
private void EditTask(Guid id)
{
_navMgr.NavigateTo($"/task-detail/{id}");
}
private async Task DeleteTask(Guid id)
{
bool confirmed = await JS.InvokeAsync<bool>("confirm", "Are you sure you want to delete this item?");
if (confirmed)
{
await TaskRepo.DeleteAsync(id);
tasks = await TaskRepo.GetAllAsync("TaskCompleted DESC, TaskPriority ASC");
UpdateTaskCounts();
}
}
private async Task ToggleTaskCompletion(ArcTask task, bool isCompleted)
{
task.TaskCompleted = isCompleted;
await TaskRepo.UpdateAsync(task);
tasks = await TaskRepo.GetAllAsync("TaskCompleted DESC, TaskPriority ASC");
}
private int ActiveTaskCount => tasks?.Count(t => !t.TaskCompleted) ?? 0;
private int CompletedTaskCount => tasks?.Count(t => t.TaskCompleted) ?? 0;
private int TotalTaskCount => tasks?.Count() ?? 0;
private void UpdateTaskCounts()
{
InvokeAsync(StateHasChanged);
}
}
I have a task list that users can check off from the list, which disappears because Show Completed Tasks is not checked. However, when the task disappears, the next task item is checked, smh which is undesired behavior. Do you see anything in the code below?
@page "/task-list"
@inject IJSRuntime JS
@rendermode InteractiveServer
@using ClassLibrary1
@inject TaskRepository TaskRepo
@inject NavigationManager _navMgr
<h3 class="my-0">Task List</h3>
<div class="mt-1">Active: @ActiveTaskCount</div>
<div class="mb-2">Completed: @CompletedTaskCount</div>
<button class="btn btn-success mb-3" @onclick="AddTask"><span class="me-2 oi oi-plus"></span>Add Task</button>
<div class="form-check form-switch mb-3 float-end">
<input class="form-check-input" type="checkbox" id="showCompletedTasksCheckbox" @bind="showCompletedTasks">
<label class="form-check-label" for="showCompletedTasksCheckbox">Show Completed Tasks</label>
</div>
@if (tasks == null)
{
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
}
else
{
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th></th> <!-- Drag handle column -->
<th>Task</th>
<th></th>
</tr>
</thead>
<tbody id="task-list">
@foreach (var task in tasks)
{
if (showCompletedTasks || !task.TaskCompleted)
{
<tr data-id="@task.Id">
<td><i class="bi bi-grip-vertical" style="cursor: grab;"></i></td> <!-- Drag handle -->
<td>
<input type="checkbox" checked="@task.TaskCompleted" @onchange="@(async (e) => await ToggleTaskCompletion(task, (bool)e.Value))" />
@task.TaskName
</td>
<td>
<div class="d-flex">
<button class="me-1 border-0" style="background-color: transparent;" @onclick="() => EditTask(task.Id)">
<span style="color: dimgrey;" class="bi bi-pencil"></span>
</button>
<button class="btn-sm border-0" style="background-color: transparent;" @onclick="() => DeleteTask(task.Id)">
<span style="color: red;" class="bi bi-x-circle-fill"></span>
</button>
</div>
</td>
</tr>
}
}
</tbody>
</table>
}
@code {
private IEnumerable<ArcTask> tasks;
private DotNetObjectReference<TaskList> dotNetRef;
private bool showCompletedTasks = false;
protected override async Task OnInitializedAsync()
{
tasks = await TaskRepo.GetAllAsync("TaskCompleted DESC, TaskPriority ASC");
dotNetRef = DotNetObjectReference.Create(this);
UpdateTaskCounts();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Task.Delay(100);
await JS.InvokeVoidAsync("initializeSortable", dotNetRef);
}
}
[JSInvokable]
public async Task UpdateTaskPriorities(string[] reorderedIds)
{
for (int i = 0; i < reorderedIds.Length; i++)
{
var taskId = Guid.Parse(reorderedIds[i]);
var task = tasks.First(t => t.Id == taskId);
task.TaskPriority = i + 1; // Assign new priority based on position
await TaskRepo.UpdateAsync(task);
}
tasks = await TaskRepo.GetAllAsync("TaskCompleted DESC, TaskPriority ASC");
UpdateTaskCounts();
await InvokeAsync(StateHasChanged);
}
private void AddTask()
{
_navMgr.NavigateTo($"/task-detail/{Guid.Empty}");
}
private void EditTask(Guid id)
{
_navMgr.NavigateTo($"/task-detail/{id}");
}
private async Task DeleteTask(Guid id)
{
bool confirmed = await JS.InvokeAsync<bool>("confirm", "Are you sure you want to delete this item?");
if (confirmed)
{
await TaskRepo.DeleteAsync(id);
tasks = await TaskRepo.GetAllAsync("TaskCompleted DESC, TaskPriority ASC");
UpdateTaskCounts();
}
}
private async Task ToggleTaskCompletion(ArcTask task, bool isCompleted)
{
task.TaskCompleted = isCompleted;
await TaskRepo.UpdateAsync(task);
tasks = await TaskRepo.GetAllAsync("TaskCompleted DESC, TaskPriority ASC");
}
private int ActiveTaskCount => tasks?.Count(t => !t.TaskCompleted) ?? 0;
private int CompletedTaskCount => tasks?.Count(t => t.TaskCompleted) ?? 0;
private int TotalTaskCount => tasks?.Count() ?? 0;
private void UpdateTaskCounts()
{
InvokeAsync(StateHasChanged);
}
}
Share
edited Mar 10 at 1:29
Zhi Lv
22k1 gold badge27 silver badges37 bronze badges
asked Mar 8 at 23:21
RodRod
15.5k35 gold badges134 silver badges264 bronze badges
1
- 1 This is a common problem in loops where you refresh the collection on which the loop is based. The diffing engine is struggling to decide if a render frame has changed, and gets it wrong. The @key [as shown in Henk's answer] helps the diffing engine out by providing a unique key for each block of markup. – MrC aka Shaun Curtis Commented Mar 9 at 9:57
2 Answers
Reset to default 1I'm being a bit lazy here as it's a fair amount of code to look through. In my experience, this kind of bugginess when changing the number of items in a list is usually resolved by keying the item in question. Try modifying your checkboxes like this and let me know if it works:
<input @key="task" type="checkbox" checked="@task.TaskCompleted" @onchange="@(async (e) => await ToggleTaskCompletion(task, (bool)e.Value))" />
Since ToggleTaskCompletion() does a call to TaskRepo.GetAllAsync() it is probably more performant to key on the Id :
<tr @key="@task.Id" data-id="@task.Id"> ...
With a little luck that will avoid rendering unchanged items.