1

Here a page, myPage.razor

@page "/myPage"
<div class="row">
    <div class="col">
        <ComponentA />
    </div>
</div>

In the ComponentA I use a ComponentB

<div class="row">
    <div class="col">
        <ComponentB />
    </div>
</div>

In the myPage.razor.cs there is an instance of a class Person. I'd like pass this instance to ComponentA and to ComponentB.

I tried with [Parameter] and [CascadingParameter] without any success.

Is there a way to do this ?

Thanks,

Update 1: The Microsoft sample below works but that's not do what I want (below the child code). Microsoft

@page "/parameter-parent"
<h1>Child component (without attribute values)</h1>
<ParameterChild />
<h1>Child component (with attribute values)</h1>
<ParameterChild Title="Set by Parent"  Body="@(panelBody)" />
@code {
    PanelBody panelBody = new PanelBody() { Text = "Set by parent.", Style = "italic" };
}

//Child
@using Testing.Pages
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">@Title</div>
    <div class="card-body" style="font-style:@Body.Style">
        @Body.Text
    </div>
</div>

@code {
    [Parameter]
    public string Title { get; set; } = "Set By Child";

    [Parameter]
    public PanelBody Body { get; set; } =
        new()
        {
            Text = "Set by child.",
            Style = "normal"
        };
}

But I have to receive the object (not create a new one), display in an <input type="text"/> be able to change the text. The new value is update in the object in the ParameterParent. I tried the code below but Body is null all the time (the parent is the same then code above), I tried CascadingParameter too

@using Testing.Pages
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">@Title</div>
    <div class="card-body" >
        @Body.Text
    </div>
</div>

<InputText id="name" @bind-Value="Body.Text" />

@code {
    [Parameter]
    public string Title { get; set; } = "Set By Child";

    [Parameter]
    public PanelBody Body { get; set; }
}
TheBoubou
  • 19,487
  • 54
  • 148
  • 236
  • 1
    `Parameter` works. Post what you actually tried. The code you posted has no parameters – Panagiotis Kanavos Sep 29 '22 at 09:33
  • I know, it's the code I would like to add parameter. – TheBoubou Sep 29 '22 at 09:43
  • You wrote `without success`. What *did* you try and what didn't work? Have you checked the [docs on component parameters?](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/?view=aspnetcore-6.0#component-parameters) - add `[Parameter]` to the property you want to expose in `ComponentB` and use that as an attribute, eg `` – Panagiotis Kanavos Sep 29 '22 at 09:48
  • I tried the same solution than Henk below, I got the same error described in the comments (compilation ok) but error at runtime – TheBoubou Sep 29 '22 at 09:54
  • @Kris-I have you tried the *documentation* examples? Parameters work. All components, even the built-in ones, use `[Parameter]`, not some internal black-box mechanism. I've been using component parameters for two years. People can't guess what's wrong with code you didn't post. I'd bet you didn't add the parameter in `ComponentB`. Or you tried to use a string literal instead of `@(_someField)` to pass the value – Panagiotis Kanavos Sep 29 '22 at 10:03
  • The code from Microsoft work. But not what I want to do. See the Update 1 in the initial post – TheBoubou Sep 29 '22 at 12:37

2 Answers2

3

There are several ways to do multi-component two way communications.

You can create a notification service and subscribe components to notifications - here's a question and answer that shows you how to do that How can I trigger/refresh my main .RAZOR page from all of its sub-components within that main .RAZOR page when an API call is complete?.

Here is how to do it with a cascaded context object with notifications - similar in many ways to EditContext.

First the context object. We don't mix delegates and events with our data objects, so we create a specific object for the purpose that contains our edit model and it's associated events.

Here's PersonEditContext:

public class PersonEditContext
{
    public Person Person { get; set; } = new();
    public Action? Updated { get; set; }

    public void NotifyUpdated()
        => this.Updated?.Invoke();
}

Next ComponentA.

It:

  1. Picks up the cascaded context and create an EditContext from the Person instance.
  2. Wires up a handler to the edit context's OnFieldChanged event which raises the Action in the PersonEditContext cascaded instance.
  3. Wires up a handler to the PersonEditContext cascaded instance Updated Action to trigger a render event on the component.
  4. Implements IDisposable to tear down the handler wiring.
  5. itsMe prevents a render event if the component itself raises the event.
@implements IDisposable

<h3>ComponentA</h3>
<div>@personContext.Person.FirstName</div>
<div>@personContext.Person.LastName</div>
<p></p>
<hr />

