6

Blazor Web App Template, selecting 'None' for the Interactive Render mode.

To begin my understanding of Static Server-Side Rendering (SSR), as it is implemented in Blazor compared to that of MVC or Razor pages, I thought I might implement the missing Counter component, similar to the interactive versions of the template.

This code seems to work fine, without query string params, cookies or JS interop for using browser storage (all approaches I considered)...

@page "/counter"

<PageTitle>Counter - BlazorSSR</PageTitle>

<h1>Counter</h1>

<p>Current count: @CurrentCount</p>

<EditForm Enhance FormName="counterForm" Model="CurrentCount" OnSubmit="IncrementCount">
    <InputNumber class="d-none" @bind-Value="CurrentCount" />
    <button class="btn btn-primary" type="submit">Click me</button>
</EditForm>

@* Thanks to Ruikai Feng's answer, this is interchangeable with the above EditForm:
<form data-enhance method="post" @formname="counterForm" @onsubmit="IncrementCount">
    <AntiforgeryToken />
    <input type="number" @bind-value="@CurrentCount" name="CurrentCount" hidden />
    <button class="btn btn-primary" type="submit">Click me</button>
</form>
*@

@code {
    [SupplyParameterFromForm]
    public int CurrentCount { get; set; }

    private void IncrementCount()
    {
        CurrentCount += 1;
    }
}

My questions:

  1. Why does this work? It appears to me the IncrementCount() method is running per the OnSubmit, and the resulting form-submission is being supplied as a parameter back to the same page, successfully incrementing the CurrentCount. Is that correct?

  2. Is this the correct and simplest approach for implementing the Counter page? Just because it works, doesn't mean it's correct.

  3. For the InputNumber, I would have preferred a standard HTML input with type="hidden", but couldn't find the correct way to bind the CurrentCount. I realize I'm not using a traditional 'model' here, and I'm just using a native int. Seemed silly to create an entire model just for a single int.

  4. Having the understanding of MVC, with POST-Redirect-Get, what's the workflow here? I'm not injecting the NavigationManager, so after the OnSubmit runs IncrementCount(), is it just reloading the same page, and somehow taking the CurrentCount from my form submission and feeding it back into itself?

  5. So what's the pattern here? If I redirected to another page, can I pass the form submission data into the other page, or do I have to stay on the current page, do something with the data, and then redirect to another page?

  6. Because this is SSR, I expect, in terms of DI, that Scoped and Transient mean the same thing. So had I injected a service to keep track of the CurrentCount, only a Singleton would work, at the expense of the Counter value being the same for everyone.

Update:

While Ruikai Feng's answer is helpful, I need to address my fundamental misunderstanding for the Blazor SSR form submission workflow, as with static server-side rendering, the workflow is quite different compared with a page having interactivity.

To that end, I created a page slightly more complex than the Counter page, albeit a simple example to demonstrate the workflow.

@page "/FormLifecycle"

<PageTitle>Form Lifecycle - BlazorSSR</PageTitle>

<div class="row">
    <div class="col-12 col-lg-6 mx-auto">
        <h1>Form Lifecycle Testing</h1>
        
        <EditForm class="mb-2" Enhance Model="Person" FormName="personForm" OnValidSubmit="SubmitPerson">
            <DataAnnotationsValidator />
            
            <div class="input-group mb-3">
                <span class="input-group-text" for="firstName">First Name</span>
                <InputText id="firstName" class="form-control" @bind-Value="Person.FirstName" />
            </div>
            <ValidationMessage For="@(() => Person.FirstName)" class="mb-3 text-danger" style="margin-top: -16px;" />

            <div class="input-group mb-3">
                <span class="input-group-text" for="lastName">Last Name</span>
                <InputText id="lastName" class="form-control" @bind-Value="Person.LastName" />
            </div>
            <ValidationMessage For="@(() => Person.LastName)" class="mb-3 text-danger" style="margin-top: -16px;" />

            <button class="btn btn-primary" type="submit">Submit</button>
        </EditForm>
        
        @if (PersonModel.IsModelValid(Person, out _))
        {
            <p>Submitted: @Person.FirstName @Person.LastName</p>
        }
    </div>
</div>

@code {
    [SupplyParameterFromForm(FormName = "personForm")]
    public PersonModel Person { get; set; } = new();

    protected override void OnInitialized()
    {
        Console.WriteLine("OnInitialized()");
    }

    protected override void OnParametersSet()
    {
        Console.WriteLine("OnParametersSet()");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        Console.WriteLine("OnAfterRender()");
    }

    private void SubmitPerson()
    {
        Console.WriteLine("SubmitPerson()");

        if (PersonModel.IsModelValid(Person, out _))
        {
            Console.WriteLine($"Submitted: {Person.FirstName} {Person.LastName}");
        }
    }
}

