Skip to content

[Blazor] component inheritance, rendering logic in base component #21664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
JvanderStad opened this issue May 10, 2020 · 16 comments
Closed

[Blazor] component inheritance, rendering logic in base component #21664

JvanderStad opened this issue May 10, 2020 · 16 comments
Labels
affected-few This issue impacts only small number of customers area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-blazor-component-model Any feature that affects the component model for Blazor (Parameters, Rendering, Lifecycle, etc) severity-major This label is used by an internal tool
Milestone

Comments

@JvanderStad
Copy link

I would like to propose a very simple inheritance improvement with Blazor components:

Simply put: If you would inherit from a base .razor component and you haven't specified any content in the inherited .razor component, do not override the BuildRenderTree method.

Why? I have created .razor components decorated with the abstract keyword in the partial class. The rendering logic (layout) is located in the base .razor component; this gives me the benefit of re-usage. If I inherit from this base component, BuildRenderTree is method is always overriden, even if I haven't specified any content. I can work around this, but its unnecessary redundant.


How is it working now?

BaseComponent.razor

@code 
{
    [Parameter]
    public string Value { get; set; }
}
The current value is: @Value

InheritNoContent.razor

@inherits BaseComponent

InheritNoContentBaseRender.razor

@inherits BaseComponent
@{ 
    base.BuildRenderTree(__builder);
}

If I use the above sample in a Razor file:

<BaseComponent Value="Sample_1" />
<InheritNoContent Value="Sample_2" />
<InheritNoContentBaseRender Value="Sample_3" />

the output would be:

The current value is: Sample_1
(empty)
The current value is: Sample_3

The above pattern is working, however it's quite redundant to create 2 razor components for every implementation of BaseComponent. I'm also calling base.BuildRenderTree(__builder); myself, this feels a bit hacky and possible unsupported/not recommended.


My proposal:

If you would inherit from a base .razor component and you haven't specified any content, do not generate the override BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder),
or, as an alternative, call the base implementation.


Sample:
BaseComponent.razor

@code 
{
    [Parameter]
    public string Value { get; set; }
}
The current value is: @Value

InheritNoContent.razor

@inherits BaseComponent

If I use the above sample in a Razor file:

<BaseComponent Value="Sample_1" />
<InheritNoContent Value="Sample_2" />

the output would be:

The current value is: Sample_1
The current value is: Sample_2
@JvanderStad JvanderStad changed the title Blazor component inheritance, rendering logic in base component [Blazor] component inheritance, rendering logic in base component May 10, 2020
@mkArtakMSFT mkArtakMSFT added the area-blazor Includes: Blazor, Razor Components label May 11, 2020
@mkArtakMSFT
Copy link
Member

Thanks for contacting us.
You can use class file (.cs) to inherit from the abstract component class and that way you'll control exactly what you override.

@mkArtakMSFT mkArtakMSFT added the ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. label May 11, 2020
@ghost ghost added the Status: Resolved label May 11, 2020
@ghost
Copy link

ghost commented May 12, 2020

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.

@ghost ghost closed this as completed May 12, 2020
@JvanderStad
Copy link
Author

JvanderStad commented May 13, 2020

@mkArtakMSFT I'm using Razor RenderFragments in the overrides

eg:

public override void SetupColumns()
{
	this[nameof(OrganizationQueryModel.Id)].RazorTemplate = (OrganizationQueryModel context) =>
	@<div>
		<img class="img-thumbnail circle thumb96 d-inline-block m-0" src="@(LinkGenerator.GetPathByAction<OrganizationController>(x=>x.Picture(context.Id)))" />
	</div>;
}

I'll make a follow-up issue

@mkArtakMSFT
Copy link
Member

Thanks for the key detail, @JvanderStad.
That's indeed an important detail we haven't considered earlier.

@mkArtakMSFT mkArtakMSFT added enhancement This issue represents an ask for new feature or an enhancement to an existing one and removed ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. Status: Resolved labels May 13, 2020
@mkArtakMSFT mkArtakMSFT added this to the Next sprint planning milestone May 13, 2020
@JvanderStad
Copy link
Author

JvanderStad commented May 13, 2020

I was thinking a little bit more about this; maybe the inherit mechanism can be even better:

If by convention you would inherit from a base component which has a ChildContent-parameter defined

[Parameter]
public RenderFragment ChildContent { get; set; }

'Inject' the Razor content of the inherited component into the ChildContent of the inherited-component. The generated code would be something like this:

protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
{
	__builder.OpenComponent<BaseComponent>(0);
	__builder.AddAttribute(1, "ChildContent", (RenderFragment)( (builder2) => {

		//  Code generation for the inherited component

	} ));

	__builder.CloseComponent();
}

alternatively

Set the value of ChildContent to the Razor content of the inherited component and call the base renderer.

ChildContent = @"generated code from this component";

protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
{
    base.BuildRenderTree(__builder);
}

This would shortcut the need to define parent/child content by simply inheriting from another component

Possible related: #7268

@mkArtakMSFT
Copy link
Member

We've moved this issue to the Backlog milestone. This means that it is not going to happen for the coming release. We will reassess the backlog following the current release and consider this item at that time. However, keep in mind that there are many other high priority features with which it will be competing for resources.

@git-net
Copy link

git-net commented Jun 6, 2020

base with a ChildContent and bool field

protected bool loadSubClass = false;

derived .razor

 @if (loadSubClass)
{
    // do derived logic
}

@if (!loadSubClass)
{
    @renderBase()
}

derived class method

renderFragment renderBase()
    {

        return builder =>
        {
            this.loadSubClass = true;
            base.ChildContent = this.BuildRenderTree;
            base.BuildRenderTree(builder);
            loadSubClass = false;
        };
    }

it works ....

