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;
}
Related
My problem is following:
I need to display content dynamically according to a method from a different component than the
the DynamicComponent tag is in.
Is this by design or did I do sth wrong?
My code:
Platform.razor:
<a href="" #onclick:preventDefault #onclick="#(()=>ChangePage("ListLinks"))>
public Type? selectedPage = typeof(Empty);
public void ChangePage(string page)
{
selectedPage = page.Length > 0 ? Type.GetType($"Namespace.Shared.{page}") : null;
}
Home.razor:
Platform platform = new Platform();
<DynamicComponent Type="#platform.selectedPage"></DynamicComponent>
When I call the method inside the same component, everything works but it's not what I need.
This line:
Platform platform = new Platform();
creates a new instance of Platform which is unconnected with the instance created by the Renderer. You do not manage component instances.
To communicate between components you can use a DI service.
Here's some code that demonstrates something like what you want to do:
First a service:
public class PlatformService
{
public Type? Component { get; private set; }
public event EventHandler? ComponentChanged;
public void SetComponent(Type type)
{
this.Component= type;
this.ComponentChanged?.Invoke(this, EventArgs.Empty);
}
}
Registered in Program as scoped so it persists for the lifetime of the user SPA instance.
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddScoped<PlatformService>();
A component to change the component type:
#inject PlatformService service
<h3>ComponentSelector</h3>
<div class="m-2 p-2">
<button class="btn btn-primary" #onclick="() => Change(typeof(Counter))"> Counter </button>
<button class="btn btn-primary" #onclick="() => Change(typeof(FetchData))"> FetchData </button>
</div>
#code {
private void Change(Type type)
=> service.SetComponent(type);
}
And Index to demo it. Note it implements an event handler registered on the service event to drive the UI update.
#page "/"
#inject PlatformService service
#implements IDisposable
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<ComponentSelector />
<SurveyPrompt Title="How is Blazor working for you?" />
#if (service.Component is not null)
{
<DynamicComponent Type="service.Component" />
}
#code {
protected override void OnInitialized()
=> this.service.ComponentChanged += this.OnUpdated;
private void OnUpdated(object? sender, EventArgs e)
=> this.InvokeAsync(StateHasChanged);
public void Dispose()
=> this.service.ComponentChanged -= this.OnUpdated;
}
As you noticed, if you have the method inside the same component it works, and as soon as you put it to the child level, it doesn't. And that is by design.
Let's assume you have the following structure:
Parent.razor
<h1>#Text</h1>
<Child Text="#text">/
#code {
private string Text = "Hello World";
}
And you update text inside your child, the parent component will not see the change:
Child.razor
<button #onclick="() =>Text = string.Empty">Click Me</button>
#code {
[Parameter]
public string Text { get; set; }= "";
}
So clicking on "Click Me" will not change the header in your parent component. The same applies to your example.
Now, how do we make this work? The most straightforward approach would be to have an event, fired when your dynamic component changes, on which the parent component listens. For that, you can also use a event (this special form is a two-way-binding: https://blazor-university.com/components/two-way-binding/)
[Parameter]
public Type SelectedPage { get; set; } = typeof(Empty);
[Parameter]
public EventCallback<Type> SelectedPageChanged { get; set; }
public async Task ChangePage(string page)
{
selectedPage = page.Length > 0 ? Type.GetType($"Namespace.Shared.{page}") : null;
await SelectedPageChanged.InvokeAsync(selectedPage);
}
I am not sure if the following will work with DynamicComponents.
<DynamicComponent #bind-Type="#platform.selectedPage"></DynamicComponent>
If that doesn't work you can fallback to this:
Platform.razor
<DynamicComponent Type="#platform.SelectedPage" Parameters="#dynamicParameters"/>
#code {
private Dictionary<string, object> dynamicComponentParameters = new();
protected override void OnInitialized()
{
dynamicComponentParameters.Add("SelectedPageChanged", EventCallback.Factory.Create<Type>(this, SelectedTypeChanged));
}
private void SelectedTypeChanged(Type newType)
{
// After the method a re-render will be triggered
platForm.SelectedPage = newType;
}
}
I want to pass a CascadingParameter to the Counter page component, but I don't want to use the router. Instead of that, I want to use JavaScript because I want the component to be rendered inside the window of Winbox.js (open-source HTML5 window manager).
The problem is that Cascading Parameters are initialized as null, I think it's happened due to the difference in RenderTree.
Here is my code:
Registration of Counter.razor component for JavaScript in Program.cs:
builder.RootComponents.RegisterForJavaScript<Counter>(identifier: "counter");
For MainLayout.razor
<!-- omitted for brief -->
<CascadingValue Value="_TestCascadingParameter">
<article class="content px-4">
#Body
</article>
</CascadingValue>
<!-- omitted for brief -->
#code{
private TestCascadingParameter _TestCascadingParameter = new() { GlobalCounter = 5 };
public class TestCascadingParameter
{
public int GlobalCounter { get; set; }
}
For Counter page:
#page "/counter"
#using static BlazorApp1.Shared.MainLayout
<PageTitle >Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: #currentCount</p>
<button class="btn btn-primary" #onclick="IncrementCount">Click me</button>
#code {
[CascadingParameter]
public TestCascadingParameter TestCascadingParameter { get; set; }
private int currentCount = 0;
protected override void OnParametersSet()
{
currentCount = TestCascadingParameter.GlobalCounter;
}
private void IncrementCount()
{
currentCount++;
}
}
For Winbox page:
#page "/WinBox"
#layout MainLayout
#inject IJSRuntime _Js
<PageTitle>Win box</PageTitle>
<button #onclick="#OpenWinAsync">Open window</button>
#code{
async Task OpenWinAsync()
{
await _Js.InvokeVoidAsync("MyWinBox.OpenTestWindow");
}
}
And for MyWinBox.js:
window.MyWinBox = {
OpenTestWindow: async () => {
let containerElement = new WinBox("WinBox.js");
await Blazor.rootComponents.add(containerElement.body, 'counter', {});
}
};
When I navigate to Counter page using router everything works fine and the currentCount starts from 5, but when I use JavaScript to render it, it throws a NullReferenceException on TestCascadingParameter.
How to solve this without passing Cascading Parameter value through JavaScript because sometimes the Cascading Parameter has a null value in the caller component as in the above example (eg: if caller component is MudDialog) or sometimes there is more than one Cascading Parameter.
RegisterForJavaScript is new, not well-documented feature and I can't tell if it's possible to pass cascading parameter like that. But I think that you can use State container (or something like Fluxor) to achieve your goals:
public class StateContainer
{
private int? globalCounter;
public int Property
{
get => globalCounter;
set
{
globalCounter = value;
NotifyStateChanged();
}
}
public event Action? OnChange;
private void NotifyStateChanged() => OnChange?.Invoke();
}
More details here: https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-6.0&pivots=webassembly
I have two blazor components the first component calls a method
<CascadingValue Value="this">
#ChildContent
</CascadingValue>
#code{
[Parameter] public RenderFragment? ChildContent { get; set; }
public Debtor? CurrentDebtor { get; private set; }
public async Task<Debtor> ConsolitdateCurrentDebtor(int debtorID)
{
Debtor? currentDebtor = null;
if (DebtorStoreList != null && DebtorStoreList.Count > 0)
{
currentDebtor = DebtorStoreList.Where(d => d.ID == debtorID).Single();
}
else
{
currentDebtor = await _debtorService.GetDebtorInformation();
}
CurrentDebtor = currentDebtor;
// StateHasChanged(); unless called no update takes place
return currentDebtor;
}
}
Now I have a property called CurrentDebtor in this AppState class. One blazor component comes and calls this method and sets the Current Debtor.
From the other component, i am doing something like
#code {
Debtor? debtor;
[CascadingParameter]
AppState AppState{ get; set; }
protected override async Task OnInitializedAsync()
{
debtor = AppState.CurrentDebtor;
}
}
However the CurrentDebtor is coming null, the only way it doesn't come null is if StateHasChanged() is called. Is there anyway without forcing it to call Statehaschanged to populate it correctly?
Trying to "wire" multiple components together using cascading parameters and two way binding isn't always the best way to do things, particularly sub-component to sub-component within a page (which it appears you are trying to do from the information shown in the question).
There is an alternative approach using the standard event model.
My Debtor class
public class Debtor
{
public Guid Id { get; set; }
public string? Name { get; set; }
}
A Debtor view service:
public class DebtorViewService
{
public Debtor Record { get; private set; } = default!;
public event EventHandler<Debtor>? DebtorChanged;
public DebtorViewService()
{
// Get and assign the Debtor Store DI service
}
public async ValueTask GetCurrentDebtor(Guid debtor)
{
// Emulate getting the Debtor from the Debtor Service
await Task.Delay(1000);
this.Record = new Debtor() { Name=$"Fred {DateTime.Now.ToLongTimeString()}", Id = Guid.NewGuid()};
this.DebtorChanged?.Invoke(this, this.Record);
}
}
Registered in services like this:
builder.Services.AddScoped<DebtorViewService>();
A Component to get the Debtor:
GetDebtor.razor
#inject DebtorViewService Service
<div class="m-2 p-3 bg-dark text-white">
<h5>GetDebtor Component</h5>
<div>
<button class="btn btn-secondary" #onclick=this.GetCurrentDebtor>Get Debtor</button>
</div>
</div>
#code {
private async Task GetCurrentDebtor()
=> await Service.GetCurrentDebtor(Guid.Empty);
}
A component to show the Debtor:
ShowDebtor.razor
#inject DebtorViewService Service
#implements IDisposable
<div class="m-2 p-3 bg-info">
<h5>ShowDebtor Component</h5>
#if (this.Service.Record is not null)
{
<div>
Id : #this.Service.Record.Id
</div>
<div>
Name : #this.Service.Record.Name
</div>
}
</div>
#code {
protected override void OnInitialized()
=> this.Service.DebtorChanged += this.OnDebtorChanged;
private void OnDebtorChanged(object? sender, Debtor value)
=> this.InvokeAsync(this.StateHasChanged);
public void Dispose()
=> this.Service.DebtorChanged -= this.OnDebtorChanged;
}
And a demo page:
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<GetDebtor />
<ShowDebtor />
<SurveyPrompt Title="How is Blazor working for you?" />
#code{
}
The data and the change event is now in a single shared (Scoped) DI service instance that everyone has access to.
You should be able to build your components based on this code.
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.
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;
}
}