Skip to content
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

Add mechanism for interactively-rendered form to submit as HTTP request to SSR endpoint #53129

Open
1 task done
megafetis opened this issue Jan 4, 2024 · 7 comments
Open
1 task done
Labels
area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one Pillar: Complete Blazor Web Priority:1 Work that is critical for the release, but we could probably ship without

Comments

@megafetis
Copy link

megafetis commented Jan 4, 2024

Is there an existing issue for this?

  • I have searched the existing issues

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

I need to work with page in interactive mode, and after that submit to handle it with access to HttpContext.

I expect that model will be filled by [SupplyParameterFromForm] RegisterInputModel Model

after i submit form with javascript interop and invoke a function submit().

But in Server interactive mode the hidden value <input type="hidden" name="_handler" value="{FormName}" /> is removed.

My temporary working soution is:

<EditForm Model="Input" method="post" OnValidSubmit="LoginUser" FormName="login">
  
     @if(HttpContext == null)
    {
        <input type="hidden" name="_handler" value="login" />
    }


    @* form code and markup *@


        <div class="buttons">
            <BButton Class="is-solid primary-button is-fullwidth raised" Type="submit">Login</BButton>
        </div>

        
</EditForm>

@code {
[CascadingParameter]
private HttpContext? HttpContext { get; set; }

[SupplyParameterFromForm]
private LoginInputModel Input { get; set; } = new();

[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }

protected override async Task OnInitializedAsync()
{

    if (HttpContext != null && HttpMethods.IsGet(HttpContext.Request.Method))
    {
        await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
    }
}


public async Task LoginUser()
{
    if(HttpContext == null)
    {
      // interactive logic
        var ref1 = await Js.InvokeAsync<IJSObjectReference>("document.getElementById", "login-form");
        await ref1.InvokeVoidAsync("parentNode.submit");
        return;
    }

    // server submit logic
    var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
    if (result.Succeeded)
    {
        Logger.LogInformation("User logged in.");
        RedirectManager.RedirectTo(ReturnUrl);
    }
    else if (result.RequiresTwoFactor)
    {
        RedirectManager.RedirectTo(
            "Account/LoginWith2fa",
            new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
    }
    else if (result.IsLockedOut)
    {
        Logger.LogWarning("User account locked out.");
        RedirectManager.RedirectTo("Account/Lockout");
    }else
    {
        AppStore.State.NotAuthorizedModel = new () { Message = "Other error" };
    }
}

}

Describe the solution you'd like

solution 1: Do not remove hidden field _handler with FormName value and in interactive mode.

solution 2: Provide some API in EditContext to call submit with reload page.

Additional context

Sorry for my English...

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label Jan 4, 2024
@SteveSandersonMS SteveSandersonMS changed the title Blazor server: Invoke EditForm submit POST reload page to handle server logic with HttpContext after editing form in interactive mode. Add mechanism for interactively-rendered form to submit as HTTP request to SSR endpoint Jan 17, 2024
@SteveSandersonMS
Copy link
Member

Related to #51046

Thanks for the suggestion, @megafetis.

@ghost
Copy link

ghost commented Jan 17, 2024

Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@SteveSandersonMS SteveSandersonMS added Pillar: Complete Blazor Web enhancement This issue represents an ask for new feature or an enhancement to an existing one labels Jan 17, 2024
@mkArtakMSFT mkArtakMSFT added the Priority:1 Work that is critical for the release, but we could probably ship without label Jan 17, 2024
@Dryvnt
Copy link

Dryvnt commented Jan 22, 2024

I think it can be easy to underestimate the impact this seemingly simple issue can have on the new user experience. Until this is solved, one way or the other, I cannot recommend Blazor as a "great default option for new projects" as I otherwise have in the past.

I realize that nothing will happen code-wise until .NET 9, but even just mentioning this problem and workaround on learn.microsoft.com would be a huge improvement.


Pardon for being a bit dramatic. I have wasted the better part of two days exploring this, and was left quite frustrated (albeit a bit vindicated) when I finally found this issue. My primary points are those above the horizontal line; the rest of the comment is trying to provide context to back up those points. Again, sorry if it's a bit ramble-y/rant-y. It's been a long day.

I was prototyping Blazor 8 for a greenfield project, since "we can use simple static/streaming SSR + forms for the simple non-interactive bits, client-side interactivity for the basic bits, and fall back to server interactivity as a last resort for the complex bits" sounds very promising.

Unfortunately, adding just a little bit of interactivity to a form throws that all out the window. Making any input component interactive forces the entire containing form to be interactive, which forces the form's handlers to be interactive, which forces the user to research and ultimately choose between:

  1. Use interactive server rendering mode for the form.
    • Nothing quite like the feeling of using your last resort immediately and always.
    • If you're using Entity Framework, don't forget to rewrite your context use to use a DbContextFactory instead, so as to not have long-lived DbContexts in your long-lived components.
    • Hope committing to server interactivity everywhere won't somehow come back to bite you in the future.
  2. Use interactive wasm rendering mode for the form. Convert the form handler into an API endpoint, use a component-level injected HTTPClient in the handler to call the API.
    • Don't forget to disable prerendering if you need to call this API for some data when your component loads.
    • Remember to create separate DTO models for your API if you're using Entity Framework.
  3. Use interactive auto rendering mode for the form. Rewrite the form handlers to use an interface with specific client and server implementations. The former calling an API endpoint on the server, and the latter serving as the endpoint implementation.
    • Remember to handle authorization properly, now that the client and server access this functionality from two separate directions.
    • Again, make seperate DTO models for the API.
  4. Use interactive auto rendering mode for the form, but keep the form handler in "SSR land". i.e. do the code-smelly workaround OP describes.
    • To anyone with a similar story googling their way here: If you're trying to keep your project simple, go with this one. I'd even call it a good solution, except for:
    • There is no guarantee it won't somehow break in the future.
  5. Give up and use something else entirely.

And don't forget, you have to make this choice (and work) for every form in your project that needs this small bit of interactivity.

Larger, more complicated projects are more likely to already have done a lot of the cruft-work described above, making it a lot easier to "just go" with option 2 or 3. My primary concern is with greenfield projects like mine.

I personally ended up choosing option 5, and I suspect most new users will as well. Being faced with this problem/choice this early is a big "quit moment." When the very first letter of a CRUD app throws you at a brick wall, why bother with the rest?

@SteveSandersonMS
Copy link
Member

@Dryvnt Thanks for the feedback. It really is useful, even though I appreciate you've been going through some inconvenience.

The solution of adding a _handler field is fine. I don't expect we'll ever stop that from working in a future version, but if somehow we had to, we'd go through the whole "breaking change" process which means having a clear announcement, clear upgrade steps, and preferably some kind of compatibility flag people can use if they can't change their code.

As for this point:

Unfortunately, adding just a little bit of interactivity to a form throws that all out the window. Making any input component interactive forces the entire containing form to be interactive, which forces the form's handlers to be interactive

Would you be able to clarify? AFAIK it's possible to add some child component into an SSR form and put @rendermode="InteractiveServer" or similar on it. For example you can define a component like this:

<input type="number" name="@Name" @bind="value" />
<button onclick="@(() => { value++; })">Increment</button>

@code {
    int value;

    [Parameter]
    public string? Name { get; set; }

    [Parameter]
    public int Value { get; set; }

    protected override void OnInitialized()
    {
        value = Value;
    }
}

... and then use it interactively inside an SSR form:

<form @formname="MyForm" method="post" @onsubmit="HandleSubmit">
    <AntiforgeryToken />
    <FormCounter Name="@nameof(SomeInput)" Value="SomeInput" @rendermode="InteractiveServer" />
    <button type="submit">Save</button>
</form>

@code {
    [SupplyParameterFromForm]
    public int SomeInput { get; set; } = 123;

    void HandleSubmit()
    {
        Console.WriteLine("You submitted " + SomeInput);
    }
}

In this case the form is static and submits via an HTTP form post, but still contains a button that changes a form field interactively.

To be clear, I'm sure you have a real scenario where something is more difficult than this. I'm not trying to say you're wrong - just asking for clarification on the sort of scenario you're actually dealing with. Hope that's OK.

@Dryvnt
Copy link

Dryvnt commented Jan 22, 2024

Thank you for your engagement. I appreciate that you take my frustration seriously.

That is a good point you bring up. Basic forms, working with raw <input> elements, etc. work fine and as expected, as you demonstrate. With what you've said in context, I suppose I was making an assumption that might be nontrivial: The use of EditForm, DataAnnotationsValidator, InputNumber, @inherits Editor<SomeFormModel>, etc.

Going by the documentation, this is the "intended" way to add validation to a form. In the spirit of "use the provided tools to make the simple stuff simple," I used those components without second thought. Things like manual input name resolution is very nice when you want to keep it simple and make it "just work" so you can focus on the "important" parts. It works perfectly fine with SSR, and I am honestly quite pleased, but trying to have interactive sub-elements does not jive.

To provide a concrete example of actual real code that does not work even though it feels like it should be possible, SomeSubForm is an empty @inherits Editor<SomeFormModel> component, SomeFormModel is an empty class. Page immediately throws an exception on render.

@page "/coolform"
@using TestBlazorBar.Shared

<EditForm Model="Model" OnValidSubmit="OnSubmit" FormName="cool-form">
    <DataAnnotationsValidator/>
    <SomeSubForm Value="Model" @rendermode="InteractiveWebAssembly"/>
    <ValidationSummary/>

    <button type="submit">Submit</button>
</EditForm>

@code {
    [SupplyParameterFromForm] public SomeFormModel? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= new SomeFormModel();
    }

    public void OnSubmit(EditContext editContext)
    {
        Console.WriteLine("wow!");
    }
}

Motivation-wise, this is a sketch of what I would ultimately like to be able to do, sans proper layouting/styling,etc.. I believe something rhyming with this (though probably with Razor Pages + JS at current point in time) is a strong way to get a project off the ground while keeping yourself flexible and still have solid fundamentals at the base of your application, though I am open to criticism in that line of thought; I might just be fundamentally holding it wrong. It's not the first time I would be doing something dumb because it felt right at the time.

@page "/widgets"
@inject MyAppContext Context
@inject NavigationManager NavigationManager

<EditForm Model="Model" OnValidSubmit="ValidSubmitHandler" FormName="new-widget">
    <DataAnnotationsValidator/>
    <InputText @bind-Value="Model.Name"/>
    @* InputDoodad has some internal interactivity, e.g.
            - toggle some elements when you press a button
            - when you fill in field A, field B gets auto-filled
            - calculate and display some preview values based on this or that
    *@
    <InputDoodad @bind-Value="Model.Doodad" @rendermode="InteractiveWebAssembly"/>
    <ValidationSummary/>
</EditForm>

@code {
    [SupplyParameterFromForm]
    public NewWidgetForm? Model { get; set; }

    // ...

    public async Task ValidSubmitHandler() {
        var widget = Model!.MapToEntity();
        await Context.Widgets.AddAsync(widget);
        await Context.SaveChangesAsync();
        NavigationManager.NavigateTo(f"/widgets/{widget.Id}")
    }

    public class NewWidgetForm {
        [StringLength(256, 3)] public string Name { get; set; }
        [Required] public DoodadDto Doodad { get; set; }
        // etc.
    }
}

