Issue updating MudBlazor component in OnAfterRenderAsync - blazor-server-side

I'm trying to recover some data asynchronously from localStorage then activate a specific tab from MudTabs component if data is found.
<MudTabs Elevation="2" Rounded="true" Centered="true" Color="Color.Primary" ApplyEffectsToContainer="true" Class="custom-tab-panel" PanelClass="pa-6 bg-white" #ref="tabs">
The problems come from my method OnAfterRenderAsync who doesn't work updating the MudBlazor component state, here is the code:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var result = await protectedSessionStore.GetAsync<int>("Call");
if (result.Success)
{
tabs.ActivatePanel(1);
StateHasChanged();
}
}
//return base.OnAfterRenderAsync(firstRender);
}
I tested a previous version of it who worked pretty well but i cannot use the await operator inside of it, the method not being asynchronously:
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
tabs.ActivatePanel(1);
}
return base.OnAfterRenderAsync(firstRender);
}
I didn't found what is specifically the difference between returning the base method and calling the StateHasChanged. When using the StateHasChanged method the active panel remain on the 0 index.
If you have any ideas, thanks in advance.

Related

how i know blazor OnInitializedAsync exec in once or twice

I want get data from db once on OnInitializedAsync. I try to use tableLoading to judue,but it's not work.
protected override async Task OnInitializedAsync()
{
if (tableLoading)
{
return;
}
tableLoading = true;
users = await userService.GetSome(1, userType);
_total = await userService.GetCount(userType);
tableLoading = false;
Console.WriteLine("OnInitializedAsync");
}
This is the official way to solve your problem. You have to persist component state during first load so that your services won't be called second time during second load.
First add <persist-component-state /> tag helper inside your apps body:
<body>
...
<persist-component-state />
</body>
Then inject PersistentComponentState in your component and use like this:
#implements IDisposable
#inject PersistentComponentState ApplicationState
#code {
private IEnumerable<User> _users;
private int _total;
private PersistingComponentStateSubscription _persistingSubscription;
protected override async Task OnInitializedAsync()
{
_persistingSubscription =
ApplicationState.RegisterOnPersisting(PersistState);
if (!ApplicationState.TryTakeFromJson<IEnumerable<User>>("users", out var restoredUsers))
{
_users = await userService.GetSome(1, userType);
}
else
{
_users = restoredUsers;
}
if (!ApplicationState.TryTakeFromJson<int>("total", out var restoredTotal))
{
_total = await userService.GetCount(userType);
}
else
{
_total = restoredTotal;
}
}
private Task PersistState()
{
ApplicationState.PersistAsJson("users", _users);
ApplicationState.PersistAsJson("total", _total);
return Task.CompletedTask;
}
void IDisposable.Dispose()
{
_persistingSubscription.Dispose();
}
}
How i know blazor OnInitializedAsync exec in once or twice?
It usually loads twice.
Once when the component is initially rendered statically as part of the page.
A second time when the browser renders the component.
However, If you want to load it once, in that case, you could go to _Host.cshtml and change render-mode="ServerPrerendered" to render-mode="Server", and it would be called only once as a result it would then load your data from the database once only.
Note: For more information you could refer to the official documents here.
I know it's usually loads twice, i want to know when the function is run, how to konw it's run on once or twice. This is my solution.
static bool first = true;
protected override async Task OnInitializedAsync()
{
if (first)
{
first = false;
Console.WriteLine("first time");
return;
}
Console.WriteLine("second time");
}

Unexpected behaviour when rendering Blazorise charts

I am working on investment tracking app that will be free for everyone. I am using net Core with blazor and Blazorise for charts.
I stumbled upon a problem with rendering the charts. From the official Blazorise documentation I added method protected override async Task OnAfterRenderAsync(bool firstRender) (see in the code below). This method should redraw the charts the first time the page renders. The problem is that this method always fires twice. It renders the charts in the first go and the second time it leaves them empty (as firstRender = false the second time it fires). If I remove the if block the charts render ok.
Furthermore I've added button that should refresh the data + charts. After pressing this button the charts refresh twice (this is unwanted behaviour as it distracts the users) and what is interesting the data itself (the values) change after the second go.
Have anybody dealt with this problem before?
My html code
...
<div class="btn" #onclick="(async () => await RerenderPage())">Refresh Data</div>
...
My code
List<Models.VM.OverView> overview = new List<Models.VM.OverView>();
protected override async Task OnInitializedAsync()
{
overview = await GetOverview(); //gets overview from api
}
public async Task<List<Models.VM.OverView>> GetOverview()
{
return await Http.GetFromJsonAsync<List<Models.VM.OverView>>("/api/Overview/GetOverView/" + await GetUserIdAsync);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await HandleRedraw();
}
}
async Task HandleRedraw()
{
await pieChart.Clear();
//this method goes in the overview object and gets data from it
await pieChart.AddLabelsDatasetsAndUpdate(GetLabelsPieChart(), GetPieChartDataset());
}
What is the await Rerenderpage() doing ?
From the samples it looks like it employs the user of a bool isAlreadyInitialised flag.
protected override async Task OnAfterRenderAsync( bool firstRender )
{
if ( !isAlreadyInitialised )
{
isAlreadyInitialised = true;
await HandleRedraw();
}
}
I am assuming that async () => await RerenderPage()) calls something that then calls the chart.Update in order for the component to know its statuschanged ?

