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();
}
}
CodePudding user response:
The 'why' is clear:
In PetDog.razor you not only inherit form PetTemplate but you also create a new, different instance.
To debug that, make string display
protected
in the template and show it in the derived component:
@inherits PetTemplate
<PetTemplate Name="Dog">
<div>Someone's best friend!</div>
</PetTemplate>
<div>@display</div>
You can also show in the template, and verify that you have 2 different display
strings.
I'm not sure how to 'fix' this, the use case here does not really require inheritance. Hiding stuff is much easier too.
CodePudding user response:
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.
CodePudding user response:
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.