So I've been playing around with Blazor WebAssembly and I can't figure out how to solve this problem. Basically, I have a NavMenu.razor page that will get an array of NavMenuItem asynchronously from a JSON file. That works fine. Now, what I'm trying to do is add an event listener to each of the anchors that are generated from the <NavLink> tags in the below foreach loop.
The NavLink outside of that loop (the one not populated by the async function) successfully has the addSmoothScrollingToNavLinks() function applied to it correctly. The other NavLinks do not. It seems as if they are not yet in the DOM.
I'm guessing there's some weird race condition, but I don't know how to resolve it. Any ideas on how I might fix this?
NavMenu.razor
#inject HttpClient Http
#inject IJSRuntime jsRuntime
#if (navMenuItems == null)
{
<div></div>
}
else
{
#foreach (var navMenuItem in navMenuItems)
{
<NavLink class="nav-link" href="#navMenuItem.URL" Match="NavLinkMatch.All">
<div class="d-flex flex-column align-items-center">
<div>
<i class="#navMenuItem.CssClass fa-2x"></i>
</div>
<div class="mt-1">
<span>#navMenuItem.Text</span>
</div>
</div>
</NavLink>
}
}
<NavLink class="nav-link" href="#" Match="NavLinkMatch.All">
<div class="d-flex flex-column align-items-center">
This works!
</div>
</NavLink>
#code {
private NavMenuItem[] navMenuItems;
protected override async Task OnInitializedAsync()
{
navMenuItems = await Http.GetJsonAsync<NavMenuItem[]>("sample-data/navmenuitems.json");
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await jsRuntime.InvokeVoidAsync("MyFunctions.addSmoothScrollingToNavLinks");
}
}
public class NavMenuItem
{
public string Text { get; set; }
public string URL { get; set; }
public string CssClass { get; set; }
}
}
index.html (underneath the webassembly.js script tag)
<script>
function scrollToTop() {
window.scroll({
behavior: 'smooth',
left: 0,
top: 0
});
}
window.MyFunctions = {
addSmoothScrollingToNavLinks: function () {
let links = document.querySelectorAll("a.nav-link");
console.log(links);
// links only has 1 item in it here. The one not generated from the async method
for (const link of links) {
link.addEventListener('click', function (event) {
event.preventDefault();
scrollToTop();
})
}
}
}
</script>
That's because Blazor will NOT wait for the OnInitializedAsync() to complete and will start rendering the view once the OnInitializedAsync has started. See source code on GitHub:
private async Task RunInitAndSetParametersAsync()
{
OnInitialized();
var task = OnInitializedAsync(); // NO await here!
if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
{
// Call state has changed here so that we render after the sync part of OnInitAsync has run
// and wait for it to finish before we continue. If no async work has been done yet, we want
// to defer calling StateHasChanged up until the first bit of async code happens or until
// the end. Additionally, we want to avoid calling StateHasChanged if no
// async work is to be performed.
StateHasChanged(); // Notify here! (happens before await)
try
{
await task; // await the OnInitializedAsync to complete!
}
...
As you see, the StateHasChanged() might start before OnInitializedAsync() is completed. Since you send a HTTP request within the OnInitializedAsync() method, , the component renders before you get the response from the sample-data/navmenuitems.json endpoint.
How to fix
You could bind event in Blazor(instead of js), and trigger the handlers written by JavaScript. For example, if you're using ASP.NET Core 3.1.x (won't work for 3.0, see PR#14509):
#foreach (var navMenuItem in navMenuItems)
{
<a class="nav-link" href="#navMenuItem.URL" #onclick="OnClickNavLink" #onclick:preventDefault>
<div class="d-flex flex-column align-items-center">
<div>
<i class="#navMenuItem.CssClass fa-2x"></i>
</div>
<div class="mt-1">
<span>#navMenuItem.Text</span>
</div>
</div>
</a>
}
#code{
...
protected override async Task OnAfterRenderAsync(bool firstRender)
{
//if (firstRender)
//{
// await jsRuntime.InvokeVoidAsync("MyFunctions.addSmoothScrollingToNavLinks");
//}
}
private async Task OnClickNavLink(MouseEventArgs e)
{
Console.WriteLine("click link!");
await jsRuntime.InvokeVoidAsync("MyFunctions.smoothScrolling");
}
}
where the MyFunctions.addSmoothScrollingToNavLinks is:
smoothScrolling:function(){
scrollToTop();
}
I am also trying to get this pattern right, call some async services in OnInitializedAsync and after that, calling some javascript on the OnAfterRenderAsync. The thing is that the javascript call is there to execute some js that depends on the data returnes by the services (the data must be there to the desired behavior take effect).
What is happening is that the js call is running before the data arrive, is there a way to implement this "dependency" between data from OnInitializedAsync and OnAfterRenderAsync?
Not perfect but works for me,
lets you wait for the download
private bool load = false;
protected override async Task OnInitializedAsync()
{
navMenuItems = await Http.GetJsonAsync<NavMenuItem[]>("sample-data/navmenuitems.json");
load = true;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (load)
{
await jsRuntime.InvokeVoidAsync("MyFunctions.addSmoothScrollingToNavLinks");
load = false;
}
}
Related
I am trying to get a child component to update a list on the parent. To do this I setup an EventCallback that takes a list and sends it to the parent. The issue is the event never fires and the HasDelegate variable on the callback is false.
Parent .razor.cs:
public async Task UpdateSelectedCompanies(List<CompanyStub> companies)
{
_selectedCompanies = companies;
await InvokeAsync(StateHasChanged);
}
Parent .razor:
<CompanyTable IncludeCheckbox="true" UpdateCompanies="#UpdateSelectedCompanies"></CompanyTable>
Child .razor.cs:
[Parameter] public EventCallback<List<CompanyStub>> UpdateCompanies { get; set; }
private async Task CheckboxRowSelectHandler(RowSelectEventArgs<CompanyStub> args)
{
SelectedCompanies.Add(args.Data);
await UpdateCompanies.InvokeAsync(SelectedCompanies);
}
CheckboxRowSelectHandler does get called, but the UpdateCompanies event never fires.
I am expecting for the event to be fired, but the event never gets fired.
Here's a simplified version of your code that works. Use it to test your child/parent.
One thing to check: Is your razor.cs file correctly linked to the razor component file?
ChildComponent.razor
<div class="m-2 p-2 bg-light">
<h3>ChildComponent</h3>
<button class="btn btn-primary" #onclick=this.OnClick>Click me</button>
</div>
#code {
[Parameter] public EventCallback<string> ValueChanged { get; set; }
private async Task OnClick()
=> await this.ValueChanged.InvokeAsync($"Set at {DateTime.Now.ToLongTimeString()}");
}
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<ChildComponent ValueChanged="#this.OnClicked" />
#if (!string.IsNullOrWhiteSpace(this.message))
{
<div class="alert alert-info">
#message
</div>
}
#code {
private string message = string.Empty;
// No call to StateHasChanged required
// The ComponentBase UI handler does it
private void OnClicked(string value)
=> message = value;
}
When i execute many task blazor ui freezes until all the tasks are completed.
I dont know how to limit for example the number of task or somehing in blazor to keep showing ui eventhough tasks keeps running in background.
I think this problem is more a asyncrhonous rather than blazor.
Heres my scenario:
I have a simple class,
public class Demo
{
public int Id;
public bool WorkDone { get; set; }
public Demo(int id)
{
this.WorkDone = false;
this.Id = id;
}
//some async job that takes time
public async Task Work()
{
await Task.Run(async () =>
{
Random r = new();
Thread.Sleep(1000 * r.Next(1, 10));
this.WorkDone = true;
});
}
}
So my main objective is to show a list of this demo objects, but this list has to be updated with respective object values when the values area updated.
So i did a simple table to show object values, and after the job is done i called statehaschanged async to forze to update the ui:
#page "/"
<button #onclick=DoWork>Do Work</button>
<table>
<tr>
<th>id</th>
<th>state</th>
</tr>
#foreach(var item in this.data)
{
<tr>
<td>#item.Id</td>
<td>#item.WorkDone</td>
</tr>
}
</table>
#if (allworkdone)
{
<div class="alert alert-success fade-in" role="alert">
Work Done!
</div>
}
#code {
int max = 10;
List<Demo> data= new();
bool allworkdone = false;
protected override void OnInitialized()
{
foreach (var item in Enumerable.Range(0, max))
{
this.data.Add(new Demo(item));
}
}
public void DoWork()
{
List<Task> tasks = new ();
data.AsParallel().ForAll(async x=>
{
await x.Work();
await InvokeAsync(StateHasChanged);
});
allworkdone = true;
StateHasChanged();
}
So far so good , but when i add more task( change max to 10000 for example), the ui locks and shows the following:
I think the cause is that all "threads" are consumed so the ui cant be refreshed till all the job is done.
Because after the job is done, the ui "comes back"
What are the ways to prevent this effect?
Working on blazor server, net6.
Thanks!
I have a button and a message that should be shown if condition is true.
<button #onclick="Helper.CallFunc">Call async func</button>
#if(Helper.Loading)
{
#("Loading...")
}
LoadingHelper class look like this
public class LoadingHelper
{
public bool Loading { get; private set; } = false;
public Func<Task> PassedFunc { private get; set; } = async () => await Task.Delay(2000);
public Func<Task> CallFunc => async () =>
{
Loading = true;
await PassedFunc();
Loading = false;
};
}
When I define Helper object like this the message is indeed shown for 2000 miliseconds
LoadingHelper Helper { get; set; } = new()
{
PassedFunc = async () => await Task.Delay(2000)
};
However when it's defined like this the message is never shown
LoadingHelper Helper => new()
{
PassedFunc = async () => await Task.Delay(2000)
};
I'm a little bit confused here as to why the Loading change is not shown in second example. Shouldn't the change be visible regardless if the Helper object is set with getter and setter or only getter since I'm not modifying the Loading property directly?
Edit
When it's defined like this for some reason it works
LoadingHelper Helper { get; } = new()
{
PassedFunc = async () => await Task.Delay(2000)
};
Building an anonymous function every time you invoke the Func is expensive.
public Func<Task> CallFunc => async () =>
{
Loading = true;
await PassedFunc();
Loading = false;
};
You can cache the function like this:
public Func<Task> CallFunc;
public LoadingHelper()
{
CallFunc = async () =>
{
Loading = true;
await PassedFunc();
Loading = false;
};
}
If you want to apply the "loader" to all UI events in a component you can create a custom IHandleEvent.HandleEventAsync that overloads the standard ComponentBase implementation. Here's a page that demonstrates how to implement one. This only updates loading if it's a MouseEventArgs event.
#page "/"
#implements IHandleEvent
<h3>Loader Demo</h3>
<div class="m-2">
<button class="btn btn-primary" #onclick=HandleClickAsync>Call async func</button>
<button class="btn btn-dark" #onclick=HandleClickAsync>Call async func</button>
</div>
<div class="m-2">
<input type="checkbox" #onchange=HandleCheckAsync />
</div>
#if (this.Loading)
{
<div class="alert alert-warning">Loading... </div>
}
#code {
protected bool Loading;
private async Task HandleClickAsync()
=> await Task.Delay(2000);
private async Task HandleCheckAsync()
=> await Task.Delay(2000);
async Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
if (arg is MouseEventArgs)
Loading = true;
var task = callback.InvokeAsync(arg);
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
StateHasChanged();
await task;
if (arg is MouseEventArgs)
Loading = false;
StateHasChanged();
}
}
I should've had researched a bit more before asking a question. The answer is pretty simple as explained here.
Basically LoadingHelper Helper => new() returned a new object which obviously had Loading set to false.
LoadingHelper Helper { get; } = new() on the other hand returned the same object every time.
I have a hosted Blazor WebAssembly app that I'm forcing users to sign in at start up. I have put this logic in the MainLayout.razor page like so:
protected override async Task OnInitializedAsync()
{
var user = (await AuthenticationStateTask).User;
if (user.Identity.IsAuthenticated)
{
await SetUserInfo(user);
}
else
{
AuthMessage = "User is NOT authenticated";
NavigationManager.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}");
}
}
What I want to do is call the SetUserInfo method after the user has logged in successfully (at the moment it will always navigate to the log in page when starting the app), but can't figure out how to do it. It seems that the OnInitializedAsync method only runs once, unless you manually refresh the page. Any ideas? Thanks in advance.
Figured it out. Need to use OnParametersSetAsync() in the component.
[CascadingParameter] protected Task<AuthenticationState> AuthenticationStateTask { get; set; }
protected override async Task OnParametersSetAsync()
{
var user = (await AuthenticationStateTask).User;
if (user.Identity.IsAuthenticated)
{
await SetUserInfo(user);
}
}
Use OnAfterRenderAsync
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var authenticationState = await authenticationStateTask;
if (authenticationState.User.Identity.IsAuthenticated)
{
}
}
}
on LoginDisplay component
#code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private string _authMessage;
private async Task LogUsername()
{
var authState = await authenticationStateTask;
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
_authMessage = $"{user.Identity.Name} is authenticated.";
wait SetUserInfo(user);
}
}
}
I think the cleanest solution is this:
Add a Blazor Component, e.g. "CallMe" (shortened Code)
#using Microsoft.AspNetCore.Components.Authorization
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
#inject IDataService DataService
...
#code{
[CascadingParameter] protected Task<AuthenticationState> AuthState { get; set; } = null!;
protected override async Task OnInitializedAsync()
{
var user = (await AuthState).User;
if (user.Identity.IsAuthenticated)
{
var email = user.FindFirst("preferred_username").Value;
var user = await DataService.GetUserAsync(email);
}
}
}
and 'call' it in LoginDisplay.razor -> Authorized Context.
<AuthorizeView>
<Authorized>
#context.User.Identity?.Name!
<CallMe></CallMe>
<button class="nav-link btn btn-link" #onclick="BeginLogout">Log out</button>
</Authorized>
<NotAuthorized>
Log in
</NotAuthorized>
</AuthorizeView>
Context
I'm following a pattern that's something like https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/ or https://jonhilton.net/blazor-state-management/
So I have two razor components Hen.razor and Basket.razor as child components inside index.razor. A button inside Hen adds to the number of eggs displayed inside Basket.
The button calls a service I've injected called EggService that handles the number of eggs in Basket by storing it in local storage using Blazored.LocalStorage.
Problem
Clicking the button increases the number of eggs in local storage but doesn't update the Basket component unless I refresh.
Code
Repository for convenience: https://github.com/EducatedStrikeCart/EggTest/
EggService:
using Blazored.LocalStorage;
namespace BlazorSandbox.Services
{
public class EggService
{
public event Action OnChange;
private readonly ILocalStorageService _localStorageService;
public EggService(ILocalStorageService localStorageService)
{
this._localStorageService = localStorageService;
}
public async Task<int> GetEggs()
{
int currentEggs = await _localStorageService.GetItemAsync<int>("Eggs");
return currentEggs;
}
public async Task AddEgg()
{
int newEggs = await GetEggs();
if (newEggs == null)
{
newEggs = 0;
} else
{
newEggs += 1;
}
await _localStorageService.SetItemAsync("Eggs", newEggs);
OnChange?.Invoke();
}
}
}
Hen:
#using BlazorSandbox.Services
#inject EggService EggService
<div>
<h3>Hen</h3>
<button #onclick="TakeAnEgg">Take an egg</button>
</div>
#code {
public async Task TakeAnEgg()
{
await EggService.AddEgg();
}
}
Egg:
#using BlazorSandbox.Services
#inject EggService EggService
#implements IDisposable
<div>
<h3>Basket</h3>
Eggs: #Eggs
</div>
#code {
public int Eggs { get; set; }
protected override async Task OnInitializedAsync()
{
Eggs = await EggService.GetEggs();
EggService.OnChange += StateHasChanged;
}
public void Dispose()
{
EggService.OnChange -= StateHasChanged;
}
}
Index:
#page "/"
#using BlazorSandbox.Services
#inject EggService EggService
<h1>
Eggs!
</h1>
<div class="d-flex flex-row justify-content-around">
<Hen />
<Basket />
</div>
#code {
}
Program.cs:
using BlazorSandbox;
using BlazorSandbox.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Blazored.LocalStorage;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<EggService>();
builder.Services.AddBlazoredLocalStorage();
await builder.Build().RunAsync();
Solution
Special thank you to person who deleted their comment. I'm kind of new to asking questions on StackOverflow so I'm sorry if I should've selected your answer as the Answer!
#code {
public int Eggs { get; set; }
protected override void OnInitialized()
{
//Subscribe
EggService.OnChange += UpdateEgg;
//Set value of Eggs on load
UpdateEgg();
}
public void UpdateEgg()
{
// Set value of Eggs to new value and trigger component rerender
InvokeAsync(async () => {Eggs = await EggService.GetEggs(); StateHasChanged(); });
}
public void Dispose()
{
// Unsubscribe
EggService.OnChange -= UpdateEgg;
}
}
There are a few oddities in your code.
if (newEggs == null)
This is an int, so it can never be null. The default value for int is 0. You should be seeing a warning for this.
Eggs = await EggService.GetEggs();
After you set Eggs here, you never update it anywhere in your code! So even if you call StateHasChanged, there is nothing to update.
What you will want to do is keep track of the egg count inside of your EggService and then inside of your Basket component you will need a way to know that the egg count has increased so you can update your Egg property and then call StateHasChanged. Let me know if you need help with this.