Aspnetcore: Blazor: Allow Storing Markup into RenderFragment

Created on 6 Mar 2020  路  23Comments  路  Source: dotnet/aspnetcore

Is your feature request related to a problem? Please describe.

Right now, dynamically generating markup is very painful.

https://github.com/dotnet/aspnetcore/issues/16104#issuecomment-385713669

We'll implement nicer APIs to build RenderFragments in the future, but for now you can:

@CreateDynamicComponent();

@functions {
    RenderFragment CreateDynamicComponent() => builder =>
    {
        builder.OpenComponent(0, typeof(SurveyPrompt));
        builder.AddAttribute(1, "Title", "Some title");
        builder.CloseComponent();
    };
}

Those are very low-level APIs (not even documented) so we hope not many people need to do this right now. Higher-level APIs for this will come later.

This limitation becomes crippling when we are attempting to create a library which allows rendering custom user-defined markup in-between fixed components.

Describe the solution you'd like

Blazor should expose JSX-like syntax as a high level API for dynamically creating / storing markup in variables:

@x
@y

@code {
    RenderFragment x = <MyComponent></MyComponent>;
    RenderFragment y = <div>Hello World!</div>;
}

This will enable a nicer API for dynamic component generation.

Additional context

Related issues:
https://github.com/dotnet/aspnetcore/issues/14599
https://github.com/dotnet/aspnetcore/issues/15973

Related articles:
https://reactjs.org/docs/introducing-jsx.html

affected-medium area-blazor enhancement severity-minor

Most helpful comment

You need to insert an @ at the start of your markup, then it works.

@x
@y

@code {
    RenderFragment x = @<MyComponent></MyComponent>;
    RenderFragment y = @<div>Hello World!</div>;
}

All 23 comments

I'd love to see a real life example of how you intend to use this.

You need to insert an @ at the start of your markup, then it works.

@x
@y

@code {
    RenderFragment x = @<MyComponent></MyComponent>;
    RenderFragment y = @<div>Hello World!</div>;
}

@NTaylorMullen is the last recommendation above what we should tell customers to use?

Thank you for that @ suggestion. I can't believe that actually worked.

Is this actually officially supported?

<p>@Value</p>

@code {
    [Parameter]
    public string Value { set; get; }
}
@x

@y

@code {
    RenderFragment x = @<div>OMG</div>;

    RenderFragment y = @<Test Value="OMG2"></Test>;
}

On related note, this does NOT work:

    RenderFragment z = @<div>@x</div>;
Error CS0236 A field initializer cannot reference the non-static field, method, or property 'SomeComponent.x'

How to make example z work?

Yes it does, if you make x static.

I have no idea whether it's officially supported or just a bonus of the inner workings of the Blazor code generator / compiler.

    private static RenderFragment x = @<div>Nested</div>;
    private RenderFragment z = @<div>Hello @x</div>;

Now that's very sketchy to me.

The fact that the inner RenderFragment has to be static to be usable in the outer RenderFragment means that we cannot use variables local to the component 馃槩

Example: This will also error:

    string LocalVariable = "WOW";

    RenderFragment x = @<div>@LocalVariable</div>;

Well what you're writing there is a normal C# class and in there you write fields. For that you have to obey the initialization rules of C#. You're of course free to Create a method to create x and z to access local fields:

@CreateZ()

@code {
   string local = "WOW";
   RenderFragment CreateX()
   {
       return @<div>@local</div>;
   }
   RenderFragment CreateZ()
   {
      return @<div>Hello @CreateX()</div>;
   }
}

Fascinating! Who would've thought making them as methods allow them to access local variables...

(EDIT: I just realized that Blazor @code markup defines a class and the field is NOT a "local variable". FML)

This should be a sweeter syntax:

@Y

@code {
    string LocalVariable = "WOW";

    RenderFragment X => @<Test Value="@LocalVariable"></Test>;

    RenderFragment Y => @<div>@X</div>;
}

@mkArtakMSFT If an MS folk can bless this syntax as the official way of storing markup into RenderFragment, feel free to close the thread.

This should probably be documented somewhere in the Blazor docs.

@ryanelian I am curious. Why are you doing it like this instead of writing a component that accepts [Parameter] decorated RenderFragment properties?

Our company is developing a custom component which renders other components in a fixed layout.

