2 way data binding through a hierarchy of components is really confusing
Parent component --> Child component --> grandchild component
Parent component:
<childComponent @bind-Property="somevalue" />
Child component:
[Parameter] public Property { get; set;}
[Parameter] public EventCallback<type> PropertyChanged { get; set; }
How can I pass the bound value on to the grandchild component?
It won't met me add OnValueChanged and @bind-Property
Thanks
⚠ Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.
Good ❓ @timcromarty ... I haven't tried a three-level multi-level bind like that. I have a suspicion that a state container is the way to go.
NOTE: This example doesn't adhere to our latest guidance on components writing to their own params. However, an approach (described in a later comment of this issue) that uses fields breaks even more badly than the following example.
Added to Shared/NavMenu.razor ...
<li class="nav-item px-3">
<NavLink class="nav-link" href="parentcomponent">
<span class="oi oi-list-rich" aria-hidden="true"></span> Parent Component
</NavLink>
</li>
Pages/ParentComponent.razor ...
@page "/parentcomponent"
<h1>ParentComponent</h1>
<p>In Parent Property: <b>@Property</b></p>
<p>
<button @onclick="ChangePropertyValue">Change Property from Parent</button>
</p>
<ChildComponent @bind-Property="@Property" />
@code {
public string Property { get; set;} = "Intial value set in Parent";
private void ChangePropertyValue()
{
Property = $"New value set in Parent {DateTime.Now}";
}
}
Shared/ChildComponent.razor ...
<h2>ChildComponent</h2>
<p>In Child Property: <b>@Property</b></p>
<p>
<button @onclick="ChangePropertyValue">Change Property from Child</button>
</p>
<GrandchildComponent @bind-Property="@Property" />
@code {
[Parameter]
public string Property { get; set;}
[Parameter]
public EventCallback<string> PropertyChanged { get; set; }
private Task ChangePropertyValue()
{
Property = $"New value set in Child {DateTime.Now}";
return PropertyChanged.InvokeAsync(Property);
}
}
Shared/GrandchildComponent.razor ...
<h2>GrandchildComponent</h2>
<p>In Grandchild Property: <b>@Property</b></p>
<p>
<button @onclick="ChangePropertyValue">Change Property from Grandchild</button>
</p>
@code {
[Parameter]
public string Property { get; set; }
[Parameter]
public EventCallback<string> PropertyChanged { get; set; }
private Task ChangePropertyValue()
{
Property = $"New value set in Grandchild {DateTime.Now}";
return PropertyChanged.InvokeAsync(Property);
}
}
StateContainer.cs ...
public class StateContainer
{
public string Property { get; set; } = "Initial value from StateContainer";
public event Action OnChange;
public void SetProperty(string value)
{
Property = value;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
In Program.Main
...
builder.Services.AddSingleton<StateContainer>();
Added to Shared/NavMenu.razor ...
<li class="nav-item px-3">
<NavLink class="nav-link" href="parentcomponent2">
<span class="oi oi-list-rich" aria-hidden="true"></span> Parent Component 2
</NavLink>
</li>
Pages/ParentComponent2.razor ...
@page "/parentcomponent2"
@inject StateContainer StateContainer
@implements IDisposable
<h1>ParentComponent2</h1>
<p>In Parent Property: <b>@StateContainer.Property</b></p>
<p>
<button @onclick="ChangePropertyValue">Change Property from Parent</button>
</p>
<ChildComponent2 />
@code {
protected override void OnInitialized()
{
StateContainer.OnChange += StateHasChanged;
}
private void ChangePropertyValue()
{
StateContainer.SetProperty($"New value set in Parent {DateTime.Now}");
}
public void Dispose()
{
StateContainer.OnChange -= StateHasChanged;
}
}
Shared/ChildComponent2.razor ...
@inject StateContainer StateContainer
@implements IDisposable
<h2>ChildComponent2</h2>
<p>In Child Property: <b>@StateContainer.Property</b></p>
<p>
<button @onclick="ChangePropertyValue">Change Property from Child</button>
</p>
<GrandchildComponent2 />
@code {
protected override void OnInitialized()
{
StateContainer.OnChange += StateHasChanged;
}
private void ChangePropertyValue()
{
StateContainer.SetProperty($"New value set in Child {DateTime.Now}");
}
public void Dispose()
{
StateContainer.OnChange -= StateHasChanged;
}
}
Shared/GrandchildComponent2.razor ...
@inject StateContainer StateContainer
@implements IDisposable
<h2>GrandchildComponent2</h2>
<p>In Grandchild Property: <b>@StateContainer.Property</b></p>
<p>
<button @onclick="ChangePropertyValue">Change Property from Grandchild</button>
</p>
@code {
protected override void OnInitialized()
{
StateContainer.OnChange += StateHasChanged;
}
private void ChangePropertyValue()
{
StateContainer.SetProperty($"New value set in Grandchild {DateTime.Now}");
}
public void Dispose()
{
StateContainer.OnChange -= StateHasChanged;
}
}
... and that turned out well. :+1:
Artak, two ❓ on this one:
How to fix (if possible) the chained bind (first approach ☝️). It only fails for one scenario, when the property is changed in the grandchild. Other than that, it works in every other aspect:
If this behavior is expected, then perhaps a callout that chained bind only works for a single parent. If that example can be fixed, then perhaps a callout for whatever the solution is in the Chained bind section of the topic.
The state container approach and example (:point_up:) for across-component state looks good. I propose that we add that in a new section at the bottom of the topic. Sound good? :ear:
@javiercn do you know why this doesn't work:
How to fix (if possible) the chained bind (first approach ☝️). It only fails for one scenario, when the property is changed in the grandchild. Other than that, it works in every other aspect:
Parent change -> Child and Grandchild updates
Child change -> Parent and Grandchild updates
Grandchild change -> Only the Child updates, not the Parent.
If this behavior is expected, then perhaps a callout that chained bind only works for a single parent. If that example can be fixed, then perhaps a callout for whatever the solution is in the Chained bind section of the topic.
Thanks @guardrex. Before documenting let's first understand why the chained bind doesn't work.
@mkArtakMSFT I still don't have a solution for the Three-level-bind approach (chained bind). Only the State container approach was working when I took a look at this earlier. Should I try the chained-bind approach again to confirm that it still isn't working?
I've to rebuilt the three-level bind scenario using the latest guidance (i.e., components shouldn't write directly to their own parameters), but it still doesn't work. The child and grandchild affect their immediate senior components, and none of them bind down components.
@SteveSandersonMS, can you provide a tip to make three-level two-way chained binding work, or should I only document the state container approach, which seems simpler and works fine? After working with these scenarios, _I really like state containers!_ :smile: That's what I would use for multiple-level two-way "binding" (using binding loosely in this context ... I just mean one component updates data and nested components regardless of depth receive the updated value). The state container example that I built is in this comment.
Pages/ParentComponent.razor ...
@page "/parentcomponent"
<h1>ParentComponent</h1>
<p>In Parent Property: <b>@parentValue</b></p>
<p>
<button @onclick="ChangeValue">Change from Parent</button>
</p>
<ChildComponent @bind-Property="parentValue" />
@code {
private string parentValue;
[Parameter]
public string Property { get; set; } = "Intial value set in Parent";
[Parameter]
public EventCallback<string> PropertyChanged { get; set; }
protected override void OnInitialized()
{
parentValue = Property;
}
private Task ChangeValue()
{
parentValue = $"New value set in Parent {DateTime.Now}";
return PropertyChanged.InvokeAsync(parentValue);
}
}
Shared/ChildComponent.razor ...
<h2>ChildComponent</h2>
<p>In Child Property: <b>@childValue</b></p>
<p>
<button @onclick="ChangeValue">Change from Child</button>
</p>
<GrandchildComponent @bind-Property="childValue" />
@code {
private string childValue;
[Parameter]
public string Property { get; set; }
[Parameter]
public EventCallback<string> PropertyChanged { get; set; }
protected override void OnInitialized()
{
childValue = Property;
}
private Task ChangeValue()
{
childValue = $"New value set in Child {DateTime.Now}";
return PropertyChanged.InvokeAsync(childValue);
}
}
Shared/GrandchildComponent.razor ...
<h2>GrandchildComponent</h2>
<p>In Grandchild Property: <b>@grandchildValue</b></p>
<p>
<button @onclick="ChangeValue">Change from Grandchild</button>
</p>
@code {
private string grandchildValue;
[Parameter]
public string Property { get; set; }
[Parameter]
public EventCallback<string> PropertyChanged { get; set; }
protected override void OnInitialized()
{
grandchildValue = Property;
}
private Task ChangeValue()
{
grandchildValue = $"New value set in Grandchild {DateTime.Now}";
return PropertyChanged.InvokeAsync(grandchildValue);
}
}
Useful for cascading values _down_ to children, not up to parents.
@javiercn I'm up to working on this one now.
Three-level chained bind doesn't seem to work. A state container works great.
We need to add the state container approach to the docs anyway, but is a three-level chained bind either ...
@SteveSandersonMS can chime in here. I'm fully booked for the next week, so I'll be slow to answer.
You can bind through any number of levels, but you have to respect the one-way data flow. That is, "changed" notifications flow up the hierarchy, and new parameter values flow down it.
Also it's simplest if you only store the underlying data in one place (the parent component) to avoid any confusion about what state needs to be updated.
Try this:
ParentComponent.razor
<h1>ParentComponent</h1>
<p>In Parent Property: <b>@parentValue</b></p>
<p>
<button @onclick="ChangeValue">Change from Parent</button>
</p>
<ChildComponent @bind-Property="parentValue" />
@code {
private string parentValue = "Intial value set in Parent";
private void ChangeValue()
{
parentValue = $"New value set in Parent {DateTime.Now}";
}
}
ChildComponent.razor
<h2>ChildComponent</h2>
<p>In Child Property: <b>@Property</b></p>
<p>
<button @onclick="ChangeValue">Change from Child</button>
</p>
<GrandchildComponent @bind-Property="BoundValue" />
@code {
[Parameter]
public string Property { get; set; }
[Parameter]
public EventCallback<string> PropertyChanged { get; set; }
private string BoundValue
{
get => Property;
set => PropertyChanged.InvokeAsync(value);
}
private Task ChangeValue()
{
return PropertyChanged.InvokeAsync($"New value set in Child {DateTime.Now}");
}
}
GrandchildComponent.razor
<h2>GrandchildComponent</h2>
<p>In Grandchild Property: <b>@Property</b></p>
<p>
<button @onclick="ChangeValue">Change from Grandchild</button>
</p>
@code {
[Parameter]
public string Property { get; set; }
[Parameter]
public EventCallback<string> PropertyChanged { get; set; }
private Task ChangeValue()
{
return PropertyChanged.InvokeAsync($"New value set in Grandchild {DateTime.Now}");
}
}
That's great. Thanks - very helpful
Most helpful comment
You can bind through any number of levels, but you have to respect the one-way data flow. That is, "changed" notifications flow up the hierarchy, and new parameter values flow down it.
Also it's simplest if you only store the underlying data in one place (the parent component) to avoid any confusion about what state needs to be updated.
Try this:
ParentComponent.razor
ChildComponent.razor
GrandchildComponent.razor