how to use generic components with multiple types in blazor

Viewed 36

I'm making a generic grid component in blazor c# to handle CRUD operations and master detail relations.

now I have two generic components

public class GridM<TParentModel> where TParentModel : new() { // My code }

and I have

public class GridD<TParentModel,TChildModel> : GridM<TChildModel>where TParentModel : new() 
{

 // here I'm overriding some methods from the parent class to handle relational stuff

}

so for plain tables I'm using GridM to display my data and when I have 1 to many relations with the master table I'm using GridD to display the relational data.

so far so good, I do not have any issues with the above approach. Then after putting some thoughts on this approach I've decided to merge GridM and GridD into one class like this:

public class MyGrid<TModel> {}
public class MyGrid<TModel, TChild> : MyGrid<TChild>{ // and override methods here }

this in theory works fine and assuming these implementations were not components I would be able to initialize them like

 var a = new MyGrid<Users>();
 var b = new MyGrid<Users,Orders>();

so when I try to use this component like

< MyGrid TModel="UserModel"> </MyGrid >

I get the following error:

The component 'MyGrid' is missing required type arguments. Specify the missing types using the attributes: 'TChildModel'.

because blazor does not now whether I'm using MyGrid<TModel> or MyGrid<TModel,TChildModel>.

my question is how can I make blazor understand this?

1 Answers

You don't have one class, you have two. The Razor compiler doesn't know which class to map the Tag to, so it throws the error:

Multiple components use the tag Fred. Components: SO73613449.Data.Fred<TModel>, SO73613449.Data.Fred<TModel, TType>

You can't use them directly in Razor markup, but you can in RenderTreeBuilder code in a RenderFragment.

Here's some code to demo how to do it:

First my classes:

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;

namespace SO73613449.Data;

public class Fred<TModel> : ComponentBase
{
    public TModel? Model { get; set; }
    protected string name = "Fred (TModel)";
    protected string cssClass = "alert alert-primary";

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(0, "div");
        builder.AddAttribute(1, "class", cssClass);
        builder.AddMarkupContent(2, $"Hello from {name} <br /> {this.GetType().FullName}");
        builder.CloseElement();
    }
}

public class Fred<TModel, TType> : Fred<TModel>
{
    public TType? Type { get; set; }

    public Fred()
    {
        name = "Fred(TModel, TType)";
        cssClass = "alert alert-dark";
    }
}

And then a demo page:

@page "/"
@using Microsoft.AspNetCore.Components.Rendering;
@using SO73613449.Data;

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

@TestFirst

@TestSecond

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

@code {
    private RenderFragment TestFirst => __builder =>
        {
            __builder.OpenComponent<Fred<int>>(0);
            __builder.CloseComponent();
        };

    private RenderFragment TestSecond => __builder =>
        {
            __builder.OpenComponent<Fred<int, string>>(0);
            __builder.CloseComponent();
        };
}

And here's what it looks like:

enter image description here

The alternative is to just give them two names!

Related