I can accept needing the entire form to be interactive, one way or another, as I imagine serializing whatever EditContext etc. could be quite gnarly, plus the generics of InputBase et. al. don't seem to like it, but so long as I could specify the (post-validation?) submission handling be done server-side - "just post the form non-interactively" - I would be happy. An attribute on the EditForm, a method on the EditContext, an attribute on the specific submit button element, some new <PlainHtmlSubmitting/> element to put in the form body, etc., the specifics of how it would work aren't too important for me.

Knowing that I can do the workaround, adding the _handler field manually, etc., and not have it unexpectedly break underneath me does help.

@Dryvnt
Copy link

Dryvnt commented Jan 22, 2024

A more concrete example of a "simple interactive input element" I would like to use in an otherwise static form: An input for a int? value, with a checkbox that allows it to be toggled null or not null, allowing the user to decide whether or not such a value should be set. This could be done with some bool SomeValueEnabled fields in the form model, some manual parsing, and possibly some CSS tricks to make the not-disabled field look disabled when the checkbox is toggled. Hell, doing this with vanilla HTML+JS is literally a one-liner of onchange="if this.checked ? this.nextElementSibling.removeAttribute('disabled') : this.nextElementSibling.setAttribute('disabled', 'disabled'), but I'm trying to work with simple models that map closely to my actual data so that I can spare those kinds of headaches.