Delay a task in Blazor without blocking the UI

I created a .razor Notification Component in Blazor, and I'm trying to autoclose the notification div after xx seconds.
So far it works with this Method
private async Task CloseToast(Guid Id, bool autoclose = false)
{
if (autoclose)
{
await Task.Delay(TimeSpan.FromSeconds(5));
}
//Code to remove the notification from list
StateHasChanged();
}
The problem is that for 5 seconds the UI data binding is stuck, any one way or two way binding update to variables (text fields etc..) is on hold until the Notification is closed and the Task resumes.
How can I launch a method or code block after xx seconds without blocking the main UI task in Blazor?
A component with a timer that counts back
<h3>#Time</h3>
#code {
[Parameter] public int Time { get; set; } = 5;
public async void StartTimerAsync()
{
while (Time > 0)
{
Time--;
StateHasChanged();
await Task.Delay(1000);
}
}
protected override void OnInitialized()
=> StartTimerAsync();
}
Usage:
<Component />
<Component Time="7"/>
Tested on client side Blazor. Should behave the same way in server-side Blazor.
Hope this helps
You can use .NET Timer from System.Timers as well and set the Delay in milisec. When it elapsed event will triggered and you can put your logic into the event handler. If you don't want to bother with all the config and Disposing of Timer you can use this Nuget package. It is a very convenient wrapper for the Timer with many extra features see docs.
<AdvancedTimer Occurring="Times.Once()" IntervalInMilisec="#_closeInMs" AutoStart="true" OnIntervalElapsed="#(e => { IsVisible = false; })" />
#code {
private int _closeInMs = 5000;
...
}
The official Blazor Server EFCore sample project includes this as an example, in TextFilter.razor. The essence of the code is:
Timer? timer;
// ... code in a function to start the timer
timer?.Dispose();
timer = new(DebounceMs);
timer.Elapsed += NotifyTimerElapsed;
timer.Enabled = true;
private async void NotifyTimerElapsed(object? sender, ElapsedEventArgs e)
{
timer?.Dispose();
timer = null;
// SomeMethodAsync will need to call StateHasChanged()
InvokeAsync(() => SomeMethodAsync());
}
and a Dispose() function for the page to dispose any timer in progress when user navigates away.

How can I change css directly(without variable) in Blazor?

