dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.36k stars 9.99k forks source link

Opt out of model binding on EditForm component and contained Inputs #27962

Closed WhitWaldo closed 3 years ago

WhitWaldo commented 3 years ago

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

Today, one cannot bind to the value of a TextInput or other input fields without putting them inside of an EditForm. One cannot set up an EditForm that doesn't either specify a Model or an EditContext for validation (which in turn requires the model).

I've got an elaborate form that accepts some strings, then optionally attaches a number of complex models if the user opts into using them. I previously set this up using this prescribed approach - a model that contains all the various required and simple properties with nullable properties for the optional fields to satisfy the EditForm requirement. Because my @code block was getting quite long and unwieldy, and because each of the optional models required a remote lookup, I took each and put them into a separate component so that lookup would only be done if the component was rendered. This cleaned up the @code block for each piece considerably, and then I just used parameterized EventCallbacks to pass the state of the component back to the parent upon successfully filling the pieces out.

But this introduced an awful lot of ceremony in the code. The parent component containing the form had to have EventCallback methods for each of these optional pieces to update the form's model out of band and each of the optional pieces had to have input parameters so I could pass in current values from the parent. It was less of a mess than putting it all on one component, but still pretty ugly.

So I got to thinking and found this blog post that speaks to three ways of communicating across Blazor components. The last approach stood out to me as optimal: Introduce one class that represents a singleton state of the data across all these form elements. Each component can just read what it needs out of there, set what it needs to via methods on the class. Each component, instead of binding to a local model can instead have each component bind to a string property in which the setter updates the global model.

This allows me to opt out of the outbound EventCallbacks between all the components, lets me drop the inbound [Parameters] and really only requires that I have an @inject MyState state at the top of each form component.

As example, this then looks like the following:

<EditForm>
  <SubComponentA/>
  @if (!_showB) {
    <button type="button" class="btn" @onclick="@(() => {_showB = true;})">Add B</button>
  } else {
    <SubComponentB />
  }
  @if (!_showC) {
    <button type="button" class="btn" @onclick="@(() => {_showC = true;})">Add C</button>
  } else {
    <SubComponentC />
  }
</EditForm>

A peek inside one of these components (they're all similar) looks like the following, to use SubComponentA here:

@using MyService.State
@implments IDisposable
@inject MyState state

<div class="row">
  <div class="form-group">
    <label for="name">Name</label>
    <InputText @bind-Value="NameValue" class="form-control" id="name"></InputText>
  </div>
  @* Additional fields as necessary *@
</div>

@code {
  private string _name = string.Empty;
  private string Name
  {
    get => _name;
    set 
    {
      _name = value;
      state.SetName(_name);
    }
  }

  protected override async Task OnInitializedAsync() {
    //Load remote property values
    state.OnChange += StateHasChanged;
  }

  public void Dispose() 
  {
    state.OnChange -= StateHasChanged;
  }
}

This would be amazing except.. it doesn't work. It fails at runtime with an error that reads "System.InvalidOperationException: EditForm requires either a Model parameter, or an EditContext parameter, please provide one of these."

A clear and concise description of what the problem is.

EditForm requires that a Model or EditContext be specified in order to use it. It's impossible to use any of the Input elements (e.g. InputText) without using an EditForm and this approach doesn't lend to more complex forms that use a global state like my approach here.

Describe the solution you'd like

Rather, I'd like to see a way to opt-out of the model binding altogether (e.g. just don't specify either a Model or an EditContext), but still let me use the @bind-Value and event handling capabilities on the input elements to manually handle the value of each control in the form.

mkArtakMSFT commented 3 years ago

Thanks for contacting us. Why are you trying to use InputText and not the HTML input element instead then?

mrpmorris commented 3 years ago

InputText etc are html inputs wrapped in a component that will additionally update state in an EditContext (Is the value modified or invalid).

If you don't want your input UI to talk to the EditContext then skip the Input* components and just do <input @bind=whatever/> instead.

WhitWaldo commented 3 years ago

@mkArtakMSFT @mrpmorris I completely missed this in the documentation, but I do understand this now.

Nothing to do here then. Thank you for your feedback and responses!