The output is as follows:
OnInitialized()
OnParametersSet()
[I filled in the form and pressed submit]
OnInitialized()
OnParametersSet()
SubmitPerson()
Submitted: Homer Simpson

I expected the SubmitPerson() method to be run before reloading the page, not after.

Suppose I was displaying a list of people from a database on the same page as the form. It seems to me, this would reload the same page over again, then submit the new person... leaving it to me to reload the page a third time, OR fake it by manually adding the submitted person to the list of people from the database, without actually reloading the page or reloading database results.

I may have answered my own question with this little experiment, just it feels the answer is not as intuitive as I would have thought. My aim is to utilize static server-side rendering as much as possible, to avoid/minimize the need for web sockets or client-side activity, as much as possible.

3
  • Upon further debugging with a more complex example than Counter, it seems submitting the form first reloads the page, i.e. runs OnInitialized(), then runs the method specified in OnSubmit on the new/refreshed page. So you have to think ahead, that submitting a form will reload the page and run OnInitialized() again. This seems less than intuitive at the moment. Hoping someone is able to make sense of this workflow. Commented Nov 29, 2023 at 6:20
  • What is the definition of the PersonModel? What are the limitations in using SSR? For example, can I use Blazor component? Is there any example of a Blazor application created only using SSR? Commented Dec 6, 2024 at 12:02
  • 1
    @Enrico, in this example, PersonModel is a class with two string properties, FirstName and LastName. That is all, nothing special! And yes, you can create a Blazor app using SSR-only. Using Visual Studio, create a new Blazor Web App, ensure you are using .NET 8 or newer, and for the "Interactive Render Mode", choose "None". Be sure to check the box to include sample pages in your case. And that will create an example for you. Using the answer to this question, you will also be able to add a Counter component, which works with SSR-only. Enjoy! Commented Dec 7, 2024 at 5:06

3 Answers 3

4

Using Blazor static server-side rendering, here is my rendition of the Counter page:

@page "/counter/{reset:bool?}"
@inject NavigationManager NavMan

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p>Current count: @CurrentCount</p>

<form class="d-inline" data-enhance method="post" @formname="counterForm" @onsubmit="IncrementCount">
    <AntiforgeryToken />
    <input type="number" @bind-value="@CurrentCount" name="CurrentCount" hidden />
    <button class="btn btn-primary" type="submit">Click me</button>
    <a class="btn btn-secondary" href="/counter/true">Reset</a>
</form>

@code {
    [Parameter]
    public bool Reset { get; set; }
    
    [SupplyParameterFromForm(FormName = "counterForm")]
    public int CurrentCount { get; set; }

    protected override void OnInitialized()
    {
        if (Reset)
        {
            NavMan.NavigateTo("/counter");
        }
    }

    private void IncrementCount()
    {
        CurrentCount += 1;
    }
}

And answers to my own questions:

Note 1: When you submit the form, the form data is loaded into the model annotated with [SupplyParameterFromForm].

In the counter example, there is only an int/primitive variable, not really a model, but the framework seems to handle this scenario, hence no Model="modelName" attribute is required on form tag. In your average scenario, there would be a Model attribute specified.

Note 1a: I've observed in some of the Microsoft samples, when there is no model, only a primitive, I've seen some folks use Model="new()", although it doesn't seem to be required.

Note 2: If you have multiple forms on the page, thus multiple models, you should annotate each model [SupplyParameterFromForm(FormName = "xyzForm")]. Careful as SupplyParameterFromForm has a 'Name' property, which isn't the right one, it's 'FormName'.

Note 3: After pressing submit, presuming the form/EditForm passes validation, it will populate the form data into the model AND the page will reload with the OnInitialized/Async() method.

As remarked in the question, the flow is this:

OnInitialized()  
OnParametersSet()  
[User Submits form]  
OnInitialized()  
OnParametersSet()  
Method specified in On[Valid]Submit attribute will run.  

This means in OnInitialized(), you must handle the scenario where your data model is already populated, i.e. take care not to re-initialize it. This is where the null-coalescing operator is helpful [??=], e.g. person ??= new(). This is because your OnInitialized needs to handle both an initial render, as well as a render for form submission. This wasn't immediately obvious to me.

This can be observed in the examples from the Blazor documentation for .NET 8: https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/?view=aspnetcore-8.0

Note 4: As for the traditional MVC pattern of GET-POST-REDIRECT, it's simply a matter of, what do you want to do in the method specified in your On[Valid]Submit attribute? Do you want to stay on the same page, or do you want to redirect somewhere else?

If you want to redirect somewhere else, inject the NavigationManager and use the NavigateTo() method. This way, you can take action with the form submission model data, and then redirect somewhere else, if that is your requirement.