Consider this custom component as something like a Rapid Application Development framework / platform / web application designer.

While this custom component sounds rigid in theory, in practice it has been proven to aid development of mostly-bugs-free apps in 10x the normal speed. (Any generic app which requires 8 hours to develop should be done in an hour or less)

As said, this custom component renders other components in a fixed layout, meaning it is not very flexible.

We are planning to introduce flexibility for inserting custom-programmer-defined components in-between fixed components using the RenderFragment feature discussed in this thread.

Imagine this:

[Parameter]
public Dictionary<string, RenderFragment> RenderBefore { set; get; }

[Parameter]
public Dictionary<string, RenderFragment> RenderAfter { set; get; }

image

Wouldn't it be neater to define those render fragments in .razor markup though rather than in C#?

Well, yes.

It's neater to define those RenderFragment using Razor syntax instead of using builder => builder.OpenElement syntax. Hence this issue / thread:

@Y

@code {
    string LocalVariable = "WOW";

    RenderFragment X => @<Test Value="@LocalVariable"></Test>;

    RenderFragment Y => @<div>@X</div>;
}

The render fragments are defined in the .razor files, no?

How about creating a layout like this? It could then cascade the dictionary value and consumers could pick it up using [CascadingParameter]

<MyComponent>
  <ChildContent>
    @body
  </ChildContent>
  <FragmentDefinitions>
    <FragmentDefinition Key="BeforeD">
      This is the fragment
    </FragmentDefinition>
    <FragmentDefinition Key="AfterD">
      This is the fragment
    </FragmentDefinition>
  </FragmentDefinitions>
</MyComponent>

We've thought about that but have serious performance concern.

Imagine there are 20 fixed components and you are inlining 5 components inbetween those components.

With the Dictionary implementation, you are doing O(1) comparisons 20 times. Easy. Just render if there is a match.

With the child content / templated component implementation, the child content must be rendered 20x BEFORE AND AFTER the fixed components, AND there will be if == comparison happening 5x for each render AKA O(n * m)

Ultimately we are too paranoid to go for the child content / templated component implementation 馃槗

Holy cow I figured out a better way to do all these.

Let us know, and we can give feedback

Hey Guys - what about this? I cannot find a way to code this so it will compile. It complains about the way @onclick is being handled, giving the following error during compilation. This code works perfectly without onclick being defined.

Severity Code Description Project File Line Suppression State
Error CS1662 Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type RugAttributeApp.Client C:Code_GIT\Unitech\RugAttributeAppClient\obj\Debug\netstandard2.1\RazorDeclaration\Pages\Details.razor.g.cs 274 Active

@code { private static void CheckChanged() { Console.WriteLine("here"); } private RenderFragment<dynamic> multiSelectTemplate = (IdDescriptionListing) => @<div> <div class="multiSelectItems"> @foreach (dynamic objectItem in objectListing) { <input type="checkbox" @onclick="@(() => { CheckChanged(); })" checked="@(((List<dynamic>)IdDescriptionListing).Where(d => d.Id == objectItem.Id).FirstOrDefault() != null)" /> @objectItem.Description <br /> } </div> <div id="test" class="multiSelectItemsText csv"> @foreach (dynamic objectItem in objectListing) { @if (((List<dynamic>)IdDescriptionListing).Where(d => d.Id == objectItem.Id).FirstOrDefault() != null) { <span>@objectItem.Description</span> } } </div> </div>; }

I get this error message

error CS1503: Argument 2: cannot convert from 'bool' to 'Microsoft.AspNetCore.Components.EventCallback'

with this kinda code:

private RenderFragment SomeMethod(object someArg) {
   return @<SomeComponent SomeEventCallback="@(x => { })" />;
  }

It works as expected if I inline it.

I like this style of coding. It feels very natural to be able to drop repetitive bits of markup into a function. However, I'm wondering, is this generally considered a no-no in Blazor due to the sequence numbers?

For example, a component like this:

<Row>
    <Column>
        @Card("Title 1")("Text 1.")
    </Column>
    <Column>
        @Card("Title 2")("Text 2.")
    </Column>
</Row>

