I have these two components in my Blazor app:
Component1.razor:
<CascadingValue Value=this>
<Component2/>
</CascadingValue>
<p>#DisplayedText</p>
#code {
public string DisplayedText = string.Empty;
}
Component2.razor:
<button #onclick=#(e => { C1.DisplayedText = "testing"; })>Set DisplayedText</button>
#code {
[CascadingParameter]
public Component1 C1 { get; set; }
}
When I click the "Set DisplayedText" button, the text in the p element in Component1 should change to testing, but it does not. How can I fix this?
Merlin04, the following code snippet demonstrate how you can do it. Note that this is really a very simple sample, but it shows how you should code when communication between distant components is required.
Here's the code, copy and run it, and if you have more questions don't hesitate to ask.
MessageService.cs
public class MessageService
{
private string message;
public string Message
{
get => message;
set
{
if (message != value)
{
message = value;
if (Notify != null)
{
Notify?.Invoke();
}
}
}
}
public event Action Notify;
}
Note: The service is a normal class... It provides services to other objects, and it should be added to the DI container in Startup.ConfigureServices method to make it available to requesting clients. Add this: to the ConfigureServices method:
services.AddScoped<MessageService>();
Note: As you can see I define an event delegate of the Action type, which is invoked from the property's set accessor, when the user type text into a text box in Component3. Triggering this delegate causes the text entered by Components3 to be displayed in the Index component which is the parent of Component2 (see code below).
Index.razor
#page "/"
#inject MessageService MessageService
#implements IDisposable
<p>I'm the parent of Component2. I've got a message from my grand child:
#MessageService.Message</p>
<Component2 />
#code {
protected override void OnInitialized()
{
MessageService.Notify += OnNotify;
}
public void OnNotify()
{
InvokeAsync(() =>
{
StateHasChanged();
});
}
public void Dispose()
{
MessageService.Notify -= OnNotify;
}
}
Note that we directly bind to the MessageService.Message property, but the StateHasChanged method must be called to refresh the display of the text.
Component2.razor
<h3>Component2: I'm the parent of component three</h3>
<Component3/>
#code {
}
Component3.razor
#inject MessageService MessageService
<p>This is component3. Please type a message to my grand parent</p>
<input placeholder="Type a message to grandpa..." type="text"
#bind="#MessageService.Message" #bind:event="oninput" />
Note that in Component3 we bind the MessageService.Message to a text box, and the binding occurs each time you press a key board( input event versus change
event).
That is all, hope this helps, and don't hesitate to ask any question.
#Merlin04, this is an abuse and misuse of the cascading value feature. Ordinarily, a parent component communicates with its child via component parameters.
You can't update a cascaded value from a descendant.
Wrong...
The following code snippet demonstrate a better solution, based on what you do, though it is not optimal because your code and mine as well update a property from a method, when in matter of fact, we should changed the property's value directly, and not through a mediator code (that is a method)
Component2.razor
<button #onclick="#(() => SetDisplayedText.InvokeAsync("testing"))">Set
DisplayedText</button>
#code {
[Parameter]
public EventCallback<string> SetDisplayedText { get; set; }
}
Component1.razor
<Component2 SetDisplayedText="#SetDisplayedText"/>
<p>#DisplayedText</p>
#code {
private string DisplayedText = string.Empty;
public void SetDisplayedText(string newText)
{
DisplayedText = newText;
}
}
Note that calling the StateHasChanged method is not necessary, as this is the bonus you get when using the EventCallback 'delegate'
Hope this helps...
See enet's answer instead of this
You can't update a cascaded value from a descendant.
Instead, make a method in the parent (Component1) to set the value:
public void SetDisplayedText(string newText) {
DisplayedText = newText;
StateHasChanged();
}
Then, you can call that in the descendant:
<button #onclick=#(e => { C1.SetDisplayedText("testing"); })>Set DisplayedText</button>
Related
I have a project in which I frequently use OnAfterRender() to call methods on child components. As a simple example, suppose I have two Blazor components, Counter.razor and CounterParent.razor:
Counter:
#page "/counter"
<p role="status">Current count: #currentCount</p>
<button class="btn btn-primary" #onclick="IncrementCount">Click me</button>
#code {
private int currentCount = 0;
// This method is private out of the box,
// but let's change it to public for this example
public void IncrementCount()
{
currentCount++;
StateHasChanged();
}
}
CounterParent:
#page "/counterParent"
<h3>Counter Parent</h3>
<Counter #ref="counterChild"></Counter>
#code {
private Counter counterChild;
private bool loaded;
protected override void OnAfterRender(bool firstRender)
{
if (!loaded && counterChild != null)
{
counterChild.IncrementCount(); // Do some operation on the child component
loaded = true;
}
}
}
The parent component in this example causes the child component (the counter) to be incremented by 1.
However, I have recently been advised that this is not a good practice, and that
On OnAfterRender{async} usage, it should (in general) only be used to do JS interop stuff.
So if this is poor practice what is the best practice for calling methods on child components when a page loads? Why might someone choose to
actually either disable it on ComponentBase based components, , or don't implement it on [their] own components?
What I tried:
Code like the above works. It is definitely slow and the user often sees an ugly mess as things load. I probably won't fix this current project (works well enough for what I need it to), but if there's a better way I'd like to be better informed.
The core problem here is #ref="counterChild" , that reference will not yet be set in OnInitialized or (the first) OnParametersSet of the parent.
"it should only be used to do JS interop stuff" is too strict, it should be used for logic that needs the render to be completed. Making use of a component reference qualifies too.
I would use one of:
Put this action in OnInitialized() of the child component. (Assumes you always run the same action on a new child).
trigger it by a parameter on the Child component. That could a simple IsLoaded boolean. Take action in OnParametersSet() of the child.
Keep it in OnAfterRender() but use firstRender instead of loading.
That last one is exactly what you already have and it's not totally wrong. But it automatically means you get 2 renders, not the best U/X.
It's better to use OnParametersSet instead of OnAfterRender.
OnAfterRender is called after each rendering of the component. At this stage, the task of loading the component, receiving information and displaying them is finished. One of its uses is the initialization of JavaScript components that require the DOM to work; Like displaying a Bootstrap modal.
Note: any changes made to the field values in these events are not applied to the UI; Because they are in the final stage of UI rendering.
OnParameterSet called once when the component is initially loaded and again whenever the child component receives a new parameter from the parent component.
Separate out the data from the UI code and you get a state object and a component.
public class CounterState
{
public event EventHandler? CounterUpdated;
public int Counter { get; private set; }
// Contrived as Async as in real coding there may well be async calls to DbContexts or HttpClient
public async ValueTask IncrementCounterAsync(object sender)
{
await Task.Delay(150);
Counter = Counter + this._appConfiguration.IncrementRate;
CounterUpdated?.Invoke(sender, EventArgs.Empty);
}
}
#page "/countercomponent"
#implements IDisposable
<p role="status">Current count: #this.State.Counter</p>
<button class="btn btn-primary" #onclick="IncrementCount">Click me</button>
#code {
// Need to define how we get this and it's nullability
private CounterState State { get; set; }
protected override void OnInitialized()
=> this.State.CounterUpdated += this.OnCounterUpdated;
private void OnCounterUpdated(object? sender, EventArgs e)
{
if (sender != this)
StateHasChanged();
}
public async Task IncrementCount(object sender)
=> await this.State.IncrementCounterAsync(sender);
public void Dispose()
=> this.State.CounterUpdated -= this.OnCounterUpdated;
}
The parent:
#page "/counter"
<h3>Counter Parent</h3>
<SimpleCounter />
#code {
// Need to define how we get this and it's nullability
private CounterState State { get; set; }
protected async override Task OnInitializedAsync()
=> await this.State.IncrementCounterAsync(this);
}
The Scoped Service
The simplest implementation is to register CounterState as a scoped Service:
builder.Services.AddScoped<CounterState>();
And then inject it into our two components:
[Inject] private CounterState State { get; set; } = default!;
This maintains state within the SPA session. Navigate away and return and the old count is retained. However, this is often too wide a scope.
The Cascade
Cascading restricts the scope of the state object to component and sub-component scope.
Capture the cascaded value in the children:
[CascadingParameter] private CounterState State { get; set; } = default!;
How and what you cascade depends on the dependancies and disposal requirements of the state object.
With no commitments, simply create an instance and cascade it.
<CascadingValue Value="State" IsFixed>
//...
<SimpleCounter />
</CascadingValue>
//....
private CounterState State { get; set; } = new();
If you have DI dependancies then you need to create the state instance in the context of the DI container. With no disposal requirements you can set the scope to Transient and get a new instance by injection.
However, if the object requires disposal, you need to use ActivatorUtilities to create an instance outside the Service Container, but in the context of the container to populate dependancies.
#page "/counter"
#inject IServiceProvider ServiceProvider
#implements IDisposable
#implements IAsyncDisposable
<h3>Counter Parent</h3>
<CascadingValue Value="State" IsFixed>
<CounterComponent />
</CascadingValue>
#code {
private CounterState State { get; set; } = default!;
// Run before any render takes place, so the cascaded value is the correct instance
protected override void OnInitialized()
=> this.State = ActivatorUtilities.CreateInstance<CounterState>(ServiceProvider);
protected async override Task OnInitializedAsync()
=> await this.State.IncrementCounterAsync(this);
// Demonstrates how to implement Dispose when you don't know if your object implements IDisposable
public void Dispose()
{
if (this.State is IDisposable disposable)
disposable.Dispose();
}
// Demonstrates how to implement Dispose when you don't know if your object implements IAsyncDisposable
public async ValueTask DisposeAsync()
{
if (this.State is IAsyncDisposable disposable)
await disposable.DisposeAsync();
}
}
Note the cascade is set as IsFixed to ensure it doesn't cause RenderTree Cascades.
Saving a few CPU Cycles
If you don't use OnAfterRender you can short circuit it (and save running a few lines of code) like this.
#implements IHandleAfterRender
//...
Task IHandleAfterRender.OnAfterRenderAsync()
=> Task.CompletedTask;
Is it possible to have 2 different implementations of a service and inject a specific implementation into a component based on the props passed to it?
I have tried to find a solution and all I could find is pulling the desired service out of the services provider in the constructor which I don't want to do. I have done something similar in an API Controller which essentially would intercept the request before the service layer was injected and inject the correct service implementation depending on the request parameters.
Edit:
Essentially during startup I want to be able to inject 2 implementations of the same interface service.
builder.Services.AddScoped<IEmployeeService,EmployeeServiceA>();
builder.Services.AddScoped<IEmployeeService,EmployeeServiceB>();
Then in my component I would do something like:
#inject IEmployeeService _employeeService;
With the actual implementation being injected into the component being decided based off of parameters being sent into the component which would be intercepted by middle wear.
You need to manually get the correct service from the ServiceProvider by injecting the IServiceProvider and calling GetService on the provider.
Note: This is not recommended for normal component injection because you need to be careful about when you use the service: you may try and access it before you have initialized it. In the example below it's not available in OnInitialized.
Here's an example for a simple notification service.
An interface:
public interface INotificationService
{
public event EventHandler? Changed;
public void NotifyChanged(object? sender);
}
Two classes:
public class WeatherForecastNotificationService
: INotificationService
{
public event EventHandler? Changed;
public void NotifyChanged(object? sender)
=> this.Changed?.Invoke(sender, EventArgs.Empty);
}
public class WeatherReportNotificationService
: INotificationService
{
public event EventHandler? Changed;
public void NotifyChanged(object? sender)
=> this.Changed?.Invoke(sender, EventArgs.Empty);
}
Registered as (scoped) services:
builder.Services.AddScoped<WeatherForecastNotificationService>();
builder.Services.AddScoped<WeatherReportNotificationService>();
A component to get the services - ServiceTestComponent.razor:
#inject IServiceProvider serviceProvider
<div class="bg-info p-2 m-2">
<h3>ServiceTestComponent</h3>
<div class="p-3">
Type: #message
</div>
</div>
#code {
[Parameter] public object? Record { get; set; }
private INotificationService notificationService = default!;
private string message = string.Empty;
protected override void OnParametersSet()
{
if (this.Record is WeatherForecast)
{
var service = serviceProvider.GetService<WeatherForecastNotificationService>();
if (service is not null)
notificationService = service as INotificationService;
}
else
{
var service = serviceProvider.GetService<WeatherReportNotificationService>();
if (service is not null)
notificationService = service as INotificationService;
}
// simple bit of code to demo what actual service we have
if (notificationService is not null)
message = notificationService.GetType().Name;
}
}
And a test page:
#page "/"
#page "/Index"
<h1>Hello</h1>
<ServiceTestComponent Record=this.record />
<div>
<button class="btn btn-primary" #onclick=ChangeType>Change Type</button>
</div>
#code
{
private object record = new WeatherForecast();
private void ChangeType()
{
if (record is WeatherForecast)
record = new WeatherReport();
else
record = new WeatherForecast();
}
}
I have a page component and within it a breadcrumbs component and a business data component that displays all of the businesses for that page based on a category alias it is passed through the route.
Some of the breadcrumb links direct the user to another routable/page component and these work fine. However, some of them require a refresh of the business data component and it's these I'm having an issue with.
I've tried creating a service with a event that the breadcrumbs component can invoke when a link is clicked and the data component can subscribe to (following this post), but I can't get that to work as the return type is wrong when I try and call GetBusinesses as a result.
Here is the front end of my page component:
#if (breadcrumbs != null)
{
<Breadcrumbs Items="breadcrumbs"></Breadcrumbs>
}
<BrowseByBusiness Alias="#Alias"></BrowseByBusiness>
I set up my breadcrumbs like so (the Alias on the API call is a route parameter):
protected async override Task OnInitializedAsync()
{
categoryDto = await _publicClient.Client.GetFromJsonAsync<CategoryDto>($"api/Categories/GetCategoryByAlias/{Alias}");
breadcrumbs = new List<BreadcrumbItem>
{
new BreadcrumbItem("Browse By Category", "/browse-by-category"),
new BreadcrumbItem(categoryDto.Name, $"/browse-by-category/{categoryDto.Alias}"),
};
if (categoryDto.ParentCategory.Name != null)
{
breadcrumbs.Insert(1, new BreadcrumbItem(categoryDto.ParentCategory.Name, $"/browse-by-category/{categoryDto.ParentCategory.Alias}"));
}
}
My BrowseByBusiness component:
#foreach (var businessDto in businessesDto)
{
<Animate Animation="Animations.Fade" Easing="Easings.Ease" DurationMilliseconds="2000">
<BusinessCard businessDto="businessDto"></BusinessCard>
</Animate>
}
#code {
[Inject] IBusinessHttpRepository _businessRepo { get; set; }
public MetaData metaData = new MetaData();
IEnumerable<BusinessDto> businessesDto;
protected async override Task OnInitializedAsync()
{
await GetBusinesses();
}
private async Task GetBusinesses()
{
var pagingResponse = await _businessRepo.GetBusinessesByCategory(Alias, businessParameters);
businessesDto = pagingResponse.Items;
metaData = pagingResponse.MetaData;
}
}
My Breadcrumbs component:
<div class="flex-row border-bottom pb-2">
#foreach (var item in Items)
{
if (item.Equals(Last))
{
<span class="me-1">#item.Name</span>
}
else
{
<a class="me-1" href="#item.Url">#item.Name </a>
<span class="me-1">/</span>
}
}
</div>
#code {
[Parameter] public List<BreadcrumbItem> Items { get; set; }
private BreadcrumbItem Last;
protected async override Task OnInitializedAsync()
{
Last = Items.Last();
}
}
I'm using Blazor WASM and .net 6.
I recently had a similar need and ended up rolling my own (extremely simple) message broker. You can see full details in this blog post, but the short story is that you add a message broker class (which in my blog post has a silly name, but should really be called something like MessageBroker)...
public class SoupDragon {
public Action<Widget> NewWidget;
public void RaiseNewWidget(Widget w) =>
NewWidget?.Invoke(w);
}
You then register this as a service, inject it into the component that is to send the message, and use it follows...
_soupDragon.RaiseNewWidget(newWidget);
The component that is to pick up the message would inject a soup dragon, and hook up a method to catch the message. Whilst you can do this with a lambda, for reasons explained in more detail over there, you are best off hooking up to a method...
_soupDragon.NewWidget += HandleNewWidget;
One nice thing about this approach is that if you're doing server-side Blazor, you get communication between different users for free by making the message broker static.
I'm a bit stumped here because I do have the parameter attribute applied. I seem to be simply following the documentation in a one to one fashion.
Error message
Unhandled exception rendering component: Object of type 'Onero.Client.Features.CourseManager.CourseModelRegister' has a property matching the name 'HighSchoolRegistrationModelId', but it does not have [ParameterAttribute] applied.
Component hierarchy
//parent
<CascadingValue Value="HighSchoolRegistrationModelId">
<CourseModelAddForm></CourseModelAddForm>
</CascadingValue>
#code {
public long HighSchoolRegistrationModelId;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
HighSchoolRegistrationModelId = await CourseService.GetHighSchool();
}
}
//middle component
#if (add)
{
<CourseModelRegister HighSchoolRegistrationModelId="#HighSchoolRegistrationModelId">
</CourseModelRegister>
}
#code {
[CascadingParameter]
protected long HighSchoolRegistrationModelId { get; set; }
private bool add = false;
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
}
private void ShowAddForm()
{
add = true;
}
}
//Grandchild where error occurs
#code {
[CascadingParameter]
protected long HighSchoolRegistrationModelId { get; set; }
// rest of code omitted
}
Am I missing something? Perhaps this has to do with lifecycle management? I tried using OnInitialized as well as OnParametersSet.
I am pretty I have found the issue, I had the exact same situation, and its because I was passing value to the component's parameter and it was define as a [CascadingParameter], so it created a conflict.
So on this line:
<CourseModelRegister HighSchoolRegistrationModelId="#HighSchoolRegistrationModelId">
</CourseModelRegister>
You don't need to pass HighSchoolRegistrationModelId a value, because it will be filled by the [CascadingParameter]. In your case, you might need to make sure your middle component also has a [CascadingParameter] for the value to be passed to its children.
Inside a Blazor component, I want to call a JS method from the HTML onclick event of an anchor tag, thus:
<a onclick="window.LoadImage();" class="#CssClass">#DisplayName</a>
My JS method is:
window.LoadImage = () => {
DotNet.invokeMethodAsync('MyProject', 'FireEvent').then();
}
Inside the JS method, I want to fire a C# static method to trigger an event, thus:
[JSInvokable]
public static void FireEvent()
{
StateEngine.LoadImage(DisplayName, Target);
}
[Parameter]
public string DisplayName { get; set; }
[Parameter]
public string Target { get; set; }
I can't get the JS method to find the C# method. I've tried multiple name configurations for the first parameter, like
MyProject (the assembly name)
MyProject.FolderName.ComponentName
FolderName.ComponentName
ComponentName
to name a few. I've also tried shuffling names through the second parameter.
How is this supposed to work? What am I missing?
P.S. Please disregard the bit about the instance properties in the static method. I've got a workaround for that, but don't want to clutter up the post.
From the looks of it, you would be better off using the reference JSInterop so you can access the components properties.
Razor
#inject IJSRuntime JS
#implements IDisposable
<a #onclick=Clicked>Click here</a>
#code
{
private DotNetObjecctReference ObjRef;
[JSInvokable]
public void FireEvent()
{
StateEngine.LoadImage(DisplayName, Target);
}
private Task Clicked()
{
return JS.InvokeVoidAsync("window.LoadImage", ObjRef);
}
protected override Task OnInitializedAsync()
{
ObjRef = DotNetObjectReference.Create(this);
}
void IDisposable.Dispose()
{
ObjRef.Dispose();
}
}
JS
window.LoadImage = (dotNetObject) => {
dotNetObject.invokeMethod('FireEvent');
}
Your code looks good. May be its unable to find your .Net method. Give it a retry..
JS
window.LoadImage = function () {
DotNet.invokeMethodAsync("MyBlazorProject.UI", "FireEvent").then();}
First Parameter must be AssemblyName of your Blazor component. In my case, it is "MyBlazorProject.UI".
Second Parameter must be method name.
HTML
<a onclick="window.LoadImage()" class="btn btn-primary">Call .Net Method FireEvent</a>
here onclick is JavaScript event, which is good in your case too.
C# Code
[JSInvokable]
public static void FireEvent()
{
//debugger hits here
//StateEngine.LoadImage(DisplayName, Target);
}
This is all working for me fine. Debugger gets hit inside FireEvent .Net method after clicking anchor tag.
Thanks