@LukeTOBrien
Copy link

Hi there,

I have just been searchin for this kind of thing, what I would liike to do is class inheritance,
I am working with a partial *.razor.cs class and I would like to inherit from a base class that itself inherits from ComponentBase.

For for eg. I have I component that I want to open as a dialog and I want other components to share this common dialog logic.
I create a Dialog class that inherits ComponentBase but I recieve an error Partial decliration must not specify different base class.
I want to do somethis like:

public partial class InsertActionPanel : Dialog

@JvanderStad
Copy link
Author

JvanderStad commented Aug 6, 2020

@LukeTOBrien Great minds think alike, did exactly the same:
After reading again. you need to specify the base class in the razor file

@inherits Dialog

@captainsafia captainsafia added affected-few This issue impacts only small number of customers severity-major This label is used by an internal tool labels Oct 9, 2020
@javiercn javiercn added feature-blazor-builtin-components Features related to the built in components we ship or could ship in the future feature-blazor-component-model Any feature that affects the component model for Blazor (Parameters, Rendering, Lifecycle, etc) and removed feature-blazor-builtin-components Features related to the built in components we ship or could ship in the future labels Apr 20, 2021
@mkArtakMSFT mkArtakMSFT modified the milestones: .NET 7 Planning, Backlog Nov 11, 2021
@ghost
Copy link

ghost commented Nov 11, 2021

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

@nvmkpk
Copy link
Contributor

nvmkpk commented Dec 24, 2021

I would like to add one more on this context.

When we define a component inheriting from another component, I would like razor code generator to add the scope identifier of the base component as an attribute (in addition to the scope identifier of the derived component) so that the derived component can inherit the scoped styles from base component and also override them for that specific component.

Current implementation requires us to set the scope identifier to the one same as the base component's. Now this derived component cannot add its own styles to customize it without applying them to other components inheriting from the same base component and using the base component's scope identifier as a custom scope identifier. This also requires the derived component to add an empty scoped css file if it does not want to customize its styles.

@robertmclaws
Copy link

robertmclaws commented Jan 11, 2022

Something else to think about, I'd like a super-simple way to just be able to supply default implementation to existing controls. For example, given the following code (which is used repeatedly in my app:

<HxInputNumber CssClass="w-50" TValue="int" @bind-Value="viewModel.NewRoleModel.DependentSourceMax">
    <InputGroupStartTemplate>
        <span class="input-group-text" @onclick="@(() => DecrementRole(c => c.DependentSourceMax))"><i class="fa-solid fa-minus"></i></span>
    </InputGroupStartTemplate>
    <InputGroupEndTemplate>
        <span class="input-group-text" @onclick="@(() => IncrementRole(c => c.DependentSourceMax))"><i class="fa-solid fa-plus"></i></span>
    </InputGroupEndTemplate>
</HxInputNumber>

@code
{
    public void IncrementRole(Expression<Func<NewRoleModel, int>> outExpr)
    {
        var expr = (MemberExpression) outExpr.Body;
        var prop = (PropertyInfo) expr.Member;

        var value = (int)prop.GetValue(viewModel.NewRoleModel);
        if (value >= 10) return;

        value++;

        prop.SetValue(viewModel.NewRoleModel, value);
    }

    public void DecrementRole(Expression<Func<NewRoleModel, int>> outExpr)
    {
        var expr = (MemberExpression) outExpr.Body;
        var prop = (PropertyInfo) expr.Member;

        var value = (int)prop.GetValue(viewModel.NewRoleModel);
        if (value <= 0) return;

        value--;

        prop.SetValue(viewModel.NewRoleModel, value);
    }

}

... I would like to basically inherit from HxInputNumber and specify the InputGroupStartTemplate and InputGroupEndTemplate, providing the extra code in the @code section, but not changing the base render code at all. Then, I just drop my new control into the UI and the behavior I want is automatically wrapped with minimal markup.

This way, I wouldn't have to re-implement all the bindings I want to use, or anything like that.

HTH!

@RicardoJarree
Copy link

Am I right in saying that this feature would allow me to write a set of razor in the inherited class, such as a wrapper, and then call the build render tree in that to then render the concrete classes razor where required? If so this is exactly what I need right now..

@mkArtakMSFT
Copy link
Member

Thanks for contacting us.
We think that using the workaround provided in the initial post is reasonable. In addition, changing this would be a breaking change so we've decided not to move forward with this ask.

@inherits BaseComponent
@{ 
    base.BuildRenderTree(__builder);
}

@mkArtakMSFT mkArtakMSFT closed this as not planned Won't fix, can't repro, duplicate, stale Sep 28, 2022
@ChristopherHaws
Copy link
Contributor

@mkArtakMSFT I dont feel like this approach meets the standards set by a lot of the other parts of the framework, specifically people using the framework should be able to fall into the "pit of success". At the very least it seems like there could be a method on the base type which doesnt take the __builder protected field. Even something as simple as base.Render() would be better IMO.

As for the breaking change, you could still call base.BuildRenderTree(__builder) if you wanted to do so. Maybe an analyzer could be created to recommend base.Render()? I just don't feel like the current approach is very discoverable and definitely feels like a hack.

@ngollan
Copy link

ngollan commented Sep 29, 2022

Indeed, having an interface like base.Render() would be preferable; accessing __builder feels like touching a fragile internal field that could simply go away in a future release, probably leading to more obscure issues than an orderly behaviour change in a major release of the framework.

@ghost ghost locked as resolved and limited conversation to collaborators Oct 29, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
affected-few This issue impacts only small number of customers area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-blazor-component-model Any feature that affects the component model for Blazor (Parameters, Rendering, Lifecycle, etc) severity-major This label is used by an internal tool
Projects
None yet
Development

No branches or pull requests