@code {
    private Func<string, RenderFragment> Card(string title)
    {
        return text =>
            @<Card>
                <CardBody>
                    <CardTitle>
                        @title
                    </CardTitle>
                    <CardText>
                        @text
                    </CardText>
                </CardBody>
            </Card>;
    }
}

Seems to produce sequence numbers like this:

  • 1-6 (Row, Column, function call)
  • 15-33 (function result)
  • 7-12 (closing markup, Column, function call)
  • 15-33 (function result)
  • 13-14 (closing markup)

Is that a problem?

As long as they are unique per parent it's fine

@mrpmorris Do you mean unique within their immediate parent component? If so, does that make the following problematic?

<Column>
    @Card("Title 1")("Text 1.")
    @Card("Title 2")("Text 2.")
</Column>

Does this become like @foreach without a @key? Would using @key on the first component in the function help in this scenario?

If you can paste the generated C# code that would be useful

@mrpmorris Sorry for the delay. For:

<div class="container">
    <div class="row">
        <div class="col">
            @Card("Title 1")("Text 1.")
            @Card("Title 2")("Text 2.")
        </div>
    </div>
</div>

@code {
    private Func<string, RenderFragment> Card(string title)
    {
        return text =>
            @<Card>
                <CardBody>
                    <CardTitle>
                        @title
                    </CardTitle>
                    <CardText>
                        @text
                    </CardText>
                </CardBody>
            </Card>;
    }
}

The decompiled code is:

protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
    __builder.OpenElement(0, "div");
    __builder.AddAttribute(1, "class", "container");
    __builder.AddMarkupContent(2, "\n    ");
    __builder.OpenElement(3, "div");
    __builder.AddAttribute(4, "class", "row");
    __builder.AddMarkupContent(5, "\n        ");
    __builder.OpenElement(6, "div");
    __builder.AddAttribute(7, "class", "col");
    __builder.AddMarkupContent(8, "\n            ");
    __builder.AddContent(9, Card("Title 1")("Text 1."));
    __builder.AddMarkupContent(10, "\n            ");
    __builder.AddContent(11, Card("Title 2")("Text 2."));
    __builder.AddMarkupContent(12, "\n        ");
    __builder.CloseElement();
    __builder.AddMarkupContent(13, "\n    ");
    __builder.CloseElement();
    __builder.AddMarkupContent(14, "\n");
    __builder.CloseElement();
}

private Func<string, RenderFragment> Card(string title)
{
    return (string text) => delegate(RenderTreeBuilder __builder2)
    {
        __builder2.OpenComponent<Card>(15);
        __builder2.AddAttribute(16, "ChildContent", (RenderFragment)delegate(RenderTreeBuilder __builder3)
        {
            __builder3.AddMarkupContent(17, "\n                ");
            __builder3.OpenComponent<CardBody>(18);
            __builder3.AddAttribute(19, "ChildContent", (RenderFragment)delegate(RenderTreeBuilder __builder4)
            {
                __builder4.AddMarkupContent(20, "\n                    ");
                __builder4.OpenComponent<CardTitle>(21);
                __builder4.AddAttribute(22, "ChildContent", (RenderFragment)delegate(RenderTreeBuilder __builder5)
                {
                    __builder5.AddMarkupContent(23, "\n                        ");
                    __builder5.AddContent(24, title);
                    __builder5.AddMarkupContent(25, "\n                    ");
                });
                __builder4.CloseComponent();
                __builder4.AddMarkupContent(26, "\n                    ");
                __builder4.OpenComponent<CardText>(27);
                __builder4.AddAttribute(28, "ChildContent", (RenderFragment)delegate(RenderTreeBuilder __builder5)
                {
                    __builder5.AddMarkupContent(29, "\n                        ");
                    __builder5.AddContent(30, text);
                    __builder5.AddMarkupContent(31, "\n                    ");
                });
                __builder4.CloseComponent();
                __builder4.AddMarkupContent(32, "\n                ");
            });
            __builder3.CloseComponent();
            __builder3.AddMarkupContent(33, "\n            ");
        });
        __builder2.CloseComponent();
    };
}

I am working on a quiz game, and i would like to dynamically load each page from a database.
Is there a way to convert the as markup or a workaround, for components to be dynamically rendered (with some values preferable)?

example:

I know with Fragments and the builder you can render components, but I cannot really get a grip on.

Was this page helpful?
0 / 5 - 0 ratings