Mind that this code isn't entirely well tested or reviewed, I'm sure there's a better way to do this, this is the mangled husk of a component I tried and failed to make work in this context.

@typeparam TValue
@inherits InputNumber<TValue?>

<InputCheckbox @bind-Value="ValueEnabled" />
<input
    @bind="CurrentValueAsString"
    @attributes="AdditionalAttributes"
    name="@NameAttributeValue"
    class="@CssClass"
    disabled="@(!ValueEnabled)"
    type="number"
    step="any"/>


@code {
    private TValue? _underlyingValue;

    private bool _valueEnabled;

    private bool ValueEnabled
    {
        get => _valueEnabled;
        set
        {
            _valueEnabled = value;
            if (_valueEnabled)
            {
                Value = _underlyingValue;
            }
            else
            {
                _underlyingValue = Value;
                Value = default;
            }
        }
    }

    protected override void OnParametersSet()
    {
        _valueEnabled = Value is not null;
    }

    protected override bool TryParseValueFromString(string? value, out TValue? result, out string? validationErrorMessage)
    {
        if (value is not (null or ""))
            return base.TryParseValueFromString(value, out result, out validationErrorMessage);

        result = default;
        validationErrorMessage = null;
        return true;
    }
}

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Jan 23, 2024

Thanks for clarifying, @Dryvnt. I think much of this is an aspect of how the @rendermode=... syntax doesn't have any clear guardrails to indicate what will work and what won't. While trying to make it as flexible as possible, we've left it up to the developer to make sense of what things can cross rendermode boundaries and what can't, but it's just not obvious in all cases.

In the examples you're giving, you're trying to use @bind-* across a rendermode boundary. The two main reasons this won't work are:

  1. The binding system means you're supplying a parameter of type Expression<Func<T>> (where T is the type of the bound property). This is not serializable so it can't actually be passed across rendermode boundaries, leading to the error you observe ("NotSupportedException: Serialization and deserialization of 'System.Type' instances is not supported" when I tried it).
  2. Even if we did somehow serialize/deserialize this, it wouldn't behave usefully in the way you want. For example you're trying to set validation messages in the interactive component, but your <ValidationSummary> is rendered statically. So there's nothing interactive to display the interactive validation messages as they happen. Likewise for this to even be meaningful, we'd also have to serialize the EditContext and its Model across the boundary, which also doesn't happen because nothing tells it to do so, and if it did, you'd potentially disclose more information to the client than you were expecting, and if your interactive component updated that state, the updates would normally just be lost when the form is then posted statically.

In summary, I think you're right on this point:

I can accept needing the entire form to be interactive, one way or another, as I imagine serializing whatever EditContext etc. could be quite gnarly, plus the generics of InputBase et. al. don't seem to like it

Exactly. In general we don't expect bindings to cross rendermode boundaries because:

  1. It's unclear what that would mean - the SSR process has already finished by the time things become interactive, so the whole concept of interactive binding has nothing to connect its output to.
  2. It's not a very economical choice for the application developer. If you have any interactive pieces in your form, you're incurring the interactivity cost (whether you choose Server or WebAssembly), so since you're already accepting that overhead, why not make the whole form/page interactive? It doesn't cost any more and will give you vastly more flexibility.

The ability to mix rendermodes achieves its greatest benefit when done on a per-page basis, or for large and sophisticated components that represent the main content on the page in some sense (e.g., a document editor or interactive map).

We're still fairly early in this new world of mixing rendermode, so the community is still developing patterns and recommendations for how and when to use it. Thanks for your feedback on this point since it does help to develop clarity. I appreciate it's not obvious at this stage, but hopefully it will become more obvious over time!

so long as I could specify the (post-validation?) submission handling be done server-side - "just post the form non-interactively" - I would be happy

That totally makes sense and is exactly the feature this issue is tracking. We see quite a few use cases for this (e.g., on a fully interactive page, wanting to submit a form as a full page load so it can set cookies in the response, e.g., for logging in or out).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one Pillar: Complete Blazor Web Priority:1 Work that is critical for the release, but we could probably ship without
Projects
None yet
Development

No branches or pull requests

4 participants