<EditForm EditContext=this.editContext OnValidSubmit=HandleValidSubmit>
    <!-- Input2 -->
    <InputText id="name" @bind-Value="personContext.Person.FirstName" />
</EditForm>

@code {
    private EditContext? editContext;
    private bool itsMe;
    private Person person => personContext.Person;
    [CascadingParameter] private PersonEditContext personContext { get; set; } = new();

    protected override void OnInitialized()
    {
        editContext = new EditContext(person);
        editContext.OnFieldChanged += OnFieldChanged;
        personContext.Updated += Updated;
        base.OnInitialized();
    }

    private void HandleValidSubmit()
    {
    }

    private void OnFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        itsMe = true;
        personContext.NotifyUpdated();
    }

    private void Updated()
    {
        if (!itsMe)
            StateHasChanged();

        itsMe = false;
    }

    public void Dispose()
    {
        if (editContext is not null)
            editContext.OnFieldChanged -= OnFieldChanged;

        if (personContext is not null)
            personContext.Updated -= Updated;
    }
}

And MyPage which is very similar:

@page "/"
@implements IDisposable

<EditForm EditContext=this.editContext OnValidSubmit=HandleValidSubmit>
    <InputText id="name" @bind-Value="person.FirstName" />
</EditForm>

<CascadingValue Value="personContext" IsFixed="true">
    <ComponentA />
    <ComponentB />
</CascadingValue>

@code {
    private EditContext? editContext;
    private PersonEditContext? personContext;
    private Person person => personContext?.Person ?? new();
    private bool itsMe;

    protected override void OnInitialized()
    {
        personContext = new PersonEditContext { Person = new Person { FirstName = "Nancy", LastName = "Dvolio" } };
        editContext = new EditContext(person);
        editContext.OnFieldChanged += OnFieldChanged;
        personContext.Updated += Updated;
    }

    private void HandleValidSubmit()
    {
        // Whatever
    }

    private void OnFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        itsMe = true;
        personContext?.NotifyUpdated();
    }

    private void Updated()
    {
        if (!itsMe)
            StateHasChanged();

        itsMe = false;
    }

    public void Dispose()
    {
        if (editContext is not null)
            editContext.OnFieldChanged -= OnFieldChanged;

        if (personContext is not null)
            personContext.Updated -= Updated;
    }
}

Here's ComponentB to demonstrate the multi-component behaviour:

@implements IDisposable
<div class="bg-primary text-white p-2 m-2" >
    <h3>ComponentB</h3>
    <div>@person.FirstName</div>
    <div>@person.LastName</div>
</div>

@code {
    private Person person => personContext.Person;
    [CascadingParameter] private PersonEditContext personContext { get; set; } = new();

    protected override void OnInitialized()
    {
        personContext.Updated += Updated;
        base.OnInitialized();
    }

    private void Updated()
    {
        StateHasChanged();
    }

    public void Dispose()
    {
        if (personContext is not null)
            personContext.Updated -= Updated;
    }
}
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • When I copy the @code {} section to MyPage.razor.cs, I get plenty of error 'editContext' does not exists, personContext does not exist, StateHasChanged does not exists. Any idea why ? The files are named ComponantA.razor and ComponantA.razor.cs with a partial class in named with the same name than the component – TheBoubou Oct 03 '22 at 05:47
  • Can you show your MyPage.razor and MyPage.razor.cs. Your errors suggest you probably have a typo somewhere. – MrC aka Shaun Curtis Oct 03 '22 at 20:07
1

It is rather straightforward:

@page "/myPage"
<div class="row">
    <div class="col">
        <ComponentA Item="myItem" />
    </div>
</div>

@code { ItemType myItem = new(); }

and

<div class="row">
    <div class="col">
        <ComponentB Item="Item" />
    </div>
</div>

@code
{
  [Parameter] public ItemType Item { get; set; }
}

Component B should have exactly the same Parameter as Component A.

H H
  • 263,252
  • 30
  • 330
  • 514
  • Error: System.InvalidOperationException: Object of type 'ComponentA' does not have a property matching the name 'Item'. By the way the intelliSense does not set Item property – TheBoubou Sep 29 '22 at 09:06
  • 1
    @Kris-I `[Parameter]` works. I've been using components with parameters for the last 2 years. Post the *actual* code you tried. Something others can copy and try to compile – Panagiotis Kanavos Sep 29 '22 at 09:35
  • The compilation is ok it's when I load the page the problem. Is it normal than 'Item" property does not appear in the intellisense ? I tried with a simple string and not an object I get the same error 'MyPage' does not have a property matching the name 'Item'. – TheBoubou Sep 29 '22 at 09:46