3

Objective

I have a form on my page that will call the method OnFinish when the user clicks the button to send the form out. In that method, I want to add new markup to the page. (The code for the form is not relevant, so I won't add it on here)

Problem

There does not seem to be a simple way (if any) to add razor markup from C# code.

What I've tried

The first thing I tried was straight markup in the C# Code. That looks like this:

private void OnFinish(EditContext editContext)
{
    <a>Placeholder</a>
}

This did - of course - not work, because I was writing it in a .cs file.


In the .razor file I tried doing the same:

@code
{
    private void OnFinish(EditContext editContext)
    {
        <a>Placeholder</a>
    }
}

The compiler didn't like this either and raised the following error:

The name '__builder' does not exist in the current context

I quickly learned from this question, that markup cannot be written in a @code block.


According to this official ASP.NET blog post, writing markup in a @functions block should work:

@functions
{
    private void OnFinish(EditContext editContext)
    {
        <a>Placeholder</a>
    }
}

This, unfortunately, had the same outcome as using a @code block.

EDIT: After reading through the blog post again I realised, that the feature only works in .cshtml files. I tested this and, indeed, it works in .cshtml files. But these files cannot be used as components, so it doesn't work for me after all.


I tried using a RenderTreeBuilder __builder as a parameter in the method, which was suggested in this answer.

The problem with this solution is, that the __builder parameter is only available from within the markup code like this:

<a>@__builder</a>

and not from the C# code (that's why the parameter is used).


The next thing I found was the @helper block, which is mentioned in this answer. I applied this to my code like this:

@helper OnFinish(EditContext editContext) 
{
    <a>Placeholder</a>
}

This does not work either, as the compiler raises the following two errors:

The helper directive is not supported.

The name 'helper' does not exist in the current context

Conclusion

Now, this is where the search comes to an end. I spent so much time researching this, that I want to believe that there might not be any solution to this at all. What have I missed?


EDIT

Some have mentioned the idea of putting the markup in an if, while the if gets then activate when some condition comes true (e.g. I have a bool that I set to true or I change a variable and the if recognizes that).

That solution can be seen in either this other question or this answer in the current thread.

The code for that solution would look like this:

if (renderNewMarkup)
{
    <a>Placeholder</a>
}
@functions
{
    private void OnFinish(EditContext editContext)
    {
        renderNewMarkup = true;
    }
}

This does work but it's very unpleasant in my opinion. The problem I have with this is the use of variables.

In my program, I retrieve some data from the UI and would pass it further to the then created markup. I can give you an example:

private void OnFinish(EditContext editContext)
{
    Dictionary<string, string> dict = new()
    {
        ["Key1"] = boundVariable,
        ["Key2"] = otherBoundVariable,
        ["Key3"] = anotherBoundVariable
    }
    
    // Create markup with dict variable
}

The markup I want to add to the HTML is a custom razor component that has a Dictionary<string, string> parameter.

You can see that I now can't just simply create the markup there. What I have to achieve what I want would look like this:

@if (renderNewCustomComponent)
{
    <Custom DictParameter="@dict" />
}

@code
{
    private Dictionary<string, string> dict;

    private void OnFinish(EditContext editContext)
    {
        dict = new()
        {
            ["Key1"] = boundVariable,
            ["Key2"] = otherBoundVariable,
            ["Key3"] = anotherBoundVariable
        }
    
        renderNewCustomComponent = true;
    }
}

I hope you see what I mean by this.


Then there's an even bigger problem with this solution. When I want to call a method, e.g. AddElementToHTML(), that will always add some element to the HTML, that would not be possible. Yes, with some complex way, with a @foreach or something, that would be possible, but why wouldn't the language just allow that in the first place?

baltermia
  • 1,151
  • 1
  • 11
  • 26
  • Does this answer your question? [How to render a component on button click in Blazor?](https://stackoverflow.com/questions/63839051/how-to-render-a-component-on-button-click-in-blazor) – Peter B Aug 03 '21 at 08:15
  • @PeterB see my edit. – baltermia Aug 03 '21 at 11:52
  • In all your first attempts, without an `@if` , how would you control _where_ the new HTML shows up in your page? – H H Aug 03 '21 at 11:53
  • @HenkHolterman hmm I never really thought of that, I was just assuming that it would go at the end. Or that I can put the C# code where I want the element to be at, e.g I place a `@functions` block in a `div` and it would add it there (example 3). – baltermia Aug 03 '21 at 11:56
  • `@functions` is just a very old version of `@code` . – H H Aug 03 '21 at 12:11
  • @HenkHolterman yeah... what I found out is, that you can have markup in the `@functions` block in a `.cshtml` file, but not in a `.razor` file. – baltermia Aug 03 '21 at 12:23
  • Once upon a time Blazor components/pages were also placed in .cshtml files. So be aware of old Blazor info vs Razor pages. – H H Aug 03 '21 at 12:36
  • @speyck what are you hoping to achieve with all this placeholder business that you can't do by making a .razor component and passing your data to it? – Bennyboy1973 Aug 03 '21 at 13:28
  • @Bennyboy1973 I can do that but I only want to create the element after the `Form` is closed. I pass data from the `Form` into the razor component. The placeholder is just to not confuse things too much. – baltermia Aug 03 '21 at 13:38

4 Answers4

2

Original Answer

This shows you how to build and display a simple render fragment from markup. It may help.

@page "/Test"
<h3>Test8</h3>
<input @bind-value="content" class="form-control" />
<button class="btn btn-dark" @onclick="RenderSomeContent">Render Content</button>
<button class="btn btn-danger ml-2" @onclick="ClearContent">Clear Content</button>

@Placeholder

@code {
    RenderFragment Placeholder { get; set; }

    string content = "<div class='m-3 bg-success'>Hello</div>";

    void RenderSomeContent()
    {
        Placeholder = (builder) =>
        {
            builder.AddMarkupContent(0, content);
        };
    }

    void ClearContent()
    {
        Placeholder = null;
    }
}

Update on comment

BinaryLogicControl
@if (this.Show)
{
    if (this.ChildContent != null)
    {
        @this.ChildContent
    }
    else
    {
        @((MarkupString)this.Body)
    }
}

@code {
    [Parameter] public bool Show { get; set; }
    [Parameter] public RenderFragment ChildContent { get; set; }
    [Parameter] public string Body { get; set; }
}
TestPage
@page "/Test"

<button class="btn btn-secondary" @onclick="DoIt">Toggle content</button>

<BinaryLogicControl Show="show">
    <div class='m-3 bg-success'>Hello</div>
</BinaryLogicControl>
<BinaryLogicControl Show="show" Body="@Display" />

@code {

    string Display = string.Empty;
    bool show;

    void DoIt()
    {
        show = !show;
        Display = "<div class='m-5 p-3 bg-danger'>Hello</div>";
    }
}

Update Based on Rendering a component

Based on the comments, what you want is the DynamicComponent coming in Net 6. At present you have to build your own (simpler) version. The code below shows an updated version of the original answer that builds out the BinaryLogicControl for rendering rather than a simple Markupstring.

@page "/Test"

<button class="btn btn-primary" @onclick="RenderMe">Render Control</button>

@Placeholder

@code {
   RenderFragment Placeholder { get; set; }

    void RenderMe()
    {
        RenderSomeContent(typeof(BinaryLogicControl), new Dictionary<string, object> { 
           { "Show", true }, 
           { "Body", "<div class='m-3 bg-warning'>Hello</div>" }
        });
    }

    void RenderSomeContent(Type componentType, Dictionary<string, object> parameters)
    {
        Placeholder = (builder) =>
        {
            builder.OpenComponent(0, componentType);
            foreach (var parameter in parameters)
            {
                builder.AddAttribute(1, parameter.Key, parameter.Value);
            }
            builder.CloseComponent();
        };
    }
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Very nice answer, and I upvoted it. But my question would be-- in what context would doing this actually be better than just having a component with the same markup? I mean, technically, you could build a complete renderer of your own using a collection of C# string manipulations and serve it from a Windows machine. – Bennyboy1973 Aug 03 '21 at 17:34
  • @Bennyboy1973 - as you've probably already guessed, none! I wouldn't do it this way. I was originally going to answer this question by proposing to build a `BinaryLogicControl` but desisted as It might be considered over the top. See Habib Hassan's PrettyBlazor for how to do more complex if+. I personally hate seeing lots of markup and logic code mixed up, so I'm a component for everything man. As is almost always the case with questions, we only see the top of the iceberg, so I added the suggestion to show how to create the placeholder. BTW thanks. – MrC aka Shaun Curtis Aug 03 '21 at 19:16
  • @MrCakaShaunCurtis thanks for the answer. This looks like it could do the work. I still have one question though: If I would send the `RenderSomeContent` method some parameters and would add them into the string, would that still be passed over? Doesn't seem like it would to me but maybe it does somehow.. Also, that PrettyBlazor class is really nice. If I go with any of the other solutions in the thread, I'll definitely use that. Could you maybe also elaborate on the `BinaryLogicControl` thing you talked about? – baltermia Aug 04 '21 at 09:24
  • @speyck. See updated answer. Can you give us a specific example of "send method some parameters"? – MrC aka Shaun Curtis Aug 04 '21 at 10:02
  • @MrCakaShaunCurtis I mean with that, that the `RenderSomeContent` method would have some parameters. The signature could look like this: `void RenderSomeContent(string value1, int value2)`. I'm passing the method two values and the method would then add the new markup with these values, e.g: ``. Also thanks for adding the other stuff to the answer. – baltermia Aug 04 '21 at 11:01
  • @speyck. Is `SomeComponent` Html markup (say a div) or as I think your example implies a Blazor Component. They are distinctly different and need handling differently. – MrC aka Shaun Curtis Aug 04 '21 at 11:13
  • @MrCakaShaunCurtis yes it is a custom blazor component. Maybe I should have mentioned that in my question? – baltermia Aug 04 '21 at 11:18
  • Yes, common problem - tip of the iceberg. I've updated my answer to show you how to build out a component dynamically. The new `DynamicComponent` is coming in Net6. – MrC aka Shaun Curtis Aug 04 '21 at 12:40
1

This is only a partial answer I think. But inline markup in a function is possible, just a little awkward because of the extra {}. Which you can merge with an @if() or other control structure:

@{ void F1() { <p>Inline Markup (local fn)</p> } }

@{ F1(); }

@if (2 > 1)
{
    F1();
    F2(__builder);
}

@code {

    void F2(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
    {
        <p>Inline Markup </p>
    }
}

Consider that the markup section of a page is compiled to an override void BuildRenderTree(builder) {...}.

You can add a local function (F1) or call another method from your class (F2).

This does not solve your OnFinish(editContext) problem though because that is called outside the rendering code path.

So you will still need an @if() .

H H
  • 263,252
  • 30
  • 330
  • 514
  • If you take a look at my 4th example, you can see that I have already tried having the `__builder` parameter in my method. The first method you mention would actually work, but I can't call it from the C# code. So yes, in the end a `@if` is still needed... – baltermia Aug 03 '21 at 12:20
  • Yes. You might as well use direct markup inside an @if. But also consider that apparently you are using OnFinish() in a very different way than the author(s) intended. – H H Aug 03 '21 at 12:22
  • What do you mean by that? – baltermia Aug 03 '21 at 12:23
  • I don't know where it comes form. Maybe look up some examples of how others are using it. – H H Aug 03 '21 at 12:24
  • @Henk OnFinish() seems to be a custom event from Telerik UI controls or some other custom UI library. – Bennyboy1973 Aug 03 '21 at 13:30
  • @Bennyboy1973 you are right, I am using AntDesign. I didn't fully understand what Henk was trying to tell me.. – baltermia Aug 03 '21 at 15:14
  • 1
    Okay, really take my advice. Forget about "adding markup" and create components with whatever markup you'd like, then show the components based on logic conditions. Use [Parameter] in your components to allow you to pass data to them (for example, your editform model). – Bennyboy1973 Aug 03 '21 at 15:35
0

Razor code uses control structures like if, foreach and so on. You can build an HTML string however you like, and use a conditional to let Blazor know when to display it.

@if (CustomHtml is not null) {
    <div>@((MarkupString)CustomHtml)</div>
}

@code
{
    string CustomHtml; // starts as null
    private void OnFinish(EditContext editContext)
    {
        CustomHtml = "<b>You are now registered!</b>";
    }
}

Generally, for adding controls, you'd include a .razor Component in your conditional logic. Components can be nested, so there's really no limit to what you can build.

Bennyboy1973
  • 3,413
  • 2
  • 11
  • 16
0

In the function you have to manipulate the editContext or return something.

Pseudo-code example with return:

@functions
{
   private string OnFinish(EditContext editContext)
   {
      return "<a>Placeholder</a>";
   }
}

Pseudo-code example with manipulate:

@functions
    {
       private void OnFinish(EditContext editContext)
       {
          editContext.change("<a>Placeholder</a>");
       }
    }
tartarismo
  • 194
  • 1
  • 10
  • I can't make the method a `string` because the razor component only wants a delegate that is a `void`. Also, `editContext` does not have a definition for any `change()` method so that does not work either. – baltermia Aug 03 '21 at 10:49