I'm new to Blazor and I'm currently working on a Blazor Webassembly .net 5.0 application.
I try to figure out the correct way to
render a child component
from a parent component
on button click (form submit)
pass parameters from the parent component to => the child component
My current solution seems to work, but unfortunately it ends in an infinite rendering loop: I use the OnParametersSetAsync method in the child component to handle the data loading.
Side note: I use Telerik Blazor components, but it should have no impact.
My parent component looks like this:
View (parent)
// I want to submit a form to set a bool = true, and then to rend the child component - is that ok?
<EditForm OnValidSubmit="#(async () => await StartEverything())">
<label for="OrderNumber">OrderNumber: </label>
<TelerikTextBox #bind-Value="OrderNumber" Id="OrderNumber" />
<TelerikButton ButtonType="#ButtonType.Submit">Start Everything</TelerikButton>
</EditForm>
#if (ShowChild)
{
<MyChildComponent OrderNumber="OrderNumber"/>
}
else
{
<div>Please enter an order number.</div>
}
Code Behind (parent)
public class MyParentComponent : ComponentBase {
protected int OrderNumber { get; set; }
protected bool ShowChild { get; set; }
protected async Task StartEverything()
{
if (OrderNumber > 0)
{
await Task.FromResult(ShowChild = true);
}
}
}
My child component looks like this:
View (child)
#if (Customer != null)
{
<p>#Customer.CustomerName</p>
<p>#Customer.AgencyName</p>
}
Code Behind (child)
public class MyChildComponent : ComponentBase {
// I need this Parameter sent from my parent component
[Parameter]
public int OrderNumber { get; set; }
protected CustomerViewModel Customer { get; set; }
protected override async Task OnParametersSetAsync()
{
var parameterForQuery = OrderNumber; // this should hold the value sent from the parent component
// Load Customer ViewModel Data here - is this the correct event? What is the best approach?
}
}
Item ViewModel
public class CustomerViewModel
{
public string CustomerName { get; set; }
public string AgencyName { get; set; }
}
Do you know how to correctly render a Child Component within a Parent Component and pass parameters from the Parent Component to the child component - then render the child component ONLY ON BUTTON CLICK (form submit, no infinite render loop)?
Do you know how to solve this problem?
I recommend going through https://blazor-university.com. It's the site that kind of got me kick-started when I first started with Blazor.
In regard to your question, I recommend the following:
https://blazor-university.com/components/component-lifecycles/
In particular, the following statement should prove useful in your case (from that link):
OnInitialized / OnInitializedAsync
This method is only executed once when the component is first created.
If the parent changes the component’s parameters at a later time, this
method is skipped.
It seems likely that simply changing which method you override will solve your problem, since OnParametersSetAsync behaves as you've described, and 'OnInitializedAsync' behaves as you want. :D
Related
I have a Blazor app that manages a lot of form input. Every form is tied to an instance of type IncidentLog, and every UI element of the form is tied to a property of that IncidentLog instance.
In the form, we have a MudSelect component where T="Department". The MudSelect has MultiSelection="true", and the results are stored in a IEnumerable(Department) Departments property of the IncidentLog instance.
This component works totally fine, but I've tried implementing FluentValidation in the form and I'm not sure how to define the expression of the For parameter of this MudSelect component. It looks like it's expecting me to pass an object that matches T="Department", but the validation I need to run against the validator is based off of IEnumerable(Department)... I just want to check that the user has selected at least one department from the multiselect component.
Error message when trying to pass IEnumerable object to the For validator:
IncidentLogPropertiesComponent razor page - relevant blurb
<MudForm Model="#Log" #ref="#form" Validation="#(incidentLogValidator.ValidateValue)" ValidationDelay="100">
<MudCardContent Class="pt-0">
...
<MudSelect T="Department" ToStringFunc="#ConverterDepartment" Dense="true" Margin="Margin.Dense" Label="Affected Departments:" MultiSelection="true" #bind-SelectedValues="Log.Departments" Clearable>
#foreach (var department in Departments.OrderBy(o=>o.DepartmentName).ToList())
{
<MudSelectItem Value="#department"/>
}
</MudSelect>
...
</MudCardContent>
</MudForm>
IncidentLog.cs:
public class IncidentLog : Log
{
[Key]
public int IncidentID { get; set; }
....
[Write(false)]
public IEnumerable<Department> Departments { get; set; } = new HashSet<Department>();//3
....
}
IncidentLogPropertiesComponentBase
public class IncidentLogPropertiesComponentBase : OwningComponentBase<iIncidentLogRepository>, IDisposable
{
protected IncidentLogValidator incidentLogValidator = new IncidentLogValidator();
...
[Parameter]
public IncidentLog? Log
{
get
{
return (AppState.SelectedLog != null && AppState.SelectedLog.LogType.LogTypeID == 2) ? (IncidentLog)AppState.SelectedLog : null;
}
set
{
AppState.SetLog(value);
}
}
}
IncidentLogValidator:
public class IncidentLogValidator: AbstractValidator<IncidentLog>
{
public IncidentLogValidator()
{
...
RuleFor(t => t.Departments).NotEmpty().WithMessage("Must enter at least one department affected.");
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(ValidationContext<IncidentLog>.CreateWithOptions((IncidentLog)model, x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
*I didn't link my AppState state management file even though it's referenced in code above - I don't think it's relevant to the issue I'm having.
How do I provide Fluent Validation to Multi-Select Dropdowns using MudBlazor?
I have a detail page for some items in my application. This detail page contains an overview section and a few tabs under the overview section.
Users can update the overview section, after a successful update I need to reload the tabs under the overview sections.
I'm using MatBlazor to render tabs. I need to re-render the tabs after the parent component update. The typical way is to pass a callback to the child component. But here the child components (tabs to be specific) are RenderFragment which is a delegate.
Here's the razor code portion of tabs in the parent component:
<div class="jds-shadow-soft-xxs jds-radius-l min-height100p pt1">
<MatTabGroup Class="jobTabs">
#foreach (var tab in tabs)
{
<MatTab>
<LabelContent>
#tab.Label
</LabelContent>
<ChildContent>
#tab.Content
</ChildContent>
</MatTab>
}
</MatTabGroup>
</div>
MatBlazor uses RenderFragment to render tab content. Here's my code in the parent component for the tabs RenderFragment
List<JobConfiguartionTabItem> tabs = new List<JobConfiguartionTabItem>();
protected override async Task OnInitializedAsync()
{
try
{
tabs.AddRange(new List<JobConfiguartionTabItem> {
new JobConfiguartionTabItem(){Label = "Scheduled Activities",Content = GetRenderFragment(typeof(JobTemplateScheduleActivityComponent))},
new JobConfiguartionTabItem(){Label = "Account Selection",Content = GetRenderFragment(typeof(AccountSelectionComponent))},
new JobConfiguartionTabItem(){Label = "Schedule",Content = GetRenderFragment(typeof(JobTemplateScheduleComponent))},
new JobConfiguartionTabItem(){Label = "Scheduled History",Content = GetRenderFragment(typeof(JobTemplateScheduledJobComponent))}
}
);
// fetching initial data for the parent component
await this.GetData();
}
catch (Exception exp)
{
Console.WriteLine("Error: " + exp);
}
}
Here's the JobConfigurationTabItem class
public class JobConfiguartionTabItem
{
public string Label { get; set; }
public RenderFragment Content { get; set; }
}
Here's the GetRenderFragment method in the parent component
private RenderFragment GetRenderFragment(Type component)
{
RenderFragment renderFragment = renderTreeBuilder =>
{
renderTreeBuilder.OpenComponent(0, component);
renderTreeBuilder.CloseComponent();
};
return renderFragment;
}
To simplify my requirement: I need to raise an event from the parent component, and the child component should handle the event with a handler method. In my case, child components are RenderFragment. I couldn't find a way to achieve it through RenderFragment.
Okay, I've solved the problem. My situation is Parent component needed to communicate with the child components. When the parent is updated it needs to tell the child to reload/ re-render themselves as well.
A typical way is to put a #ref keyword in the child markup in the parent to capture the reference. Then we can call any method of the child via the ref.
In my case, I was using a RenderFragment which is not an instance of the child rather a delegate.
I've refactored the GetRenderFragment method to capture the reference of the target child.
private JobTemplateScheduleActivityComponent jobTemplateScheduleActivityComponent { get; set; }
private RenderFragment GetRenderFragment(Type component)
{
RenderFragment renderFragment = renderTreeBuilder =>
{
renderTreeBuilder.OpenComponent(0, component);
if (component == typeof(JobTemplateScheduleActivityComponent))
{
// capturring RenderFragment component reference
renderTreeBuilder.AddComponentReferenceCapture(1, (_value =>
{
jobTemplateScheduleActivityComponent = (JobTemplateScheduleActivityComponent)_value;
}));
}
renderTreeBuilder.CloseComponent();
};
return renderFragment;
}
I have the following method in the parent to run after the update:
protected async Task RunAfterUpdate()
{
await this.GetData();
// calling the reload method of the target child
await jobTemplateScheduleActivityComponent.Reload();
StateHasChanged();
}
RenderTreeBuilder.AddComponentReferenceCapture is the method I was looking for to capture the reference of a RenderFragment.
Considering I have a class called Tab.
public class Tab
{
public Guid Id { get; }
public bool IsVisible { get; set; }
protected Tab()
{
Id = Guid.NewGuid();
}
}
I want to strictly couple these tabs to a Blazor Component instance and render those instances by iterating over the tabs. I want to have control over when a Component is created and when it is destroyed again.
I want to do this because that way I can persist the state for each component.
Here is the problem with the easy approach. Considering something like this:
#code {
public void CreateNewTabAndRemoveOldTab()
{
Tabs.RemoveAt(0);
Tabs.Add(new Tab());
}
}
foreach (var tab in Tabs)
{
<MyTabComponent/>
}
The newly created tab will simply take over the state of the removed tab. OnInitialized will not be called.
I have looked into RenderFragment, but it does not look like its working property. The problem is that the Blazor Framework will still decide when a new component is created (thus calling OnInitialized) or when existing instance are used.
If I read this correctly, Tab is a class, not a component. You need to decouple your list of Tabs from the component that renders them. Your list of tabs lives in a service, the scope depends on what you're doing with them. Your Tab Component displays the currently selected Tab in the list. If you show a little more of your logic I can probably show you a relevant working example.
Check this
#ref it's reference to your component it's added to list of components.
#implements IDisposable
#foreach (var tab in tabs)
{
<MyTabComponent #ref=#TabRef Tab=#tab />
}
#code {
List<Tab> tabs = new List<Tab>();
List<MyTabComponent> tabsComp = new List<MyTabComponent>();
MyTabComponent TabRef {
set { tabsComp.Add(value); }
}
public void CreateNewTabAndRemoveOldTab()
{
tabs.RemoveAt(0);
tabs.Add(new Tab());
}
public void Dispose()
{
}
}
MyTabs.razor
<CascadingValue Value="#this" IsFixed="true">
foreach (var tab in Tabs)
{
<MyTabComponent/>
}
</CascadingValue>
#code{
private List<MyTabComponent> Tabs;
public void Register(MyTabComponent tab)
{
this.Tabs.Add(tab);
}
public void UnRegister(MyTabComponent tab)
{
this.Tabs.Remove(tab);
}
}
MyTabComponent.razor
#implements IDisposable
<div></div>
#code{
[CascadingParameter]
protected MyTabs Context { get; set; }
protected override void OnInitialized()
{
Context?.Register(this);
// implement your logic here
}
public void Dispose()
{
Context?.UnRegister(this);
// implement your logic here
}
}
I have a base component PetTemplate and a second PetDog that inherits and uses the template of PetTemplate. PetTemplate has a method named ToggleDisplay. My goal is when I click the button on the Index page that invokes the PetDog.ToggleDisplay method and show/hide the PetDog details on the page.
The "Inside" button in the sample code below works but "Outside" button don't. How can I invoke the ToggleDisplay method from a page or a parent component correctly?
Index.razor
#page "/"
<button #onclick="ShowPetDetails">Show Details (Outside)</button>
<PetDog #ref="dog" />
#code {
PetDog dog;
void ShowPetDetails()
{
dog.ToggleDisplay();
}
}
PetDog.razor
#inherits PetTemplate
<PetTemplate Name="Dog">
<div>Someone's best friend!</div>
</PetTemplate>
PetTemplate.razor
<div class="mt-3">
<button #onclick="ToggleDisplay">Show Details (Inside)</button>
<h3>Pet Name: #Name</h3>
<div style="display:#display">
#ChildContent
</div>
</div>
#code {
string display = "none";
[Parameter]
public string Name { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
public void ToggleDisplay()
{
display = display == "none" ? "block" : "none";
StateHasChanged();
}
}
When you use
<PetDog #ref="dog" />
#code {
PetDog dog;
void ShowPetDetails()
{
dog.ToggleDisplay();
}
}
You actually create a reference to the PetDog component, and then try to call a derived method, dog.ToggleDisplay(), on object you have no reference to ( the instance of the PetTemplate). In order to make it work, you'll have to get a reference to the parent component (PetTemplate), and provide it to the derived component (PetDog), like this:
PetTemplate.razor
<div class="mt-3">
<button #onclick="ToggleDisplay">Show Details (Inside)</button>
<h3>Pet Name: #Name</h3>
<div style="display:#display">
#ChildContent
</div>
</div>
#code {
string display = "none";
string val;
[Parameter]
public string Name { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
public void ToggleDisplay()
{
display = display == "none" ? "block" : "none";
InvokeAsync(() => StateHasChanged());
}
}
PetDog.razor
#inherits PetTemplate
<PetTemplate #ref="petTemplate" Name="Dog">
<div>Someone's best friend!</div>
</PetTemplate>
#code
{
PetTemplate petTemplate;
public PetTemplate PetTemplateProp { get; set; }
protected override void OnAfterRender(bool firstRender)
{
if(firstRender)
{
PetTemplateProp = petTemplate;
}
base.OnAfterRender(firstRender);
}
}
Index.razor
#page "/"
<button #onclick="ShowPetDetails">Show Details (Outside)</button>
<PetDog #ref="dog" />
#code {
PetDog dog;
void ShowPetDetails()
{
dog.PetTemplateProp.ToggleDisplay();
}
}
Note: Though Razor components are C# classes, you cannot treat them as normal classes. They behave differently. As for instance, you can't define a variable instance, and set its parameters, etc. outside of the component. At best, you can capture a reference to a component as well as call public methods on the component instance, as is done in the current sample. In short, component objects differ from normal classes.
It's also important to remember that each component is a separate island that can render independently of its parents and children.
But just wondering how can I change a component parameter value from outside of it, that inherited/uses a template. I tried the methods in the documentation or the resources I found, but it didn't work for my case
You should not (it was a warning) and probably cannot ( it may be now an error) change a component parameter's value outside of the component. As for instance, you can't capture a reference to a component and assign a value to its parameter property:
<PetTemplate #ref="petTemplate">
<div>Someone's best friend!</div>
</PetTemplate>
PetTemplate petTemplate;
This is not allowed: petTemplate.Name="Dog" as this is changing the parameter outside of its component. You can only do that like this:
<PetTemplate Name="Dog">
<div>Someone's best friend!</div>
</PetTemplate>
Furthermore, modification of a parameter property from within the component itself is deprecated ( currently you should get a warning, at least that is what Steve Sanderson suggested to the Blazor team).
To make it clear, you should not modify the parameter property Name from within the PetTemplate component. A parameter property should be automatic property; that is, having a get and set accessors like this: [Parameter] public string Name { get; set; }
And you should not use it like this:
private string name;
[Parameter]
public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
// Code to a method or whatever to do something
}
}
}
This is deprecated as it may have side effects. Component parameters should be treated as DTO, and should not be modified. If you wish to perform some manipulation of the parameter value, then copy it to a local variable, and do your thing.
As pointed out by #enet Blazor component inheritance doesn't behave exactly as one would intuitively expect. This is a cleaner approach when you want to control a UI functionality that can be controlled both internally and externally:
Declare an event in the base component that is raised when the UI state is changed from within the component. Also let the variable that controls the state be a parameter. In you case, something like
PetTemplate.razor:
[Parameter]
public EventCallback OnToggleRequested {get;set;}
[Parameter]
public string Display {get;set;}
protected async Task RaiseToggle()
{
await OnToggleRequested.InvokeAsync();
}
In your PetDog, simple call the toggle method when inside click is raised
PetDog.razor:
<button #onclick="RaiseToggle">Show Details (Inside)</button>
In your container (in this case, index.razor) listen to the event and make changes. Also wire the outside button to the same method:
Index.razor:
<button #onclick="ToggleDisplay">Show Details (Outside)</button>
<PetDog OnToggleRequested="ToggleDisplay" Display="#display"/>
string display = "block";
void ToggleDisplay()
{
display = display == "none" ? "block" : "none";
}
Note that the event can be used at level of hierarchy and you don't need to capture any references anywhere.
Let's say I have a collection of data in my page loaded via OnInitializedAsync(). The data is shown graphically in a table but later on also in more detail in another table further down on the page.
Since the rows in the detailed table has a lot of controls and logic I decided to make a component for the row e.g. <RowData Data="#rowdata" /> and bound each row data.
The problem is that if the data gets changed in my child controller (RowData) it won't reflect in my first table in the "parent" component where the same data is also listed.
Is there an easy way to signal change or should I avoid making child components?
I have sovled it by making an EventCallback in my child component and updating via callback in my parent component. But I have the feeling I'm missing something.
The following sample shows how to perform two-way data-binding between a parent
component and its child component. In each of these two component is a text box controls. When you type text in the parent component's text box, the text in the child component's text box changes to reflect the changes made in the parent,
and vice versa...
ChildComponent.razor
<div style="border:solid 1px red">
<h2>Child Component</h2>
<input type="text" #bind="Text" #bind:event="oninput" />
</div>
#code {
private string text { get; set; }
[Parameter]
public string Text
{
get { return text; }
set
{
if (text != value) {
text = value;
if (TextChanged.HasDelegate)
{
TextChanged.InvokeAsync(value);
}
}
}
}
[Parameter]
public EventCallback<string> TextChanged { get; set; }
}
ParentComponent.razor
#page "/ParentComponent"
<h1>Parent Component</h1>
<input type="text" #bind="Text" #bind:event="oninput" />
<p></p>
<ChildComponent #bind-Text="Text" />
#code {
[Parameter]
public string Text { get; set; } = "Hello Blazor";
}
I have sovled it by making an EventHandler in my child component and updating via callback. But I have the feeling I'm missing something
What you've been missing is the existence of the EventCallback 'delegate' used in this sample to call the parent component and pass it the value entered in the child component. This is how we define the 'delegate'
[Parameter]
public EventCallback<string> TextChanged { get; set; }
And this is how we invoke it, when the value of the Text property changes:
TextChanged.InvokeAsync(value);
What delegate did you use ? Note that the EventCallback's target is not the child component, but the parent component...
Good luck... If something is not clear, don't hesitate to ask...
If you have a root component with N levels of nested components (children within children ad-nauseam) then you can use a cascading value. Try something like this
public class MyState
{
public List<MyObject> Objects { get; set; }
public Action OnModified { get; }
public MyState(List<MyObject> objects, Action onModified)
{
Objects = objects;
OnModified = onModified;
}
}
In your parent component
MyState State;
protected override OnInitialized()
{
State = new MyState(your objects, () => InvokeAsync(StateHasChanged));
}
In your parent markup
<CascadingValue Value=State>
All your child content here
</CascadingValue>
In your various child components that need access
[CascadingParameter]
public MyState State { get; set; }
protected void SomeEditWasMade()
{
State.Objects[23].Name = "Bob";
State.OnModified();
}
That should call the () => InvokeAsync(StateHasChanged) in the parent, and then that component and every component that consumes the MyState cascading value will get rerendered.
Or you could use something like Fluxor :)