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

Blazor InputLargeTextArea #34856

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
48649a9
.NET to JS Streaming Interop
TanayParikh Jul 26, 2021
77ae1dc
Refactor WebView Impl
TanayParikh Jul 28, 2021
a18f82b
Update TransmitDataStreamToJS.cs
TanayParikh Jul 28, 2021
cb9c856
Blazor `InputLargeTextArea`
TanayParikh Jul 29, 2021
29aaa05
E2E Tests
TanayParikh Jul 29, 2021
fe9d5ef
Merge branch 'main' into taparik/largeTextAreaComponent
TanayParikh Jul 29, 2021
af4f4b7
Update InputLargeTextAreaTest.cs
TanayParikh Jul 29, 2021
9eef2b0
Merge branch 'main' into taparik/largeTextAreaComponent
TanayParikh Aug 3, 2021
75c4071
Merge branch 'main' into taparik/dotnetToJSStreaming
TanayParikh Aug 3, 2021
fdfffda
Update PublicAPI.Unshipped.txt
TanayParikh Aug 3, 2021
4893067
++PublicAPIs
TanayParikh Aug 3, 2021
44f71a5
Fix Build
TanayParikh Aug 3, 2021
87903e5
Unit Tests
TanayParikh Aug 3, 2021
c2af324
E2E Tests
TanayParikh Aug 3, 2021
fff1afe
Add get/set cancellation tokens
TanayParikh Aug 3, 2021
3d99404
E2E test fixes
TanayParikh Aug 4, 2021
2503d28
Remove dotNetToJSReceiveDotNetStreamReference Sync Tests
TanayParikh Aug 4, 2021
a3b90f1
Cleanup usings
TanayParikh Aug 4, 2021
6c690cd
Merge branch 'main' into taparik/dotnetToJSStreaming
TanayParikh Aug 4, 2021
588590d
Cleanup Tests
TanayParikh Aug 4, 2021
1a8bc1d
IAsyncEnumerable Based Approach
TanayParikh Aug 4, 2021
cb7b3da
Merge branch 'main' into taparik/largeTextAreaComponent
TanayParikh Aug 5, 2021
3756c06
Merge branch 'main' into taparik/dotnetToJSStreaming
TanayParikh Aug 5, 2021
9654117
Merge branch 'taparik/dotnetToJSStreaming' into taparik/largeTextArea…
TanayParikh Aug 5, 2021
39eba0e
API Update
TanayParikh Aug 5, 2021
b2f36fd
Add Test `CanGetValue_ThrowsIfTextAreaHasMoreContentThanMaxAllowed`
TanayParikh Aug 6, 2021
b380039
IAsyncEnumerable Based Approach Cleanup
TanayParikh Aug 6, 2021
0361961
Merge branch 'main' into taparik/dotnetToJSStreaming
TanayParikh Aug 6, 2021
80d884e
Update InputLargeTextAreaTest.cs
TanayParikh Aug 6, 2021
e3f8efe
Merge branch 'taparik/dotnetToJSStreaming' into taparik/largeTextArea…
TanayParikh Aug 6, 2021
03f071b
Update release js files
TanayParikh Aug 6, 2021
dc519e9
Merge branch 'main' into taparik/largeTextAreaComponent
TanayParikh Aug 6, 2021
ff7d5d6
PR Feedback
TanayParikh Aug 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webview.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/Components/Web.JS/src/GlobalExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Platform, Pointer, System_String, System_Array, System_Object, System_B
import { getNextChunk, receiveDotNetDataStream } from './StreamingInterop';
import { RootComponentsFunctions } from './Rendering/JSRootComponents';
import { attachWebRendererInterop } from './Rendering/WebRendererInteropMethods';
import { InputLargeTextArea } from './InputLargeTextArea';

