Here's a weird one:
I have a Blazor contact list page and when I scroll down and click a contact to go to detail page the detail page is scrolled to the bottom. If I pick the first record on the contact list and go to detail page it is at the top of detail. it seems like when I scroll down a little on the contact list page it scrolls all way the down on the detail page. Does that sound weird or what?
I haven't put in any state to hold scroll position or anything.
Contacts List Page
@page "/contact-list"
@rendermode InteractiveServer
@using AutoMapper
@inject ContactService ContactService
@inject IMapper Mapper
@inject NavigationManager Navigation
@inject IJSRuntime JS
<PageTitle>Contacts</PageTitle>
<div class="container mt-1">
<h3>Contacts</h3>
<div class="d-flex justify-content-between mb-3 align-items-center">
<button class="border-0" style="background-color: transparent;" @onclick="AddContact">
<span style="color: green;" class="bi bi-plus-circle-fill"> New</span>
</button>
<div class="input-group" style="width: 70%;">
<input type="text" placeholder="Search..." @bind="searchTerm" @bind:event="oninput" class="form-control" />
<span class="input-group-text" style="cursor: pointer;" @onclick="ClearSearch">✖</span>
</div>
</div>
<table class="table table-striped">
<thead>
<tr>
<th @onclick='() => SortContacts("FirstName")' style="cursor: pointer;">
First @if (currentSortField == "FirstName")
{
<span>@(isAscending ? "↑" : "↓")</span>
}
</th>
<th @onclick='() => SortContacts("LastName")' style="cursor: pointer;">
Last @if (currentSortField == "LastName")
{
<span>@(isAscending ? "↑" : "↓")</span>
}
</th>
<th></th>
</tr>
</thead>
<tbody>
@if (isAdding)
{
<tr>
<td colspan="3">
<EditForm Model="newContact" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="d-flex justify-content-between mb-3">
<div>
<button class="btn btn-outline-secondary" @onclick="CancelAdd">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
<div class="form-group">
<label for="firstName">First Name</label>
<InputText id="firstName" class="form-control" @bind-Value="newContact.FirstName" />
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<InputText id="lastName" class="form-control" @bind-Value="newContact.LastName" />
</div>
</EditForm>
</td>
</tr>
}
@foreach (var contact in PagedContacts)
{
<tr @key="contact.Id">
<td>@contact.FirstName</td>
<td>@contact.LastName</td>
<td>
<button class="me-1 border-0" style="background-color: transparent;" @onclick="() => EditContact(contact.Id)">
<span style="color: dimgrey;" class="bi bi-pencil"></span>
</button>
<button class="me-1 border-0" style="background-color: transparent;" @onclick="() => ConfirmDelete(contact.Id)">
<span style="color: red;" class="bi bi-x-circle-fill"></span>
</button>
</td>
</tr>
}
</tbody>
</table>
@if (totalPages > 1)
{
<div class="d-flex justify-content-between mt-3">
<button class="btn btn-outline-secondary" @onclick="PrevPage" disabled="@(!CanPrevPage)">Previous</button>
<span>Page @currentPage of @totalPages</span>
<button class="btn btn-outline-secondary" @onclick="NextPage" disabled="@(!CanNextPage)">Next</button>
</div>
}
</div>
@code {
private List<Contact> contacts = new();
private string searchTerm = string.Empty;
private const int PageSize = 1000;
private int currentPage = 1;
private int totalPages => (int)Math.Ceiling((double)contacts.Count / PageSize);
private string currentSortField = "FirstName";
private bool isAscending = true;
private Timer _debounceTimer;
private List<Contact> PagedContacts => contacts
.Where(c => string.IsNullOrEmpty(searchTerm) || c.FirstName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || c.LastName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
.OrderByDynamic(currentSortField, isAscending)
.Skip((currentPage - 1) * PageSize)
.Take(PageSize)
.ToList();
private Contact newContact = new();
private bool isAdding = false;
private bool CanPrevPage => currentPage > 1;
private bool CanNextPage => currentPage < totalPages;
protected override async Task OnInitializedAsync()
{
contacts = await ContactService.GetContactsAsync();
}
private void EditContact(Guid id)
{
Navigation.NavigateTo($"/contact-detail/{id}");
}
private void AddContact()
{
isAdding = true;
}
private void CancelAdd()
{
isAdding = false;
newContact = new();
}
private async Task HandleValidSubmit()
{
await ContactService.AddContactAsync(newContact);
contacts = await ContactService.GetContactsAsync(); // Refresh the contact list
newContact = new(); // Reset the form
isAdding = false; // Exit the add mode
}
private async Task ConfirmDelete(Guid id)
{
bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Are you sure you want to delete this contact?");
if (confirmed)
{
await ContactService.DeleteContactAsync(id);
contacts = await ContactService.GetContactsAsync(); // Refresh the contact list
}
}
private void PrevPage()
{
if (CanPrevPage)
{
currentPage--;
}
}
private void NextPage()
{
if (CanNextPage)
{
currentPage++;
}
}
private void SortContacts(string field)
{
if (currentSortField == field)
{
isAscending = !isAscending; // Toggle sorting order
}
else
{
currentSortField = field;
isAscending = true; // Default to ascending order when field changes
}
}
private void SearchChanged(ChangeEventArgs e)
{
searchTerm = e.Value.ToString();
_debounceTimer?.Dispose();
_debounceTimer = new Timer(_ => InvokeAsync(StateHasChanged), null, 500, Timeout.Infinite); // 500ms delay
}
private void ClearSearch()
{
searchTerm = string.Empty;
InvokeAsync(StateHasChanged);
}
}
Details Page
@page "/contact-detail/{id:guid}"
@rendermode InteractiveServer
@using AutoMapper
@inject ContactService ContactService
@inject IMapper Mapper
@inject NavigationManager Navigation
<PageTitle>Edit Contact</PageTitle>
<div class="container mt-5">
<h3>Edit Contact</h3>
<EditForm Model="contact" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="d-flex justify-content-between mb-3">
<div>
<button type="button" class="btn btn-outline-secondary" @onclick="Cancel">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
<div class="form-group">
<label for="firstName">First Name:</label>
<InputText id="firstName" class="form-control" @bind-Value="contact.FirstName" />
</div>
<div class="form-group">
<label for="middleName">Middle Name:</label>
<InputText id="middleName" class="form-control" @bind-Value="contact.MiddleName" />
</div>
<div class="form-group">
<label for="lastName">Last Name:</label>
<InputText id="lastName" class="form-control" @bind-Value="contact.LastName" />
</div>
<div class="form-group">
<label for="nickName">Nick Name:</label>
<InputText id="nickName" class="form-control" @bind-Value="contact.NickName" />
</div>
<div class="form-group">
<label for="birthDate">Birth Date:</label>
<InputDate id="birthDate" class="form-control" @bind-Value="contact.BirthDate" />
</div>
<div class="form-group">
<label for="publishPermission">Publish Permission:</label>
<InputText id="publishPermission" class="form-control" @bind-Value="contact.PublishPermission" />
</div>
</EditForm>
<div class="mt-4 mb-4">
<AddressList ContactId="@Id" />
</div>
<div class="mt-4 mb-4">
<ContactInfoList ContactId="@Id" />
</div>
</div>
@code {
[Parameter]
public Guid Id { get; set; }
private Contact contact = new();
protected override async Task OnInitializedAsync()
{
contact = await ContactService.GetContactByIdAsync(Id);
}
private async Task HandleValidSubmit()
{
await ContactService.UpdateContactAsync(contact);
Navigation.NavigateTo("/contact-list");
}
private void Cancel()
{
Navigation.NavigateTo("/contact-list");
}
}
Here's a weird one:
I have a Blazor contact list page and when I scroll down and click a contact to go to detail page the detail page is scrolled to the bottom. If I pick the first record on the contact list and go to detail page it is at the top of detail. it seems like when I scroll down a little on the contact list page it scrolls all way the down on the detail page. Does that sound weird or what?
I haven't put in any state to hold scroll position or anything.
Contacts List Page
@page "/contact-list"
@rendermode InteractiveServer
@using AutoMapper
@inject ContactService ContactService
@inject IMapper Mapper
@inject NavigationManager Navigation
@inject IJSRuntime JS
<PageTitle>Contacts</PageTitle>
<div class="container mt-1">
<h3>Contacts</h3>
<div class="d-flex justify-content-between mb-3 align-items-center">
<button class="border-0" style="background-color: transparent;" @onclick="AddContact">
<span style="color: green;" class="bi bi-plus-circle-fill"> New</span>
</button>
<div class="input-group" style="width: 70%;">
<input type="text" placeholder="Search..." @bind="searchTerm" @bind:event="oninput" class="form-control" />
<span class="input-group-text" style="cursor: pointer;" @onclick="ClearSearch">✖</span>
</div>
</div>
<table class="table table-striped">
<thead>
<tr>
<th @onclick='() => SortContacts("FirstName")' style="cursor: pointer;">
First @if (currentSortField == "FirstName")
{
<span>@(isAscending ? "↑" : "↓")</span>
}
</th>
<th @onclick='() => SortContacts("LastName")' style="cursor: pointer;">
Last @if (currentSortField == "LastName")
{
<span>@(isAscending ? "↑" : "↓")</span>
}
</th>
<th></th>
</tr>
</thead>
<tbody>
@if (isAdding)
{
<tr>
<td colspan="3">
<EditForm Model="newContact" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="d-flex justify-content-between mb-3">
<div>
<button class="btn btn-outline-secondary" @onclick="CancelAdd">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
<div class="form-group">
<label for="firstName">First Name</label>
<InputText id="firstName" class="form-control" @bind-Value="newContact.FirstName" />
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<InputText id="lastName" class="form-control" @bind-Value="newContact.LastName" />
</div>
</EditForm>
</td>
</tr>
}
@foreach (var contact in PagedContacts)
{
<tr @key="contact.Id">
<td>@contact.FirstName</td>
<td>@contact.LastName</td>
<td>
<button class="me-1 border-0" style="background-color: transparent;" @onclick="() => EditContact(contact.Id)">
<span style="color: dimgrey;" class="bi bi-pencil"></span>
</button>
<button class="me-1 border-0" style="background-color: transparent;" @onclick="() => ConfirmDelete(contact.Id)">
<span style="color: red;" class="bi bi-x-circle-fill"></span>
</button>
</td>
</tr>
}
</tbody>
</table>
@if (totalPages > 1)
{
<div class="d-flex justify-content-between mt-3">
<button class="btn btn-outline-secondary" @onclick="PrevPage" disabled="@(!CanPrevPage)">Previous</button>
<span>Page @currentPage of @totalPages</span>
<button class="btn btn-outline-secondary" @onclick="NextPage" disabled="@(!CanNextPage)">Next</button>
</div>
}
</div>
@code {
private List<Contact> contacts = new();
private string searchTerm = string.Empty;
private const int PageSize = 1000;
private int currentPage = 1;
private int totalPages => (int)Math.Ceiling((double)contacts.Count / PageSize);
private string currentSortField = "FirstName";
private bool isAscending = true;
private Timer _debounceTimer;
private List<Contact> PagedContacts => contacts
.Where(c => string.IsNullOrEmpty(searchTerm) || c.FirstName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || c.LastName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
.OrderByDynamic(currentSortField, isAscending)
.Skip((currentPage - 1) * PageSize)
.Take(PageSize)
.ToList();
private Contact newContact = new();
private bool isAdding = false;
private bool CanPrevPage => currentPage > 1;
private bool CanNextPage => currentPage < totalPages;
protected override async Task OnInitializedAsync()
{
contacts = await ContactService.GetContactsAsync();
}
private void EditContact(Guid id)
{
Navigation.NavigateTo($"/contact-detail/{id}");
}
private void AddContact()
{
isAdding = true;
}
private void CancelAdd()
{
isAdding = false;
newContact = new();
}
private async Task HandleValidSubmit()
{
await ContactService.AddContactAsync(newContact);
contacts = await ContactService.GetContactsAsync(); // Refresh the contact list
newContact = new(); // Reset the form
isAdding = false; // Exit the add mode
}
private async Task ConfirmDelete(Guid id)
{
bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Are you sure you want to delete this contact?");
if (confirmed)
{
await ContactService.DeleteContactAsync(id);
contacts = await ContactService.GetContactsAsync(); // Refresh the contact list
}
}
private void PrevPage()
{
if (CanPrevPage)
{
currentPage--;
}
}
private void NextPage()
{
if (CanNextPage)
{
currentPage++;
}
}
private void SortContacts(string field)
{
if (currentSortField == field)
{
isAscending = !isAscending; // Toggle sorting order
}
else
{
currentSortField = field;
isAscending = true; // Default to ascending order when field changes
}
}
private void SearchChanged(ChangeEventArgs e)
{
searchTerm = e.Value.ToString();
_debounceTimer?.Dispose();
_debounceTimer = new Timer(_ => InvokeAsync(StateHasChanged), null, 500, Timeout.Infinite); // 500ms delay
}
private void ClearSearch()
{
searchTerm = string.Empty;
InvokeAsync(StateHasChanged);
}
}
Details Page
@page "/contact-detail/{id:guid}"
@rendermode InteractiveServer
@using AutoMapper
@inject ContactService ContactService
@inject IMapper Mapper
@inject NavigationManager Navigation
<PageTitle>Edit Contact</PageTitle>
<div class="container mt-5">
<h3>Edit Contact</h3>
<EditForm Model="contact" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="d-flex justify-content-between mb-3">
<div>
<button type="button" class="btn btn-outline-secondary" @onclick="Cancel">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
<div class="form-group">
<label for="firstName">First Name:</label>
<InputText id="firstName" class="form-control" @bind-Value="contact.FirstName" />
</div>
<div class="form-group">
<label for="middleName">Middle Name:</label>
<InputText id="middleName" class="form-control" @bind-Value="contact.MiddleName" />
</div>
<div class="form-group">
<label for="lastName">Last Name:</label>
<InputText id="lastName" class="form-control" @bind-Value="contact.LastName" />
</div>
<div class="form-group">
<label for="nickName">Nick Name:</label>
<InputText id="nickName" class="form-control" @bind-Value="contact.NickName" />
</div>
<div class="form-group">
<label for="birthDate">Birth Date:</label>
<InputDate id="birthDate" class="form-control" @bind-Value="contact.BirthDate" />
</div>
<div class="form-group">
<label for="publishPermission">Publish Permission:</label>
<InputText id="publishPermission" class="form-control" @bind-Value="contact.PublishPermission" />
</div>
</EditForm>
<div class="mt-4 mb-4">
<AddressList ContactId="@Id" />
</div>
<div class="mt-4 mb-4">
<ContactInfoList ContactId="@Id" />
</div>
</div>
@code {
[Parameter]
public Guid Id { get; set; }
private Contact contact = new();
protected override async Task OnInitializedAsync()
{
contact = await ContactService.GetContactByIdAsync(Id);
}
private async Task HandleValidSubmit()
{
await ContactService.UpdateContactAsync(contact);
Navigation.NavigateTo("/contact-list");
}
private void Cancel()
{
Navigation.NavigateTo("/contact-list");
}
}
Share
edited Mar 13 at 14:05
phuclv
42.3k15 gold badges184 silver badges527 bronze badges
asked Mar 11 at 0:55
RodRod
15.5k35 gold badges134 silver badges264 bronze badges
1
- [Polite] Are you sure you're describing the Edit behaviour? I can see you have a new inline form for adding that would work as you describe, but the edit navigates to a new route so I don't see how you can preserve a scrolled position in a edit form where the content probably fits on the page. Can you provide a screen shot or two? – MrC aka Shaun Curtis Commented Mar 11 at 8:54
1 Answer
Reset to default 0You could add the js codes in app.razor:
Blazor.addEventListener('enhancedload', () => {
console.log("enhancedload")
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
});
check the console if enhancedload is triggered and scroll to the position you want