1

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();
    }
}

2 Answers 2

5

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Am I have to use the code part of the "PetDog" for every new "Pet" component? This will be so tedious :( Should I go back to js :P
No, don't go to JS, please. Just learn the Blazor component model and how to use it. Learn what is a component, how it differs from normal classes, and how to design your components.
No I'm not going anywhere, just kidding. 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.
OK. your answer works. But repeating the code block in the PetDog for every new "pet" component still annoying for me. I hope better solutions available for upcoming blazor versions.
"I hope better solutions available for upcoming blazor versions." You actually mean correct solution, not better solution. My solution was the best for a faulty design. But I would never code like that. I guess what you want is to use Templated component in which the template component gets a generic parameter type, so that you can pass to the template a type param TItem whose underlying type may be a Dog object or a Cat object etc. Look up the docs for Templated Components
1

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:

  1. 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(); 
}
  1. In your PetDog, simple call the toggle method when inside click is raised

PetDog.razor:

<button @onclick="RaiseToggle">Show Details (Inside)</button>
  1. 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.

4 Comments

The SO formatting is messed up for some reason and I can't fix it. So pardon the messy code formatting.
Sory maybe I missed something but your code dosn't run.
What is the error?
Just do nothing like mine :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.