Note 5: Running in debug mode, you'll receive a 'NavigationException' from this line: NavMan.NavigateTo("/counter");. This is actually by design [, unfortunately]. Reference https://github.com/dotnet/aspnetcore/issues/50478 for more info.

Note 6: On the topic redirecting, let's say you have a button on your form, or a link, which will redirect the user to another page. If you don't need to take any action other than redirecting, you need not a form for this, you can just use an anchor tag, e.g. from the Counter page, the reset button:

<a class="btn btn-secondary" href="/counter/true">Reset</a>

The OnInitialized() method of the Counter page will recognize when the Reset route parameter is populated and reload the page to effectively 'reset' the count.

Perhaps MVC seems more intuitive, purely because it was learned before Blazor, and the idea of a Controller vs a code-behind helps to think of server-side vs view concerns separately, but it is really a matter of orienting yourself to Blazor, when working purely in static SSR, that the code section is running server-side, and the markup is the client-side view.

Note 7: One additional item that I see common. When submitting a form, in order for the On[Valid]Submit method to run, the form must be rendered in this workflow:

OnInitialized()  
OnParametersSet()  
[User Submits form]  
OnInitialized()  
OnParametersSet()  
[Form being submitted must be rendered on the page]  
Method specified in OnSubmit/OnValidSubmit runs.

Essentially, if you have some conditional logic that excludes the form being submitted from rendering, then you'll get this error, "Cannot submit the form 'formName' because no form on the page currently has that name."

This was not immediately obvious to me, due to general misunderstanding of this static server-side rendered workflow.

Sign up to request clarification or add additional context in comments.

5 Comments

How are you getting NavMan.NavigateTo("/counter"); to work?
Are you referring to the NavigationException? If so, it's unfortunately by design. Reference github.com/dotnet/aspnetcore/issues/50478, "That exception is launched and caught internally by the framework, the behavior is by design. You can avoid pausing on it if you turn off first chance exceptions." Though you can just ignore this particular instance of the exception, rather than all first-chance exceptions. Let me know if you're referring to something else, otherwise this is working in a demo project on my end. If necessary, I can share a repo.
Thanks Brian, I got it figured out. I had brain fart last night.
Brian, does note 7 mean I can't create a <form action="login/submit"> in interactive rendering mode login page(@page "/login") to process this form on loginSubmit page(@page "/login/submit") with static SSR mode? I trying to do this and have the same error: "Cannot submit the form 'formName' because no form on the page currently has that name."
@likquietly, I wrote Note 7 in terms of static-SSR, only. If some conditional logic preventing the form being submitted from being rendered on the server, than you would receive the error. In interactive mode, typically a form has an on[valid]submit handler, so I wouldn't anticipate getting that error. It would be helpful if you posted your question on SO with sample code, and placed a link here, and I will take a look.
2

For Q1,Q2:

You could Press F12 and check NetWork in your browser,you are sending http request to server instead of interacting with SignalR as usual : enter image description here

enter image description here

For Q3,if you wish it work in SSR,you may try:

<input type="number" @bind-value="@CurrentCount" name="CurrentCount" hidden/>

For Q4: There's no POST-Redirect-Get here,it just send the rendered html codes back as the response of your post request

For Q5: You could check the document related with Blazor state management

For Q6: Scoped and Transient never mean the same thing,if you register a service as Scoped,it would keep the same instance per http request in asp.net core ,if you register it as Transient,once you inject them into different components/services,you would get different instance

2 Comments

My questions indeed have some overlap, and I've done some further debugging since asking the question. Obviously I understand there's no POST-Redirect-Get here, but I've come to understand that a form submission using SSR-only seems to reload the same page, running OnInitialized() again, and then runs the method indicated by the OnSubmit from the form. Is that observation correct? This means (to me) that you must be careful in your OnIntialized() method, especially when you're trying to POST some update to your backend and update your frontend view.
You indeed answered my question regarding using a standard HTML form with an input, for which I've updated my question to include the alternative to the EditForm. However, to me, the workflow described in my above comment is unclear in the Blazor documentation I've viewed thus far-- that submitting a form results in calling OnInitlized() over again. Also, you're answer to Q6, I understand the difference in general, but require some context with Blazor SSR. On an SSR page, there seems to be no difference between Scoped and Transient.
0

The post handler itself relies on form input binding '[SupplyParameterFromForm]'. The common pain point is the redirect part: oftentimes we need to persist some kind of temp data between posts and redirects, be it same page or multi page. A classic example is a 2 stage form.

In all the SSR examples, it was typically done via query strings, which is okay for some simple non-sensitive value. But if one wants to not reply on that, there is the 'TempData' alternative. See https://github.com/mq-gh-dev/blazor-ssr-tempdata

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.