interface IBlazor {
navigateTo: (uri: string, options: NavigationOptions) => void;
Expand All @@ -31,6 +32,7 @@ interface IBlazor {
PageTitle: typeof PageTitle,
forceCloseConnection?: () => Promise<void>;
InputFile?: typeof InputFile,
InputLargeTextArea?: typeof InputLargeTextArea,
invokeJSFromDotNet?: (callInfo: Pointer, arg0: any, arg1: any, arg2: any) => any;
endInvokeDotNetFromJS?: (callId: System_String, success: System_Boolean, resultJsonOrErrorMessage: System_String) => void;
receiveByteArray?: (id: System_Int, data: System_Array<System_Byte>) => void;
Expand Down Expand Up @@ -76,6 +78,7 @@ export const Blazor: IBlazor = {
Virtualize,
PageTitle,
InputFile,
InputLargeTextArea,
getJSDataStreamChunk: getNextChunk,
receiveDotNetDataStream: receiveDotNetDataStream,
attachWebRendererInterop,
Expand Down
32 changes: 32 additions & 0 deletions src/Components/Web.JS/src/InputLargeTextArea.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DotNet } from '@microsoft/dotnet-js-interop';

export const InputLargeTextArea = {
init,
getText,
setText,
enableTextArea,
};

function init(callbackWrapper: any, elem: HTMLTextAreaElement): void {
elem.addEventListener('change', function(): void {
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved
callbackWrapper.invokeMethodAsync('NotifyChange', elem.value.length);
});
}

function getText(elem: HTMLTextAreaElement): Uint8Array {
const textValue = elem.value;
const utf8Encoder = new TextEncoder();
const encodedTextValue = utf8Encoder.encode(textValue);
return encodedTextValue;
}

async function setText(elem: HTMLTextAreaElement, streamRef: DotNet.IDotNetStreamReference): Promise<void> {
const bytes = await streamRef.arrayBuffer();
const utf8Decoder = new TextDecoder();
const newTextValue = utf8Decoder.decode(bytes);
elem.value = newTextValue;
}

function enableTextArea(elem: HTMLTextAreaElement, disabled: boolean): void {
elem.disabled = disabled;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Components.Forms
{
internal interface IInputLargeTextAreaJsCallbacks
{
Task NotifyChange(int length);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.JSInterop;

namespace Microsoft.AspNetCore.Components.Forms
{
/// <summary>
/// A multiline input component for editing large <see cref="string"/> values, supports async
/// content access without binding nor validations.
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public class InputLargeTextArea : ComponentBase, IInputLargeTextAreaJsCallbacks, IDisposable
{
private ElementReference _inputLargeTextAreaElement;

private InputLargeTextAreaJsCallbacksRelay? _jsCallbacksRelay;

[Inject]
private IJSRuntime JSRuntime { get; set; } = default!;

/// <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>
/// Gets or sets a collection of additional attributes that will be applied to the input element.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object>? AdditionalAttributes { get; set; }

/// <summary>
/// Gets or sets the associated <see cref="ElementReference"/>.
/// <para>
/// May be <see langword="null"/> if accessed before the component is rendered.
/// </para>
/// </summary>
[DisallowNull]
public ElementReference? Element
{
get => _inputLargeTextAreaElement;
protected set => _inputLargeTextAreaElement = value!.Value;
}

/// <inheritdoc/>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_jsCallbacksRelay = new InputLargeTextAreaJsCallbacksRelay(this);
await JSRuntime.InvokeVoidAsync(InputLargeTextAreaInterop.Init, _jsCallbacksRelay.DotNetReference, _inputLargeTextAreaElement);
}
}

/// <inheritdoc/>
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "textarea");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddElementReferenceCapture(2, elementReference => _inputLargeTextAreaElement = elementReference);
builder.CloseElement();
}

Task IInputLargeTextAreaJsCallbacks.NotifyChange(int length)
=> OnChange.InvokeAsync(new InputLargeTextAreaChangeEventArgs(this, length));

/// <summary>
/// Retrieves the textarea value asyncronously.
/// </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.TextReader"/> which facilitates reading of the textarea value.</returns>
public async ValueTask<StreamReader> GetTextAsync(int maxLength = 32_000, CancellationToken cancellationToken = default)
{
try
{
var streamRef = await JSRuntime.InvokeAsync<IJSStreamReference>(InputLargeTextAreaInterop.GetText, cancellationToken, _inputLargeTextAreaElement);
var stream = await streamRef.OpenReadStreamAsync(maxLength, cancellationToken);
var streamReader = new StreamReader(stream);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine we wouldn't want to expose the leaveOpen parameter on this, do we?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's necessary, but I don't feel strongly, so happy to add it in if you'd prefer.

return streamReader;
}
catch (JSException jsException)
{
// Special casing support for empty textareas. Due to security considerations
// 0 length streams/textareas aren't permitted from JS->.NET Streaming Interop.
if (jsException.InnerException is ArgumentOutOfRangeException)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we throw a more specific exception? Hard to say that ArgumentOutOfRange is specifically because of a 0-length stream?

Copy link
Contributor Author

@TanayParikh TanayParikh Aug 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm in the original context, I believe the ArgumentOutOfRange exception may make sense, however I see your point about it still being ambiguous with other areas where that exception may be thrown.

How about this:
https://github.com/dotnet/aspnetcore/pull/34856/files#diff-30d94608ca41ec93dcbe8eb2bb646257519e86080e81ed5c246a715a8782c4b9R94-R102

Alternatively, we can decorate the Exception.Data property with something more specific (though that may not necessarily be statically typed).

{
return StreamReader.Null;
}

throw;
}
}

/// <summary>
/// Sets the textarea value asyncronously.
/// </summary>
/// <param name="streamWriter">A <see cref="System.IO.StreamWriter"/> used to set the value of the textarea.</param>
/// <param name="leaveTextAreaEnabled">Don't disable the textarea while settings the new textarea value from the stream.</param>
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved
/// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> used to relay cancellation of the request.</param>
public async ValueTask SetTextAsync(StreamWriter streamWriter, bool leaveTextAreaEnabled = false, CancellationToken cancellationToken = default)
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved
{
if (streamWriter.Encoding is not UTF8Encoding)
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved
{
throw new FormatException($"Expected {typeof(UTF8Encoding)}, got ${streamWriter.Encoding}");
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved
}

try
{
if (!leaveTextAreaEnabled)
{
await JSRuntime.InvokeVoidAsync(InputLargeTextAreaInterop.EnableTextArea, cancellationToken, _inputLargeTextAreaElement, /* disabled: */ true);
}

// Ensure we're reading from the beginning of the stream,
// the StreamWriter.BaseStream.Position will be at the end by default
var stream = streamWriter.BaseStream;
if (stream.Position != 0)
{
if (!stream.CanSeek)
{
throw new NotSupportedException("Unable to read from the beginning of the stream.");
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved
}
stream.Seek(0, SeekOrigin.Begin);
}

using var streamRef = new DotNetStreamReference(stream);
await JSRuntime.InvokeVoidAsync(InputLargeTextAreaInterop.SetText, cancellationToken, _inputLargeTextAreaElement, streamRef);
}
finally
{
if (!leaveTextAreaEnabled)
{
await JSRuntime.InvokeVoidAsync(InputLargeTextAreaInterop.EnableTextArea, cancellationToken, _inputLargeTextAreaElement, /* disabled: */ false);
}
}
}

void IDisposable.Dispose()
{
_jsCallbacksRelay?.Dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;

namespace Microsoft.AspNetCore.Components.Forms
{
/// <summary>
/// Supplies information about an <see cref="Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.OnChange"/> event being raised.
/// </summary>
public sealed class InputLargeTextAreaChangeEventArgs : EventArgs
{
/// <summary>
/// Constructs a new <see cref="InputLargeTextAreaChangeEventArgs"/> instance.
/// </summary>
/// <param name="sender">The textarea element for which the event was raised.</param>
/// <param name="length">The length of the textarea value.</param>
public InputLargeTextAreaChangeEventArgs(InputLargeTextArea sender, int length)
{
Sender = sender;
Length = length;
}

/// <summary>
/// The textarea element for which the event was raised.
Copy link
Member

@SteveSandersonMS SteveSandersonMS Aug 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// The textarea element for which the event was raised.
/// The <see cref="InputLargeTextArea" /> for which the event was raised.

/// </summary>
public InputLargeTextArea Sender { get; }

/// <summary>
/// Gets the length of the textarea value.
/// </summary>
public int Length { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the type be long? Can it be > 2GB? I know that it would be pretty outrageous to have a 2GB textarea, but do we definitely want to preclude it?

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components.Forms
{
internal static class InputLargeTextAreaInterop
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved
{
private const string JsFunctionsPrefix = "Blazor._internal.InputLargeTextArea.";

public const string Init = JsFunctionsPrefix + "init";

public const string GetText = JsFunctionsPrefix + "getText";

public const string SetText = JsFunctionsPrefix + "setText";

public const string EnableTextArea = JsFunctionsPrefix + "enableTextArea";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.JSInterop;

namespace Microsoft.AspNetCore.Components.Forms
{
internal class InputLargeTextAreaJsCallbacksRelay : IDisposable
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly IInputLargeTextAreaJsCallbacks _callbacks;

public IDisposable DotNetReference { get; }

[DynamicDependency(nameof(NotifyChange))]
public InputLargeTextAreaJsCallbacksRelay(IInputLargeTextAreaJsCallbacks callbacks)
{
_callbacks = callbacks;

DotNetReference = DotNetObjectReference.Create(this);
}

[JSInvokable]
public Task NotifyChange(int length)
=> _callbacks.NotifyChange(length);

public void Dispose()
{
DotNetReference.Dispose();
}
}
}
16 changes: 16 additions & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ Microsoft.AspNetCore.Components.Forms.InputDateType.Month = 2 -> Microsoft.AspNe
Microsoft.AspNetCore.Components.Forms.InputDateType.Time = 3 -> Microsoft.AspNetCore.Components.Forms.InputDateType
Microsoft.AspNetCore.Components.Forms.InputFile.Element.get -> Microsoft.AspNetCore.Components.ElementReference?
Microsoft.AspNetCore.Components.Forms.InputFile.Element.set -> void
Microsoft.AspNetCore.Components.Forms.InputLargeTextArea
Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.AdditionalAttributes.get -> System.Collections.Generic.IDictionary<string!, object!>?
Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.AdditionalAttributes.set -> void
Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.Element.get -> Microsoft.AspNetCore.Components.ElementReference?
Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.Element.set -> void
Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.GetTextAsync(int maxLength = 32000, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<System.IO.StreamReader!>
Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.InputLargeTextArea() -> void
Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.OnChange.get -> Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Forms.InputLargeTextAreaChangeEventArgs!>
Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.OnChange.set -> void
Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.SetTextAsync(System.IO.StreamWriter! streamWriter, bool leaveTextAreaEnabled = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
Microsoft.AspNetCore.Components.Forms.InputLargeTextAreaChangeEventArgs
Microsoft.AspNetCore.Components.Forms.InputLargeTextAreaChangeEventArgs.InputLargeTextAreaChangeEventArgs(Microsoft.AspNetCore.Components.Forms.InputLargeTextArea! sender, int length) -> void
Microsoft.AspNetCore.Components.Forms.InputLargeTextAreaChangeEventArgs.Length.get -> int
Microsoft.AspNetCore.Components.Forms.InputLargeTextAreaChangeEventArgs.Sender.get -> Microsoft.AspNetCore.Components.Forms.InputLargeTextArea!
Microsoft.AspNetCore.Components.Forms.InputNumber<TValue>.Element.get -> Microsoft.AspNetCore.Components.ElementReference?
Microsoft.AspNetCore.Components.Forms.InputNumber<TValue>.Element.set -> void
Microsoft.AspNetCore.Components.Forms.InputSelect<TValue>.Element.get -> Microsoft.AspNetCore.Components.ElementReference?
Expand Down Expand Up @@ -60,6 +74,8 @@ Microsoft.AspNetCore.Components.Web.PageTitle.ChildContent.set -> void
Microsoft.AspNetCore.Components.Web.PageTitle.PageTitle() -> void
override Microsoft.AspNetCore.Components.Forms.InputDate<TValue>.OnParametersSet() -> void
abstract Microsoft.AspNetCore.Components.RenderTree.WebRenderer.AttachRootComponentToBrowser(int componentId, string! domElementSelector) -> void
override Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder! builder) -> void
override Microsoft.AspNetCore.Components.Forms.InputLargeTextArea.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task!
override Microsoft.AspNetCore.Components.RenderTree.WebRenderer.Dispose(bool disposing) -> void
override Microsoft.AspNetCore.Components.Routing.FocusOnNavigate.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task!
override Microsoft.AspNetCore.Components.Routing.FocusOnNavigate.OnParametersSet() -> void
Expand Down
Loading