I am using the server-side of Blazor.
I want to change the CSS of the body.
In Jquery I can write the code like this easily:
$("body").css("overflow-y","hidden");
However, with this tutorial(Blazor Change Validation default css class names) said, it seems I can only change the CSS by changing the class name.
It is so complex while crossing the component, especially the body is at the top of all the components.
I wonder whether there is a way can changes CSS directly in Blazor. Thank you.
There are several ways of getting out of the "blazor way" of doing things and accomplishing css modification of an element.
Simplest: Just like you can use the class attribute, use the style attribute
<element style=#myStyle></element>
#code {
string myStyle;
void MyMethod() {
myStyle="overflow-y: hidden;"
}
}
Advanced: Use JS interop
a. In the main view (index.html or Pages/_Host.cshtml depending on project type), create a js endpoint for your component
<script>
window.applyStyleForElement = function(styleOp) {
document.getElementById(styleOp.id).style[styleOp.attrib] = styleOp.value;
}
</script>
b. In razor file:
#Inject IJRRuntime JSRuntime
<element id=#myId></element>
#code {
string myId = Guid.NewGuid().ToString("n");
async Task MyMethod() {
await JSRuntime.InvokeAsync("applyStyleForElement",
new { id = myId, attrib = "overflowY", value = "hidden" });
}
}
Finally, applying to your special case with body element ("advanced" method above).
a. In the main view (index.html or Pages/_Host.cshtml depending on project type), create a js endpoint
<script>
window.applyStyleForBody = function(style) {
document.body.style[style.attrib] = style.value;
}
</script>
b. In razor file:
#Inject IJRRuntime JSRuntime
(...)
#code {
async Task MyMethod() {
await JSRuntime.InvokeAsync("applyStyleForBody",
new { attrib = "overflowY", value = "hidden" });
}
}
Well, Blazor does not support direct css modification yet, since Web Assembly doesn't. Anyway heads up, it is on the road-map for Web Assembly/Blazor.
Therefor your best bet is, changing the class name with variables. At least for now.
I'M NOT SURE IT'S THE RECOMMENDED WAY BUT it works!
For one of my project, I include a in the page html itself, using params :
<style>
html
{
background-color:#_ColorCss;
}
</style>
//html stuff here
#code
{
public string Color{ get; set; } = "white";
string _ColorCss => $"{Color}"; //use this in case of formatting (ex : add 'px' or that kind of things)
//code stuff here
}
not the very sexiest way but it works
have fun !
Well, actually there is a way to do that and it works really good (it might suffer a little delay though).
I know this answer is a little bit late but it might help other people who face the same challenge.
We need to create some JS code that includes the wanted files:
function includeLeftStyle() {
appendStyle("left.css");
}
function includeRightStyle() {
appendStyle("right.css");
}
function appendStyle(path) {
var element = document.createElement("link");
element.setAttribute("rel", "stylesheet");
element.setAttribute("type", "text/css");
element.setAttribute("href", path);
document.getElementsByTagName("head")[0].appendChild(element);
}
The wished CSS can be called according to the language (any other coditions) in the MainLayout:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if (// is left)
{
await JSRuntime.InvokeAsync<object>("includeLeftStyle");
}
else
{
await JSRuntime.InvokeAsync<object>("includeRightStyle");
}
}
}
Happy coding! :)
Beginnig from the #Tewr answer, we can also change the whole class:
a) In the main view (index.html or Pages/_Host.cshtml depending on project type), create a js endpoint
<script>
window.applyStyleForElement = function (styleOp) {
if (styleOp != null) {
document.getElementById(styleOp.id).className = styleOp.value;
}
}
</script>
b) Then in the razor file
async Task MyMethod(string sortColumn)
{
await JsRuntime.InvokeVoidAsync("applyStyleForElement",
new { id = sortColumn, value = "newClassName" });
}

ASP.NET Core: global handling for showing exception message in current page

In my application I have a set of errors that are "expected" and thus they shouldn't redirect the user to a full error page when they occur. For example, I would like to show the error message in a red box above the current page.
I can easily catch an error on a model action and show the message on the current view, but I was wondering how to do it globally.
I tried a custom middleware with TempData and a filter with ModelState, but can't wrap my head around how to actually pass the error data back to the current page.
This works for a single model (setting the error data in TempData):
public async Task<IActionResult> OnPost() {
try {
// methodThatThrows
}
catch (ApplicationError e) {
TempData["Error"] = e.Message;
return RedirectToPage("Current_Page");
}
return RedirectToPage("Other_Page");
}
For some reason, this doesn't work (in a global middleware), as TempData is empty when the redirect completes. Also setting the middleware doesn't really work with showing the other, more critical errors in the normal error page:
public class ApplicationErrorMiddleware {
private readonly RequestDelegate _next;
private readonly ITempDataDictionaryFactory _tempFactory;
public ApplicationErrorMiddleware(RequestDelegate next, ITempDataDictionaryFactory tempFactory) {
_next = next;
_tempFactory = tempFactory;
}
public async Task InvokeAsync(HttpContext httpContext) {
try {
await _next(httpContext);
}
catch (ApplicationError ex) {
HandleError(httpContext, ex);
}
}
private void HandleError(HttpContext context, ApplicationError error) {
var tempData = _tempFactory.GetTempData(context);
tempData.Add("Error", error.Message);
context.Response.Redirect(context.Request.Path);
}
}
By the tip of #hiiru, I went through a wild goose chase through the configuration options to find a working solution.
My issue was a missing call from the middleware HandleError-method:
private void HandleError(HttpContext context, ApplicationError error) {
var tempData = _tempFactory.GetTempData(context);
tempData.Add("Error", error.Message);
tempData.Save(); // this call was missing
context.Response.Redirect(context.Request.Path);
}
After popping that in there, the tempdata is sent with the redirection back to the original page. Note that this is using the default cookie-based temp data, so no specific configuration is needed.
Now, this works, but it might not be the best way to do this.