-
Dear Blazorise Team and users, First of all big thank you for this amazing open source project. Which sped up my development tremendously. I stumbled upon this bug in my application, which I believe is not a bug in Blazorise itself but rather something itself with how rendering is handled with asynchronous operations and how ReadData is hooked on these events and I need your advice/help. Consider following code: protected override async Task OnInitializedAsync()
{
var state = await LocalStorage.GetItemAsync<FleetState>(_fleetStateLocalStorageKey);
if (state is not null)
{
Page = state.Page;
PageSize = state.PageSize;
MotorVehicleFilter = state.Filter;
foreach (var columnState in state.ColumnStates)
{
_ = columnState.Field switch
{
"Brand" => BrandSortDirection = columnState.Direction,
"Model" => ModelSortDirection = columnState.Direction,
"ChassisNumber" => ChassisNumberSortDirection = columnState.Direction,
_ => _ = columnState.Direction
};
}
}
await LocalStorage.RemoveItemAsync(_fleetStateLocalStorageKey);
} I am using localstorage to persist the filter I used, last page and page size, and column ordering so when the user returns he will be on the same page with same settings as before. ReadData delegate used on ReadData EventCallBack private async Task ReadData(DataGridReadDataEventArgs<MotorVehicleResponse> eventArgs)
{
try
{
DataLoading = true;
Page = eventArgs.Page;
PageSize = eventArgs.PageSize;
Columns = eventArgs.Columns.ToList();
await GetMotorVehicles();
DataLoading = false;
}
catch (HttpRequestException)
{
await SnackbarStack.PushAsync("Something went wrong fetching the data. If this persists, contact the system administrator.", SnackbarColor.Warning);
}
} I believed that rendering the component, and ReadData in general waits before OnInitialized completes. But this is not the case. Since the processing thread is released on the await call in the OnInitialized method, it continues to the ReadData method and sends down a default query. When the data from localstorage eventually is retrieved, it sets the page, page size and other settings which in turn triggers another render event and so calls ReadData again with the state's values. Making twice a query call to the API. Thank you in advance. Further below full implementation if needed for analysis. Details - Click here to expand and see full component implementation
Here is my component @page "/fleet/"
@using FleetManagement.Blazor.Responses
<Heading>Fleet</Heading>
<Bar Mode="BarMode.Horizontal"
Breakpoint="Breakpoint.None"
ThemeContrast="ThemeContrast.None"
Visible="@FilterIsVisible"
Padding="Padding.Is0"
Margin="Margin.Is2.FromBottom">
<BarBrand>
<BarLink Clicked="@FilterVisibilityToggle">
<BarItem>
<BarIcon IconName="FontAwesomeIcons.Filter" />
</BarItem>
Filter
</BarLink>
</BarBrand>
<BarMenu>
<BarStart>
<BarItem>
<Row Gutter="(32, 16)">
<Column ColumnSize="ColumnSize.Is3.OnDesktop.IsFull.OnMobile">
<Addons>
<Addon AddonType="AddonType.Start">
<AddonLabel>Brand</AddonLabel>
</Addon>
<Addon AddonType="AddonType.Body">
<TextEdit Placeholder="Tesla"
@bind-Text="@MotorVehicleFilter.Brand" />
</Addon>
</Addons>
</Column>
<Column ColumnSize="ColumnSize.Is3.OnDesktop.IsFull.OnMobile">
<Addons>
<Addon AddonType="AddonType.Start">
<AddonLabel>Model</AddonLabel>
</Addon>
<Addon AddonType="AddonType.Body">
<TextEdit Placeholder="Model 3"
@bind-Text="@MotorVehicleFilter.Model" />
</Addon>
</Addons>
</Column>
<Column ColumnSize="ColumnSize.Is3.OnDesktop.IsFull.OnMobile">
<Addons>
<Addon AddonType="AddonType.Start">
<AddonLabel>VIN</AddonLabel>
</Addon>
<Addon AddonType="AddonType.Body">
<TextEdit Placeholder="A VIN number"
@bind-Text="@MotorVehicleFilter.ChassisNumber" />
</Addon>
</Addons>
</Column>
<Column ColumnSize="ColumnSize.Is1.OnDesktop.IsFull.OnMobile">
<Field Margin="Margin.Is4.FromLeft.OnDesktop.IsAuto">
<Switch TValue="bool"
@bind-Checked="@MotorVehicleFilter.Operational" Size="Size.Medium">Operational</Switch>
</Field>
</Column>
</Row>
</BarItem>
</BarStart>
<BarEnd Margin="Margin.Is2.FromTop">
<BarItem>
<Buttons Margin="Margin.IsAuto.OnDesktop.Is2.OnMobile">
<Button Color="Color.Primary"
Clicked="@ApplyFilter"
Disabled="@DataLoading"
Loading="@DataLoading">
Apply filter
</Button>
<Button Color="Color.Secondary"
Clicked="@ClearFilter"
Disabled="@DataLoading">
Clear filter
</Button>
</Buttons>
</BarItem>
</BarEnd>
</BarMenu>
</Bar>
<DataGrid TItem="MotorVehicleResponse"
Data="@MotorVehicles"
ReadData="@ReadData"
TotalItems="@MotorVehiclesTotal"
PageSize="@PageSize"
CurrentPage="@Page"
ShowPager="true"
RowClicked="@RowClicked">
<ChildContent>
<DataGridColumn Direction="@ChassisNumberSortDirection" TItem="MotorVehicleResponse" Field="@nameof(MotorVehicleResponse.ChassisNumber)" Caption="VIN"></DataGridColumn>
<DataGridColumn Direction="@BrandSortDirection"TItem="MotorVehicleResponse" Field="@nameof(MotorVehicleResponse.Brand)" Caption="Brand"></DataGridColumn>
<DataGridColumn Direction="@ModelSortDirection"TItem="MotorVehicleResponse" Field="@nameof(MotorVehicleResponse.Model)" Caption="Model"></DataGridColumn>
<DataGridColumn TItem="MotorVehicleResponse" Field="@nameof(MotorVehicleResponse.LicensePlateIdentifier)" Sortable="false" Caption="Active plate">
<DisplayTemplate>
@if (context.LicensePlateIdentifier is not null)
{
<Button Color="Color.Danger" Outline="true" Disabled="true" ReadOnly="true">@context.LicensePlateIdentifier</Button>
}
else
{
<Paragraph>-</Paragraph>
}
</DisplayTemplate>
</DataGridColumn>
<DataGridCheckColumn TItem="MotorVehicleResponse" Field="@nameof(MotorVehicleResponse.Operational)" Sortable="false" Caption="Operational">
<DisplayTemplate>
<Check TValue="bool" Checked="context.Operational" Disabled="true" ReadOnly="true" />
</DisplayTemplate>
</DataGridCheckColumn>
</ChildContent>
<EmptyTemplate>
<Paragraph>No items to be displayed.</Paragraph>
</EmptyTemplate>
<LoadingTemplate>
<Loader />
</LoadingTemplate>
</DataGrid>
<SnackbarStack @ref="SnackbarStack" Location="SnackbarStackLocation.Right" Interval="5000" Multiline="true" /> And it's code using Blazored.LocalStorage;
using Blazorise;
using Blazorise.DataGrid;
using Blazorise.Snackbar;
using FleetManagement.Blazor.Filters;
using FleetManagement.Blazor.Models;
using FleetManagement.Blazor.Queries;
using FleetManagement.Blazor.Responses;
using FleetManagement.Blazor.Services;
using FleetManagement.Blazor.States;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace FleetManagement.Blazor.Pages
{
public partial class Fleet : ComponentBase
{
[Parameter]
public int Page { get; set; } = 1;
[Parameter]
public int PageSize { get; set; } = 10;
private List<MotorVehicleResponse> MotorVehicles { get; set; } = new List<MotorVehicleResponse>();
private List<DataGridColumnInfo> Columns { get; set; }
private MotorVehicleFilter MotorVehicleFilter { get; set; } = new MotorVehicleFilter();
private SnackbarStack SnackbarStack { get; set; }
private SortDirection ChassisNumberSortDirection { get; set; }
private SortDirection ModelSortDirection { get; set; }
private SortDirection BrandSortDirection { get; set; }
private int MotorVehiclesTotal { get; set; }
private bool DataLoading { get; set; } = true;
private bool FilterIsVisible { get; set; } = false;
private bool RetrievingState { get; set; } = false;
private const string _fleetStateLocalStorageKey = "fleetStateKey";
[Inject]
private NavigationManager NavigationManager { get; set; }
[Inject]
private IApiRequestService ApiRequestService { get; set; }
[Inject]
private ILocalStorageService LocalStorage { get; set; }
[Inject]
private ISyncLocalStorageService SynchronousLocalStorage { get; set; }
protected override async Task OnInitializedAsync()
{
var state = SynchronousLocalStorage.GetItem<FleetState>(_fleetStateLocalStorageKey);
if (state is not null)
{
Page = state.Page;
PageSize = state.PageSize;
MotorVehicleFilter = state.Filter;
foreach (var columnState in state.ColumnStates)
{
_ = columnState.Field switch
{
"Brand" => BrandSortDirection = columnState.Direction,
"Model" => ModelSortDirection = columnState.Direction,
"ChassisNumber" => ChassisNumberSortDirection = columnState.Direction,
_ => _ = columnState.Direction
};
}
}
await LocalStorage.RemoveItemAsync(_fleetStateLocalStorageKey);
}
private async Task ReadData(DataGridReadDataEventArgs<MotorVehicleResponse> eventArgs)
{
try
{
DataLoading = true;
Page = eventArgs.Page;
PageSize = eventArgs.PageSize;
Columns = eventArgs.Columns.ToList();
await GetMotorVehicles();
DataLoading = false;
}
catch (HttpRequestException)
{
await SnackbarStack.PushAsync("Something went wrong fetching the data. If this persists, contact the system administrator.", SnackbarColor.Warning);
}
}
private async Task GetMotorVehicles()
{
var query = new MotorVehiclesQuery
{
Page = Page,
PageSize = PageSize,
MotorVehicleFilter = MotorVehicleFilter,
Sortables = GetSortables(Columns).ToList()
};
var content = await ApiRequestService.SendGetRequest<PaginatedResponse<MotorVehicleResponse>>(query);
MotorVehicles = content.Items.ToList();
MotorVehiclesTotal = content.TotalCount;
}
private void FilterVisibilityToggle()
{
FilterIsVisible = !FilterIsVisible;
}
private async Task ClearFilter()
{
MotorVehicleFilter = new MotorVehicleFilter();
await ApplyFilter();
}
private async Task ApplyFilter()
{
Page = 1;
await GetMotorVehicles();
}
private async Task RowClicked(DataGridRowMouseEventArgs<MotorVehicleResponse> e)
{
await LocalStorage.SetItemAsync(_fleetStateLocalStorageKey, new FleetState
{
Filter = MotorVehicleFilter,
Page = Page,
PageSize = PageSize,
FilterIsVisible = FilterIsVisible,
ColumnStates = GetColumnState(Columns).ToList()
});
NavigationManager.NavigateTo($"/fleet/details/{e.Item.ChassisNumber}");
}
private static IEnumerable<ColumnState> GetColumnState(List<DataGridColumnInfo> columns)
{
foreach (var column in columns)
{
yield return new ColumnState
{
Direction = column.Direction,
Field = column.Field
};
}
}
private static IEnumerable<ISortable> GetSortables(List<DataGridColumnInfo> columns)
{
if (columns.Count is not 0)
foreach (var column in columns)
{
if (column.Direction is SortDirection.None)
continue;
yield return new Sortable
{
Descending = IsDescending(column.Direction),
PropertyName = column.Field
};
}
}
private static bool IsDescending(SortDirection direction) => direction switch
{
SortDirection.Descending => true,
SortDirection.Ascending => false,
SortDirection.None => false,
_ => false
};
}
} |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 7 replies
-
Hi, thank you for this very detailed report. If only more people would do this 😅 What version are you using? I'm trying it to reproduce and ReadData is called only once. Recently we changed how ReadData is called when DataGrid is initialized, but I think it is still the problem on 0.9.2.x. |
Beta Was this translation helpful? Give feedback.
Hi, thank you for this very detailed report. If only more people would do this 😅
What version are you using? I'm trying it to reproduce and ReadData is called only once. Recently we changed how ReadData is called when DataGrid is initialized, but I think it is still the problem on 0.9.2.x.