Skip to content

InputLargeTextArea Component API Proposal #35007

@TanayParikh

Description

@TanayParikh

Background and Motivation

The current input textarea doesn't perform well with large amounts of text within Blazor Server. Due to binding/validation requirements, each keypress could lead to the entire text needing to be transferred to the server (ie 20k+ chars) which leads to unresponsiveness. This new InputLargeTextArea allows for async streaming based access to the textarea and forgoes binding/validation.

Proposed API

namespace Microsoft.AspNetCore.Components.Forms
{
    /// <summary>
    /// A multiline input component for editing large <see cref="string"/> values. It supports asynchronous
    /// content access without binding or validation.
    /// </summary>
    public class InputLargeTextArea : ComponentBase, IInputLargeTextAreaJsCallbacks, IDisposable
    {
        /// <summary>
        /// Gets or sets the event callback that will be invoked when the textarea content changes.
        /// </summary>
        [Parameter]
        public EventCallback<InputLargeTextAreaChangeEventArgs> OnChange { get; set; }

        /// <summary>
        /// Retrieves the textarea value asynchronously.
        /// </summary>
        /// <param name="maxLength">The maximum length of content to fetch from the textarea.</param>
        /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> used to relay cancellation of the request.</param>
        /// <returns>A <see cref="System.IO.StreamReader"/> which facilitates reading of the textarea value.</returns>
        public ValueTask<StreamReader> GetTextAsync(int maxLength = 50_000, CancellationToken cancellationToken = default);

        /// <summary>
        /// Sets the textarea value asynchronously.
        /// </summary>
        /// <param name="newValue">A <see cref="System.IO.StreamWriter"/> used to set the value of the textarea.</param>
        /// <param name="leaveTextAreaEnabled">Don't disable the textarea while set text is in progress.</param>
        /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> used to relay cancellation of the request.</param>
        public ValueTask SetTextAsync(StreamWriter newValue, bool leaveTextAreaEnabled = false, CancellationToken cancellationToken = default);

    }

    /// <summary>
    /// Provides information for a text change event on a <see cref="InputLargeTextArea" /> component.
    /// </summary>
    public class InputLargeTextAreaChangeEventArgs
    {
        /// <summary>
        /// Gets the <see cref="InputLargeTextArea" /> whose text has changed.
        /// </summary>
        public InputLargeTextArea Source { get; set; }

        /// <summary>
        /// Gets the length of the updated text in characters.
        /// </summary>
        public long TextLength { get; set; }
    }
}

Usage Examples

@using System.IO
@using System.Text

<InputLargeTextArea @ref="TextArea" OnChange="TextAreaChanged" />


@code {
  InputLargeTextArea TextArea;

  public async Task GetTextAsync()
  {
      var streamReader = await TextArea.GetTextAsync();
      GetTextResult = await streamReader.ReadToEndAsync();
      StateHasChanged();
  }

  public async Task SetTextAsync()
  {
      var memoryStream = new MemoryStream();
      var streamWriter = new StreamWriter(memoryStream);
      await streamWriter.WriteAsync(new string('c', 50_000));
      await streamWriter.FlushAsync();
      await TextArea.SetTextAsync(streamWriter);
  }

  public void TextAreaChanged(InputLargeTextAreaChangeEventArgs args)
  {
      // args.Length represents the new textarea value length
  }
}

Alternative Designs

Alternatively, to get past the 32k SignalR message size limit, we can take a Stream based approach:

[Old approach]

        /// <summary>
        /// Retrieves the textarea value asynchronously.
        /// </summary>
        /// <returns>The string value of the textarea.</returns>
        public ValueTask<string> GetTextStreamAsync();

        /// <summary>
        /// Sets the textarea value asynchronously.
        /// </summary>
        /// <param name="newValue">The new content to set for the textarea.</param>
        public ValueTask SetTextAsync(string newValue);

Questions

This section added by @SteveSandersonMS just to capture some ongoing thoughts:

  • For SetTextAsync,
    • Should the developer really pass a StreamWriter? Why not pass the underlying stream from it? If there are cases like setting an initial value from data in a file or other stream source, you wouldn't have a StreamWriter - you'd just want to supply a stream. And if you do have a StreamWriter you can also pass the .BaseStream from it, so that seems more flexible. I know this means the developer becomes responsible for ensuring the stream data actually is UTF8-encoded text, but that seems like a reasonable tradeoff.
    • Could we eliminate the explicit leaveTextAreaEnabled parameter and instead trigger the disabling behavior based on whether the initial chunk is "complete" vs whether more data is yet to arrive asynchronously?
  • Should there also be an InitialText parameter? I'm probably inclined to go with "no" just to avoid having too many different ways to do things, but we should bear in mind that the usage pattern for setting some initial text from a string will be pretty indirect, as you'll need to use @ref and then add an OnAfterRenderAsync, and then do if (firstRender) and inside that construct a stream from your string and supply that. Lots of steps!

Risks

Potential issue with >32k chars (SignalR message size limit) in the textbox.

PR: #34856
Issue: #30291

Metadata

Metadata

Assignees

Labels

DoneThis issue has been fixedapi-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-blazorIncludes: Blazor, Razor Components

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions