InputFile Component (#24640)
This commit is contained in:
parent
a700662dec
commit
0b1042c54e
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
internal class BrowserFile : IBrowserFile
|
||||
{
|
||||
internal InputFile Owner { get; set; } = default!;
|
||||
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public DateTime LastModified { get; set; }
|
||||
|
||||
public long Size { get; set; }
|
||||
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
public string? RelativePath { get; set; }
|
||||
|
||||
public Stream OpenReadStream(CancellationToken cancellationToken = default)
|
||||
=> Owner.OpenReadStream(this, cancellationToken);
|
||||
|
||||
public Task<IBrowserFile> ToImageFileAsync(string format, int maxWidth, int maxHeight)
|
||||
=> Owner.ConvertToImageFileAsync(this, format, maxWidth, maxHeight);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
internal abstract class BrowserFileStream : Stream
|
||||
{
|
||||
private long _position;
|
||||
|
||||
protected BrowserFile File { get; }
|
||||
|
||||
protected BrowserFileStream(BrowserFile file)
|
||||
{
|
||||
File = file;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length => File.Size;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _position;
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
=> throw new NotSupportedException("Synchronous reads are not supported.");
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
=> ReadAsync(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
|
||||
|
||||
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int maxBytesToRead = (int)(Length - Position);
|
||||
|
||||
if (maxBytesToRead > buffer.Length)
|
||||
{
|
||||
maxBytesToRead = buffer.Length;
|
||||
}
|
||||
|
||||
if (maxBytesToRead <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var bytesRead = await CopyFileDataIntoBuffer(_position, buffer.Slice(0, maxBytesToRead), cancellationToken);
|
||||
|
||||
_position += bytesRead;
|
||||
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
protected abstract ValueTask<int> CopyFileDataIntoBuffer(long sourceOffset, Memory<byte> destination, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the data of a file selected from an <see cref="InputFile"/> component.
|
||||
/// </summary>
|
||||
public interface IBrowserFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the file.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last modified date.
|
||||
/// </summary>
|
||||
DateTime LastModified { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of the file in bytes.
|
||||
/// </summary>
|
||||
long Size { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the MIME type of the file.
|
||||
/// </summary>
|
||||
string Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens the stream for reading the uploaded file.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token to signal the cancellation of streaming file data.</param>
|
||||
Stream OpenReadStream(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the current image file to a new one of the specified file type and maximum file dimensions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The image will be scaled to fit the specified dimensions while preserving the original aspect ratio.
|
||||
/// </remarks>
|
||||
/// <param name="format">The new image format.</param>
|
||||
/// <param name="maxWith">The maximum image width.</param>
|
||||
/// <param name="maxHeight">The maximum image height</param>
|
||||
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
|
||||
Task<IBrowserFile> ToImageFileAsync(string format, int maxWith, int maxHeight);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
internal interface IInputFileJsCallbacks
|
||||
{
|
||||
Task NotifyChange(BrowserFile[] files);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A component that wraps the HTML file input element and exposes a <see cref="Stream"/> for each file's contents.
|
||||
/// </summary>
|
||||
public class InputFile : ComponentBase, IInputFileJsCallbacks, IDisposable
|
||||
{
|
||||
private ElementReference _inputFileElement;
|
||||
|
||||
private IJSUnmarshalledRuntime? _jsUnmarshalledRuntime;
|
||||
|
||||
private InputFileJsCallbacksRelay? _jsCallbacksRelay;
|
||||
|
||||
[Inject]
|
||||
private IJSRuntime JSRuntime { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
private IOptions<RemoteBrowserFileStreamOptions> Options { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the event callback that will be invoked when the collection of selected files changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<InputFileChangeEventArgs> 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; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_jsUnmarshalledRuntime = JSRuntime as IJSUnmarshalledRuntime;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
_jsCallbacksRelay = new InputFileJsCallbacksRelay(this);
|
||||
await JSRuntime.InvokeVoidAsync(InputFileInterop.Init, _jsCallbacksRelay.DotNetReference, _inputFileElement);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenElement(0, "input");
|
||||
builder.AddMultipleAttributes(1, AdditionalAttributes);
|
||||
builder.AddAttribute(2, "type", "file");
|
||||
builder.AddElementReferenceCapture(3, elementReference => _inputFileElement = elementReference);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
internal Stream OpenReadStream(BrowserFile file, CancellationToken cancellationToken)
|
||||
=> _jsUnmarshalledRuntime != null ?
|
||||
(Stream)new SharedBrowserFileStream(JSRuntime, _jsUnmarshalledRuntime, _inputFileElement, file) :
|
||||
new RemoteBrowserFileStream(JSRuntime, _inputFileElement, file, Options.Value, cancellationToken);
|
||||
|
||||
internal async Task<IBrowserFile> ConvertToImageFileAsync(BrowserFile file, string format, int maxWidth, int maxHeight)
|
||||
{
|
||||
var imageFile = await JSRuntime.InvokeAsync<BrowserFile>(InputFileInterop.ToImageFile, _inputFileElement, file.Id, format, maxWidth, maxHeight);
|
||||
|
||||
imageFile.Owner = this;
|
||||
|
||||
return imageFile;
|
||||
}
|
||||
|
||||
Task IInputFileJsCallbacks.NotifyChange(BrowserFile[] files)
|
||||
{
|
||||
foreach (var file in files)
|
||||
{
|
||||
file.Owner = this;
|
||||
}
|
||||
|
||||
return OnChange.InvokeAsync(new InputFileChangeEventArgs(files));
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
_jsCallbacksRelay?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Supplies information about an <see cref="InputFile.OnChange"/> event being raised.
|
||||
/// </summary>
|
||||
public class InputFileChangeEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The updated file entries list.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IBrowserFile> Files { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="InputFileChangeEventArgs"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="files">The updated file entries list.</param>
|
||||
public InputFileChangeEventArgs(IReadOnlyList<IBrowserFile> files)
|
||||
{
|
||||
Files = files;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
internal static class InputFileInterop
|
||||
{
|
||||
private const string JsFunctionsPrefix = "_blazorInputFile.";
|
||||
|
||||
public const string Init = JsFunctionsPrefix + "init";
|
||||
|
||||
public const string EnsureArrayBufferReadyForSharedMemoryInterop = JsFunctionsPrefix + "ensureArrayBufferReadyForSharedMemoryInterop";
|
||||
|
||||
public const string ReadFileData = JsFunctionsPrefix + "readFileData";
|
||||
|
||||
public const string ReadFileDataSharedMemory = JsFunctionsPrefix + "readFileDataSharedMemory";
|
||||
|
||||
public const string ToImageFile = JsFunctionsPrefix + "toImageFile";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
internal class InputFileJsCallbacksRelay : IDisposable
|
||||
{
|
||||
private readonly IInputFileJsCallbacks _callbacks;
|
||||
|
||||
public IDisposable DotNetReference { get; }
|
||||
|
||||
public InputFileJsCallbacksRelay(IInputFileJsCallbacks callbacks)
|
||||
{
|
||||
_callbacks = callbacks;
|
||||
|
||||
DotNetReference = DotNetObjectReference.Create(this);
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public Task NotifyChange(BrowserFile[] files)
|
||||
=> _callbacks.NotifyChange(files);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DotNetReference.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
internal struct ReadRequest
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public string InputFileElementReferenceId;
|
||||
|
||||
[FieldOffset(4)]
|
||||
public int FileId;
|
||||
|
||||
[FieldOffset(8)]
|
||||
public long SourceOffset;
|
||||
|
||||
[FieldOffset(16)]
|
||||
public byte[] Destination;
|
||||
|
||||
[FieldOffset(20)]
|
||||
public int DestinationOffset;
|
||||
|
||||
[FieldOffset(24)]
|
||||
public int MaxBytes;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
internal class RemoteBrowserFileStream : BrowserFileStream
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private readonly ElementReference _inputFileElement;
|
||||
private readonly int _maxSegmentSize;
|
||||
private readonly PipeReader _pipeReader;
|
||||
private readonly CancellationTokenSource _fillBufferCts;
|
||||
private readonly TimeSpan _segmentFetchTimeout;
|
||||
|
||||
private bool _isReadingCompleted;
|
||||
private bool _isDisposed;
|
||||
|
||||
public RemoteBrowserFileStream(
|
||||
IJSRuntime jsRuntime,
|
||||
ElementReference inputFileElement,
|
||||
BrowserFile file,
|
||||
RemoteBrowserFileStreamOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
: base(file)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
_inputFileElement = inputFileElement;
|
||||
_maxSegmentSize = options.SegmentSize;
|
||||
_segmentFetchTimeout = options.SegmentFetchTimeout;
|
||||
|
||||
var pipe = new Pipe(new PipeOptions(pauseWriterThreshold: options.MaxBufferSize, resumeWriterThreshold: options.MaxBufferSize));
|
||||
_pipeReader = pipe.Reader;
|
||||
_fillBufferCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
_ = FillBuffer(pipe.Writer, _fillBufferCts.Token);
|
||||
}
|
||||
|
||||
private async Task FillBuffer(PipeWriter writer, CancellationToken cancellationToken)
|
||||
{
|
||||
long offset = 0;
|
||||
|
||||
while (offset < File.Size)
|
||||
{
|
||||
var pipeBuffer = writer.GetMemory(_maxSegmentSize);
|
||||
var segmentSize = (int)Math.Min(_maxSegmentSize, File.Size - offset);
|
||||
|
||||
try
|
||||
{
|
||||
using var readSegmentCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
readSegmentCts.CancelAfter(_segmentFetchTimeout);
|
||||
|
||||
var bytes = await _jsRuntime.InvokeAsync<byte[]>(
|
||||
InputFileInterop.ReadFileData,
|
||||
readSegmentCts.Token,
|
||||
_inputFileElement,
|
||||
File.Id,
|
||||
offset,
|
||||
segmentSize);
|
||||
|
||||
if (bytes.Length != segmentSize)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A segment with size {bytes.Length} bytes was received, but {segmentSize} bytes were expected.");
|
||||
}
|
||||
|
||||
bytes.CopyTo(pipeBuffer);
|
||||
writer.Advance(segmentSize);
|
||||
offset += segmentSize;
|
||||
|
||||
var result = await writer.FlushAsync(cancellationToken);
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await writer.CompleteAsync(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await writer.CompleteAsync();
|
||||
}
|
||||
|
||||
protected override async ValueTask<int> CopyFileDataIntoBuffer(long sourceOffset, Memory<byte> destination, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_isReadingCompleted)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int totalBytesCopied = 0;
|
||||
|
||||
while (destination.Length > 0)
|
||||
{
|
||||
var result = await _pipeReader.ReadAsync(cancellationToken);
|
||||
var bytesToCopy = (int)Math.Min(result.Buffer.Length, destination.Length);
|
||||
|
||||
if (bytesToCopy == 0)
|
||||
{
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
_isReadingCompleted = true;
|
||||
await _pipeReader.CompleteAsync();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var slice = result.Buffer.Slice(0, bytesToCopy);
|
||||
slice.CopyTo(destination.Span);
|
||||
|
||||
_pipeReader.AdvanceTo(slice.End);
|
||||
|
||||
totalBytesCopied += bytesToCopy;
|
||||
destination = destination.Slice(bytesToCopy);
|
||||
}
|
||||
|
||||
return totalBytesCopied;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_fillBufferCts.Cancel();
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Repesents configurable options for <see cref="RemoteBrowserFileStream"/>.
|
||||
/// </summary>
|
||||
[UnsupportedOSPlatform("browser")]
|
||||
public class RemoteBrowserFileStreamOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum segment size for file data sent over a SignalR circuit.
|
||||
/// The default value is 20K.
|
||||
/// <para>
|
||||
/// This only has an effect when using Blazor Server.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public int SegmentSize { get; set; } = 20 * 1024; // SignalR limit is 32K.
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum internal buffer size for unread data sent over a SignalR circuit.
|
||||
/// <para>
|
||||
/// This only has an effect when using Blazor Server.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public int MaxBufferSize { get; set; } = 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time limit for fetching a segment of file data.
|
||||
/// <para>
|
||||
/// This only has an effect when using Blazor Server.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public TimeSpan SegmentFetchTimeout { get; set; } = TimeSpan.FromSeconds(3);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||
{
|
||||
internal class SharedBrowserFileStream : BrowserFileStream
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
|
||||
private readonly IJSUnmarshalledRuntime _jsUnmarshalledRuntime;
|
||||
|
||||
private readonly ElementReference _inputFileElement;
|
||||
|
||||
public SharedBrowserFileStream(IJSRuntime jsRuntime, IJSUnmarshalledRuntime jsUnmarshalledRuntime, ElementReference inputFileElement, BrowserFile file)
|
||||
: base(file)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
_jsUnmarshalledRuntime = jsUnmarshalledRuntime;
|
||||
_inputFileElement = inputFileElement;
|
||||
}
|
||||
|
||||
protected override async ValueTask<int> CopyFileDataIntoBuffer(long sourceOffset, Memory<byte> destination, CancellationToken cancellationToken)
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync(InputFileInterop.EnsureArrayBufferReadyForSharedMemoryInterop, cancellationToken, _inputFileElement, File.Id);
|
||||
|
||||
var readRequest = new ReadRequest
|
||||
{
|
||||
InputFileElementReferenceId = _inputFileElement.Id,
|
||||
FileId = File.Id,
|
||||
SourceOffset = sourceOffset
|
||||
};
|
||||
|
||||
if (MemoryMarshal.TryGetArray(destination, out ArraySegment<byte> destinationArraySegment))
|
||||
{
|
||||
readRequest.Destination = destinationArraySegment.Array!;
|
||||
readRequest.DestinationOffset = destinationArraySegment.Offset;
|
||||
readRequest.MaxBytes = destinationArraySegment.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Worst case, we need to copy to a temporary array.
|
||||
readRequest.Destination = new byte[destination.Length];
|
||||
readRequest.DestinationOffset = 0;
|
||||
readRequest.MaxBytes = destination.Length;
|
||||
|
||||
destination.CopyTo(new Memory<byte>(readRequest.Destination));
|
||||
}
|
||||
|
||||
return _jsUnmarshalledRuntime.InvokeUnmarshalled<ReadRequest, int>(InputFileInterop.ReadFileDataSharedMemory, readRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
(function () {
|
||||
|
||||
// Exported functions
|
||||
|
||||
function init(callbackWrapper, elem) {
|
||||
elem._blazorInputFileNextFileId = 0;
|
||||
|
||||
elem.addEventListener('click', function () {
|
||||
// Permits replacing an existing file with a new one of the same file name.
|
||||
elem.value = '';
|
||||
});
|
||||
|
||||
elem.addEventListener('change', function () {
|
||||
// Reduce to purely serializable data, plus an index by ID.
|
||||
elem._blazorFilesById = {};
|
||||
|
||||
const fileList = Array.prototype.map.call(elem.files, function (file) {
|
||||
const result = {
|
||||
id: ++elem._blazorInputFileNextFileId,
|
||||
lastModified: new Date(file.lastModified).toISOString(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
};
|
||||
|
||||
elem._blazorFilesById[result.id] = result;
|
||||
|
||||
// Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON.
|
||||
Object.defineProperty(result, 'blob', { value: file });
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
callbackWrapper.invokeMethodAsync('NotifyChange', fileList);
|
||||
});
|
||||
}
|
||||
|
||||
function toImageFile(elem, fileId, format, maxWidth, maxHeight) {
|
||||
var originalFile = getFileById(elem, fileId);
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
var originalFileImage = new Image();
|
||||
originalFileImage.onload = function () { resolve(originalFileImage); };
|
||||
originalFileImage.src = URL.createObjectURL(originalFile.blob);
|
||||
}).then(function (loadedImage) {
|
||||
return new Promise(function (resolve) {
|
||||
var desiredWidthRatio = Math.min(1, maxWidth / loadedImage.width);
|
||||
var desiredHeightRatio = Math.min(1, maxHeight / loadedImage.height);
|
||||
var chosenSizeRatio = Math.min(desiredWidthRatio, desiredHeightRatio);
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = Math.round(loadedImage.width * chosenSizeRatio);
|
||||
canvas.height = Math.round(loadedImage.height * chosenSizeRatio);
|
||||
canvas.getContext('2d').drawImage(loadedImage, 0, 0, canvas.width, canvas.height);
|
||||
canvas.toBlob(resolve, format);
|
||||
});
|
||||
}).then(function (resizedImageBlob) {
|
||||
var result = {
|
||||
id: ++elem._blazorInputFileNextFileId,
|
||||
lastModified: originalFile.lastModified,
|
||||
name: originalFile.name, // Note: we're not changing the file extension.
|
||||
size: resizedImageBlob.size,
|
||||
type: format,
|
||||
relativePath: originalFile.relativePath
|
||||
};
|
||||
|
||||
elem._blazorFilesById[result.id] = result;
|
||||
|
||||
// Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON.
|
||||
Object.defineProperty(result, 'blob', { value: resizedImageBlob });
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
function ensureArrayBufferReadyForSharedMemoryInterop(elem, fileId) {
|
||||
return getArrayBufferFromFileAsync(elem, fileId).then(function (arrayBuffer) {
|
||||
getFileById(elem, fileId).arrayBuffer = arrayBuffer;
|
||||
});
|
||||
}
|
||||
|
||||
function readFileData(elem, fileId, startOffset, count) {
|
||||
return getArrayBufferFromFileAsync(elem, fileId).then(function (arrayBuffer) {
|
||||
return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer, startOffset, count)));
|
||||
});
|
||||
}
|
||||
|
||||
function readFileDataSharedMemory(readRequest) {
|
||||
const inputFileElementReferenceId = Blazor.platform.readStringField(readRequest, 0);
|
||||
const inputFileElement = document.querySelector(`[_bl_${inputFileElementReferenceId}]`);
|
||||
const fileId = Blazor.platform.readInt32Field(readRequest, 4);
|
||||
const sourceOffset = Blazor.platform.readUint64Field(readRequest, 8);
|
||||
const destination = Blazor.platform.readInt32Field(readRequest, 16);
|
||||
const destinationOffset = Blazor.platform.readInt32Field(readRequest, 20);
|
||||
const maxBytes = Blazor.platform.readInt32Field(readRequest, 24);
|
||||
|
||||
const sourceArrayBuffer = getFileById(inputFileElement, fileId).arrayBuffer;
|
||||
const bytesToRead = Math.min(maxBytes, sourceArrayBuffer.byteLength - sourceOffset);
|
||||
const sourceUint8Array = new Uint8Array(sourceArrayBuffer, sourceOffset, bytesToRead);
|
||||
|
||||
const destinationUint8Array = Blazor.platform.toUint8Array(destination);
|
||||
destinationUint8Array.set(sourceUint8Array, destinationOffset);
|
||||
|
||||
return bytesToRead;
|
||||
}
|
||||
|
||||
// Local helpers
|
||||
|
||||
function getFileById(elem, fileId) {
|
||||
const file = elem._blazorFilesById[fileId];
|
||||
|
||||
if (!file) {
|
||||
throw new Error(`There is no file with ID ${fileId}. The file list may have changed.`);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
function getArrayBufferFromFileAsync(elem, fileId) {
|
||||
const file = getFileById(elem, fileId);
|
||||
|
||||
// On the first read, convert the FileReader into a Promise<ArrayBuffer>.
|
||||
if (!file.readPromise) {
|
||||
file.readPromise = new Promise(function (resolve, reject) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function () { resolve(reader.result); };
|
||||
reader.onerror = function (err) { reject(err); };
|
||||
reader.readAsArrayBuffer(file.blob);
|
||||
});
|
||||
}
|
||||
|
||||
return file.readPromise;
|
||||
}
|
||||
|
||||
window._blazorInputFile = {
|
||||
init,
|
||||
toImageFile,
|
||||
ensureArrayBufferReadyForSharedMemoryInterop,
|
||||
readFileData,
|
||||
readFileDataSharedMemory,
|
||||
};
|
||||
})();
|
||||
|
|
@ -11,7 +11,7 @@ namespace Microsoft.JSInterop.WebAssembly
|
|||
/// Provides methods for invoking JavaScript functions for applications running
|
||||
/// on the Mono WebAssembly runtime.
|
||||
/// </summary>
|
||||
public abstract class WebAssemblyJSRuntime : JSInProcessRuntime
|
||||
public abstract class WebAssemblyJSRuntime : JSInProcessRuntime, IJSUnmarshalledRuntime
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override string InvokeJS(string identifier, string argsJson)
|
||||
|
|
@ -42,52 +42,20 @@ namespace Microsoft.JSInterop.WebAssembly
|
|||
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the JavaScript function registered with the specified identifier.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||
/// <returns>The result of the function invocation.</returns>
|
||||
public TResult InvokeUnmarshalled<TResult>(string identifier)
|
||||
=> InvokeUnmarshalled<object, object, object, TResult>(identifier, null, null, null);
|
||||
/// <inheritdoc />
|
||||
TResult IJSUnmarshalledRuntime.InvokeUnmarshalled<TResult>(string identifier)
|
||||
=> ((IJSUnmarshalledRuntime)this).InvokeUnmarshalled<object, object, object, TResult>(identifier, null, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the JavaScript function registered with the specified identifier.
|
||||
/// </summary>
|
||||
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||
/// <param name="arg0">The first argument.</param>
|
||||
/// <returns>The result of the function invocation.</returns>
|
||||
public TResult InvokeUnmarshalled<T0, TResult>(string identifier, T0 arg0)
|
||||
=> InvokeUnmarshalled<T0, object, object, TResult>(identifier, arg0, null, null);
|
||||
/// <inheritdoc />
|
||||
TResult IJSUnmarshalledRuntime.InvokeUnmarshalled<T0, TResult>(string identifier, T0 arg0)
|
||||
=> ((IJSUnmarshalledRuntime)this).InvokeUnmarshalled<T0, object, object, TResult>(identifier, arg0, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the JavaScript function registered with the specified identifier.
|
||||
/// </summary>
|
||||
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||
/// <typeparam name="T1">The type of the second argument.</typeparam>
|
||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||
/// <param name="arg0">The first argument.</param>
|
||||
/// <param name="arg1">The second argument.</param>
|
||||
/// <returns>The result of the function invocation.</returns>
|
||||
public TResult InvokeUnmarshalled<T0, T1, TResult>(string identifier, T0 arg0, T1 arg1)
|
||||
=> InvokeUnmarshalled<T0, T1, object, TResult>(identifier, arg0, arg1, null);
|
||||
/// <inheritdoc />
|
||||
TResult IJSUnmarshalledRuntime.InvokeUnmarshalled<T0, T1, TResult>(string identifier, T0 arg0, T1 arg1)
|
||||
=> ((IJSUnmarshalledRuntime)this).InvokeUnmarshalled<T0, T1, object, TResult>(identifier, arg0, arg1, null);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the JavaScript function registered with the specified identifier.
|
||||
/// </summary>
|
||||
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||
/// <typeparam name="T1">The type of the second argument.</typeparam>
|
||||
/// <typeparam name="T2">The type of the third argument.</typeparam>
|
||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||
/// <param name="arg0">The first argument.</param>
|
||||
/// <param name="arg1">The second argument.</param>
|
||||
/// <param name="arg2">The third argument.</param>
|
||||
/// <returns>The result of the function invocation.</returns>
|
||||
public TResult InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2)
|
||||
/// <inheritdoc />
|
||||
TResult IJSUnmarshalledRuntime.InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2)
|
||||
{
|
||||
var result = InternalCalls.InvokeJSUnmarshalled<T0, T1, T2, TResult>(out var exception, identifier, arg0, arg1, arg2);
|
||||
return exception != null
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.JSInterop;
|
||||
using Microsoft.JSInterop.WebAssembly;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
|
||||
|
|
@ -95,7 +96,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
|
|||
/// <inheritdoc />
|
||||
protected override Task UpdateDisplayAsync(in RenderBatch batch)
|
||||
{
|
||||
DefaultWebAssemblyJSRuntime.Instance.InvokeUnmarshalled<int, RenderBatch, object>(
|
||||
((IJSUnmarshalledRuntime)DefaultWebAssemblyJSRuntime.Instance).InvokeUnmarshalled<int, RenderBatch, object>(
|
||||
"Blazor._internal.renderBatch",
|
||||
_webAssemblyRendererId,
|
||||
batch);
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
|||
var newAssembliesToLoad = assembliesToLoad.Where(assembly => !_loadedAssemblyCache.Contains(assembly));
|
||||
var loadedAssemblies = new List<Assembly>();
|
||||
|
||||
var count = (int)await ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
|
||||
var count = (int)await ((IJSUnmarshalledRuntime)_jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
|
||||
GetDynamicAssemblies,
|
||||
newAssembliesToLoad.ToArray(),
|
||||
null,
|
||||
|
|
@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
|||
return loadedAssemblies;
|
||||
}
|
||||
|
||||
var assemblies = ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
|
||||
var assemblies = ((IJSUnmarshalledRuntime)_jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
|
||||
ReadDynamicAssemblies,
|
||||
null,
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
|||
_jsRuntime.InvokeVoid("console.error", formattedMessage);
|
||||
break;
|
||||
case LogLevel.Critical:
|
||||
_jsRuntime.InvokeUnmarshalled<string, object>("Blazor._internal.dotNetCriticalError", formattedMessage);
|
||||
((IJSUnmarshalledRuntime)_jsRuntime).InvokeUnmarshalled<string, object>("Blazor._internal.dotNetCriticalError", formattedMessage);
|
||||
break;
|
||||
default: // LogLevel.None or invalid enum values
|
||||
Console.WriteLine(formattedMessage);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.JSInterop.WebAssembly;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// This class exists to enable unit testing for code that needs to call
|
||||
/// <see cref="WebAssemblyJSRuntime.InvokeUnmarshalled{T0, T1, T2, TResult}(string, T0, T1, T2)"/>.
|
||||
/// <see cref="IJSUnmarshalledRuntime.InvokeUnmarshalled{T0, T1, T2, TResult}(string, T0, T1, T2)"/>.
|
||||
///
|
||||
/// We should only use this in non-perf-critical code paths (for example, during hosting startup,
|
||||
/// where we only call this a fixed number of times, and not during rendering where it might be
|
||||
|
|
@ -22,6 +22,6 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
|||
public static WebAssemblyJSRuntimeInvoker Instance = new WebAssemblyJSRuntimeInvoker();
|
||||
|
||||
public virtual TResult InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2)
|
||||
=> DefaultWebAssemblyJSRuntime.Instance.InvokeUnmarshalled<T0, T1, T2, TResult>(identifier, arg0, arg1, arg2);
|
||||
=> ((IJSUnmarshalledRuntime)DefaultWebAssemblyJSRuntime.Instance).InvokeUnmarshalled<T0, T1, T2, TResult>(identifier, arg0, arg1, arg2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BasicTestApp;
|
||||
using Microsoft.AspNetCore.Components.E2ETest;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETests.Tests
|
||||
{
|
||||
public class InputFileTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>, IDisposable
|
||||
{
|
||||
private string _tempDirectory;
|
||||
|
||||
public InputFileTest(
|
||||
BrowserFixture browserFixture,
|
||||
ToggleExecutionModeServerFixture<Program> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture, output)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void InitializeAsyncCore()
|
||||
{
|
||||
_tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_tempDirectory);
|
||||
|
||||
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
|
||||
Browser.MountTestComponent<InputFileComponent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUploadSingleSmallFile()
|
||||
{
|
||||
// Create a temporary text file
|
||||
var file = TempFile.Create(_tempDirectory, "txt", "This file was uploaded to the browser and read from .NET.");
|
||||
|
||||
// Upload the file
|
||||
var inputFile = Browser.FindElement(By.Id("input-file"));
|
||||
inputFile.SendKeys(file.Path);
|
||||
|
||||
var fileContainer = Browser.FindElement(By.Id($"file-{file.Name}"));
|
||||
var fileSizeElement = fileContainer.FindElement(By.Id("file-size"));
|
||||
var fileContentElement = fileContainer.FindElement(By.Id("file-content"));
|
||||
|
||||
// Validate that the file was uploaded correctly
|
||||
Browser.Equal(file.Contents.Length.ToString(), () => fileSizeElement.Text);
|
||||
Browser.Equal(file.Text, () => fileContentElement.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUploadSingleLargeFile()
|
||||
{
|
||||
// Create a large text file
|
||||
var fileContentSizeInBytes = 1024 * 1024;
|
||||
var contentBuilder = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < fileContentSizeInBytes; i++)
|
||||
{
|
||||
contentBuilder.Append((i % 10).ToString());
|
||||
}
|
||||
|
||||
var file = TempFile.Create(_tempDirectory, "txt", contentBuilder.ToString());
|
||||
|
||||
// Upload the file
|
||||
var inputFile = Browser.FindElement(By.Id("input-file"));
|
||||
inputFile.SendKeys(file.Path);
|
||||
|
||||
var fileContainer = Browser.FindElement(By.Id($"file-{file.Name}"));
|
||||
var fileSizeElement = fileContainer.FindElement(By.Id("file-size"));
|
||||
var fileContentElement = fileContainer.FindElement(By.Id("file-content"));
|
||||
|
||||
// Validate that the file was uploaded correctly
|
||||
Browser.Equal(file.Contents.Length.ToString(), () => fileSizeElement.Text);
|
||||
Browser.Equal(file.Text, () => fileContentElement.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUploadMultipleFiles()
|
||||
{
|
||||
// Create multiple small text files
|
||||
var files = Enumerable.Range(1, 3)
|
||||
.Select(i => TempFile.Create(_tempDirectory, "txt", $"Contents of file {i}."))
|
||||
.ToList();
|
||||
|
||||
// Upload each file
|
||||
var inputFile = Browser.FindElement(By.Id("input-file"));
|
||||
inputFile.SendKeys(string.Join("\n", files.Select(f => f.Path)));
|
||||
|
||||
// VAlidate that each file was uploaded correctly
|
||||
Assert.All(files, file =>
|
||||
{
|
||||
var fileContainer = Browser.FindElement(By.Id($"file-{file.Name}"));
|
||||
var fileSizeElement = fileContainer.FindElement(By.Id("file-size"));
|
||||
var fileContentElement = fileContainer.FindElement(By.Id("file-content"));
|
||||
|
||||
Browser.Equal(file.Contents.Length.ToString(), () => fileSizeElement.Text);
|
||||
Browser.Equal(file.Text, () => fileContentElement.Text);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUploadAndConvertImageFile()
|
||||
{
|
||||
var sourceImageId = "image-source";
|
||||
|
||||
// Get the source image base64
|
||||
var base64 = Browser.ExecuteJavaScript<string>($@"
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
const image = document.getElementById('{sourceImageId}');
|
||||
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
context.drawImage(image, 0, 0, image.naturalWidth, image.naturalHeight);
|
||||
|
||||
return canvas.toDataURL().split(',').pop();");
|
||||
|
||||
// Save the image file locally
|
||||
var file = TempFile.Create(_tempDirectory, "png", Convert.FromBase64String(base64));
|
||||
|
||||
// Re-upload the image file (it will be converted to a JPEG and scaled to fix 640x480)
|
||||
var inputFile = Browser.FindElement(By.Id("input-image"));
|
||||
inputFile.SendKeys(file.Path);
|
||||
|
||||
// Validate that the image was converted without error and is the correct size
|
||||
var uploadedImage = Browser.FindElement(By.Id("image-uploaded"));
|
||||
|
||||
Browser.Equal(480, () => uploadedImage.Size.Width);
|
||||
Browser.Equal(480, () => uploadedImage.Size.Height);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Directory.Delete(_tempDirectory, recursive: true);
|
||||
}
|
||||
|
||||
private struct TempFile
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Path { get; }
|
||||
public byte[] Contents { get; }
|
||||
|
||||
public string Text => Encoding.ASCII.GetString(Contents);
|
||||
|
||||
private TempFile(string tempDirectory, string extension, byte[] contents)
|
||||
{
|
||||
Name = $"{Guid.NewGuid():N}.{extension}";
|
||||
Path = $"{tempDirectory}\\{Name}";
|
||||
Contents = contents;
|
||||
}
|
||||
|
||||
public static TempFile Create(string tempDirectory, string extension, byte[] contents)
|
||||
{
|
||||
var file = new TempFile(tempDirectory, extension, contents);
|
||||
|
||||
File.WriteAllBytes(file.Path, contents);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
public static TempFile Create(string tempDirectory, string extension, string text)
|
||||
=> Create(tempDirectory, extension, Encoding.ASCII.GetBytes(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@
|
|||
<option value="BasicTestApp.HttpClientTest.CookieCounterComponent">HttpClient cookies</option>
|
||||
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
|
||||
<option value="BasicTestApp.InputEventComponent">Input events</option>
|
||||
<option value="BasicTestApp.InputFileComponent">Input file</option>
|
||||
<option value="BasicTestApp.InteropComponent">Interop component</option>
|
||||
<option value="BasicTestApp.InteropOnInitializationComponent">Interop on initialization</option>
|
||||
<option value="BasicTestApp.JsonSerializationCases">JSON serialization</option>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
@using System.IO;
|
||||
@using Microsoft.AspNetCore.Components.Web.Extensions
|
||||
|
||||
<h1>File preview</h1>
|
||||
|
||||
<InputFile OnChange="LoadFiles" id="input-file" multiple /><br />
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<p>Loading...</p><br />
|
||||
}
|
||||
|
||||
@foreach (var (file, content) in loadedFiles)
|
||||
{
|
||||
<p id="file-@(file.Name)">
|
||||
<strong>File name:</strong> @(file.Name)<br />
|
||||
<strong>File size (bytes):</strong> <span id="file-size">@(file.Size)</span><br />
|
||||
<strong>File content:</strong> <span id="file-content">@content</span><br />
|
||||
</p>
|
||||
}
|
||||
|
||||
<h1>Image upload</h1>
|
||||
|
||||
<InputFile OnChange="LoadImage" id="input-image" /><br />
|
||||
|
||||
@if (imageDataUri != null)
|
||||
{
|
||||
<p>
|
||||
Uploaded image:<br />
|
||||
<img id="image-uploaded" src="@imageDataUri" />
|
||||
</p>
|
||||
}
|
||||
|
||||
<p>
|
||||
Source image:<br />
|
||||
<img id="image-source" src="images/blazor_logo_1000x.png" />
|
||||
</p>
|
||||
|
||||
@code {
|
||||
Dictionary<IBrowserFile, string> loadedFiles = new Dictionary<IBrowserFile, string>();
|
||||
|
||||
bool isLoading;
|
||||
|
||||
string imageDataUri;
|
||||
|
||||
async Task LoadFiles(InputFileChangeEventArgs e)
|
||||
{
|
||||
isLoading = true;
|
||||
loadedFiles.Clear();
|
||||
|
||||
foreach (var file in e.Files)
|
||||
{
|
||||
StateHasChanged();
|
||||
|
||||
using var reader = new StreamReader(file.OpenReadStream());
|
||||
|
||||
loadedFiles.Add(file, await reader.ReadToEndAsync());
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
async Task LoadImage(InputFileChangeEventArgs e)
|
||||
{
|
||||
var file = e.Files.SingleOrDefault();
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
var format = "image/jpeg";
|
||||
var imageFile = await file.ToImageFileAsync(format, 640, 480);
|
||||
|
||||
using var fileStream = imageFile.OpenReadStream();
|
||||
using var memoryStream = new MemoryStream();
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
|
||||
imageDataUri = $"data:{format};base64,{Convert.ToBase64String(memoryStream.ToArray())}";
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -45,6 +45,8 @@
|
|||
|
||||
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/headManager.js"></script>
|
||||
|
||||
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/inputFile.js"></script>
|
||||
|
||||
<!-- Used by ExternalContentPackage -->
|
||||
<script src="_content/TestContentPackage/prompt.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@
|
|||
|
||||
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/headManager.js"></script>
|
||||
|
||||
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/inputFile.js"></script>
|
||||
|
||||
<!-- Used by ExternalContentPackage -->
|
||||
<script src="_content/TestContentPackage/prompt.js"></script>
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an instance of a JavaScript runtime to which calls may be dispatched without JSON marshalling.
|
||||
/// Not all JavaScript runtimes support this capability. Currently it is only supported on WebAssembly and for
|
||||
/// security reasons, will never be supported for .NET code that runs on the server.
|
||||
/// This is an advanced mechanism that should only be used in performance-critical scenarios.
|
||||
/// </summary>
|
||||
public interface IJSUnmarshalledRuntime
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes the JavaScript function registered with the specified identifier.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||
/// <returns>The result of the function invocation.</returns>
|
||||
TResult InvokeUnmarshalled<TResult>(string identifier);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the JavaScript function registered with the specified identifier.
|
||||
/// </summary>
|
||||
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||
/// <param name="arg0">The first argument.</param>
|
||||
/// <returns>The result of the function invocation.</returns>
|
||||
TResult InvokeUnmarshalled<T0, TResult>(string identifier, T0 arg0);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the JavaScript function registered with the specified identifier.
|
||||
/// </summary>
|
||||
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||
/// <typeparam name="T1">The type of the second argument.</typeparam>
|
||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||
/// <param name="arg0">The first argument.</param>
|
||||
/// <param name="arg1">The second argument.</param>
|
||||
/// <returns>The result of the function invocation.</returns>
|
||||
TResult InvokeUnmarshalled<T0, T1, TResult>(string identifier, T0 arg0, T1 arg1);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the JavaScript function registered with the specified identifier.
|
||||
/// </summary>
|
||||
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||
/// <typeparam name="T1">The type of the second argument.</typeparam>
|
||||
/// <typeparam name="T2">The type of the third argument.</typeparam>
|
||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||
/// <param name="arg0">The first argument.</param>
|
||||
/// <param name="arg1">The second argument.</param>
|
||||
/// <param name="arg2">The third argument.</param>
|
||||
/// <returns>The result of the function invocation.</returns>
|
||||
TResult InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue