Skip to content

Commit 0b1042c

Browse files
InputFile Component (#24640)
1 parent a700662 commit 0b1042c

25 files changed

+1104
-51
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.AspNetCore.Components.Web.Extensions
10+
{
11+
internal class BrowserFile : IBrowserFile
12+
{
13+
internal InputFile Owner { get; set; } = default!;
14+
15+
public int Id { get; set; }
16+
17+
public string Name { get; set; } = string.Empty;
18+
19+
public DateTime LastModified { get; set; }
20+
21+
public long Size { get; set; }
22+
23+
public string Type { get; set; } = string.Empty;
24+
25+
public string? RelativePath { get; set; }
26+
27+
public Stream OpenReadStream(CancellationToken cancellationToken = default)
28+
=> Owner.OpenReadStream(this, cancellationToken);
29+
30+
public Task<IBrowserFile> ToImageFileAsync(string format, int maxWidth, int maxHeight)
31+
=> Owner.ConvertToImageFileAsync(this, format, maxWidth, maxHeight);
32+
}
33+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.AspNetCore.Components.Web.Extensions
10+
{
11+
internal abstract class BrowserFileStream : Stream
12+
{
13+
private long _position;
14+
15+
protected BrowserFile File { get; }
16+
17+
protected BrowserFileStream(BrowserFile file)
18+
{
19+
File = file;
20+
}
21+
22+
public override bool CanRead => true;
23+
24+
public override bool CanSeek => false;
25+
26+
public override bool CanWrite => false;
27+
28+
public override long Length => File.Size;
29+
30+
public override long Position
31+
{
32+
get => _position;
33+
set => throw new NotSupportedException();
34+
}
35+
36+
public override void Flush()
37+
=> throw new NotSupportedException();
38+
39+
public override int Read(byte[] buffer, int offset, int count)
40+
=> throw new NotSupportedException("Synchronous reads are not supported.");
41+
42+
public override long Seek(long offset, SeekOrigin origin)
43+
=> throw new NotSupportedException();
44+
45+
public override void SetLength(long value)
46+
=> throw new NotSupportedException();
47+
48+
public override void Write(byte[] buffer, int offset, int count)
49+
=> throw new NotSupportedException();
50+
51+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
52+
=> ReadAsync(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
53+
54+
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
55+
{
56+
int maxBytesToRead = (int)(Length - Position);
57+
58+
if (maxBytesToRead > buffer.Length)
59+
{
60+
maxBytesToRead = buffer.Length;
61+
}
62+
63+
if (maxBytesToRead <= 0)
64+
{
65+
return 0;
66+
}
67+
68+
var bytesRead = await CopyFileDataIntoBuffer(_position, buffer.Slice(0, maxBytesToRead), cancellationToken);
69+
70+
_position += bytesRead;
71+
72+
return bytesRead;
73+
}
74+
75+
protected abstract ValueTask<int> CopyFileDataIntoBuffer(long sourceOffset, Memory<byte> destination, CancellationToken cancellationToken);
76+
}
77+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.AspNetCore.Components.Web.Extensions
10+
{
11+
/// <summary>
12+
/// Represents the data of a file selected from an <see cref="InputFile"/> component.
13+
/// </summary>
14+
public interface IBrowserFile
15+
{
16+
/// <summary>
17+
/// Gets the name of the file.
18+
/// </summary>
19+
string Name { get; }
20+
21+
/// <summary>
22+
/// Gets the last modified date.
23+
/// </summary>
24+
DateTime LastModified { get; }
25+
26+
/// <summary>
27+
/// Gets the size of the file in bytes.
28+
/// </summary>
29+
long Size { get; }
30+
31+
/// <summary>
32+
/// Gets the MIME type of the file.
33+
/// </summary>
34+
string Type { get; }
35+
36+
/// <summary>
37+
/// Opens the stream for reading the uploaded file.
38+
/// </summary>
39+
/// <param name="cancellationToken">A cancellation token to signal the cancellation of streaming file data.</param>
40+
Stream OpenReadStream(CancellationToken cancellationToken = default);
41+
42+
/// <summary>
43+
/// Converts the current image file to a new one of the specified file type and maximum file dimensions.
44+
/// </summary>
45+
/// <remarks>
46+
/// The image will be scaled to fit the specified dimensions while preserving the original aspect ratio.
47+
/// </remarks>
48+
/// <param name="format">The new image format.</param>
49+
/// <param name="maxWith">The maximum image width.</param>
50+
/// <param name="maxHeight">The maximum image height</param>
51+
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
52+
Task<IBrowserFile> ToImageFileAsync(string format, int maxWith, int maxHeight);
53+
}
54+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
6+
namespace Microsoft.AspNetCore.Components.Web.Extensions
7+
{
8+
internal interface IInputFileJsCallbacks
9+
{
10+
Task NotifyChange(BrowserFile[] files);
11+
}
12+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Components.Rendering;
10+
using Microsoft.Extensions.Options;
11+
using Microsoft.JSInterop;
12+
13+
namespace Microsoft.AspNetCore.Components.Web.Extensions
14+
{
15+
/// <summary>
16+
/// A component that wraps the HTML file input element and exposes a <see cref="Stream"/> for each file's contents.
17+
/// </summary>
18+
public class InputFile : ComponentBase, IInputFileJsCallbacks, IDisposable
19+
{
20+
private ElementReference _inputFileElement;
21+
22+
private IJSUnmarshalledRuntime? _jsUnmarshalledRuntime;
23+
24+
private InputFileJsCallbacksRelay? _jsCallbacksRelay;
25+
26+
[Inject]
27+
private IJSRuntime JSRuntime { get; set; } = default!;
28+
29+
[Inject]
30+
private IOptions<RemoteBrowserFileStreamOptions> Options { get; set; } = default!;
31+
32+
/// <summary>
33+
/// Gets or sets the event callback that will be invoked when the collection of selected files changes.
34+
/// </summary>
35+
[Parameter]
36+
public EventCallback<InputFileChangeEventArgs> OnChange { get; set; }
37+
38+
/// <summary>
39+
/// Gets or sets a collection of additional attributes that will be applied to the input element.
40+
/// </summary>
41+
[Parameter(CaptureUnmatchedValues = true)]
42+
public IDictionary<string, object>? AdditionalAttributes { get; set; }
43+
44+
protected override void OnInitialized()
45+
{
46+
_jsUnmarshalledRuntime = JSRuntime as IJSUnmarshalledRuntime;
47+
}
48+
49+
protected override async Task OnAfterRenderAsync(bool firstRender)
50+
{
51+
if (firstRender)
52+
{
53+
_jsCallbacksRelay = new InputFileJsCallbacksRelay(this);
54+
await JSRuntime.InvokeVoidAsync(InputFileInterop.Init, _jsCallbacksRelay.DotNetReference, _inputFileElement);
55+
}
56+
}
57+
58+
protected override void BuildRenderTree(RenderTreeBuilder builder)
59+
{
60+
builder.OpenElement(0, "input");
61+
builder.AddMultipleAttributes(1, AdditionalAttributes);
62+
builder.AddAttribute(2, "type", "file");
63+
builder.AddElementReferenceCapture(3, elementReference => _inputFileElement = elementReference);
64+
builder.CloseElement();
65+
}
66+
67+
internal Stream OpenReadStream(BrowserFile file, CancellationToken cancellationToken)
68+
=> _jsUnmarshalledRuntime != null ?
69+
(Stream)new SharedBrowserFileStream(JSRuntime, _jsUnmarshalledRuntime, _inputFileElement, file) :
70+
new RemoteBrowserFileStream(JSRuntime, _inputFileElement, file, Options.Value, cancellationToken);
71+
72+
internal async Task<IBrowserFile> ConvertToImageFileAsync(BrowserFile file, string format, int maxWidth, int maxHeight)
73+
{
74+
var imageFile = await JSRuntime.InvokeAsync<BrowserFile>(InputFileInterop.ToImageFile, _inputFileElement, file.Id, format, maxWidth, maxHeight);
75+
76+
imageFile.Owner = this;
77+
78+
return imageFile;
79+
}
80+
81+
Task IInputFileJsCallbacks.NotifyChange(BrowserFile[] files)
82+
{
83+
foreach (var file in files)
84+
{
85+
file.Owner = this;
86+
}
87+
88+
return OnChange.InvokeAsync(new InputFileChangeEventArgs(files));
89+
}
90+
91+
void IDisposable.Dispose()
92+
{
93+
_jsCallbacksRelay?.Dispose();
94+
}
95+
}
96+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.AspNetCore.Components.Web.Extensions
8+
{
9+
/// <summary>
10+
/// Supplies information about an <see cref="InputFile.OnChange"/> event being raised.
11+
/// </summary>
12+
public class InputFileChangeEventArgs : EventArgs
13+
{
14+
/// <summary>
15+
/// The updated file entries list.
16+
/// </summary>
17+
public IReadOnlyList<IBrowserFile> Files { get; }
18+
19+
/// <summary>
20+
/// Constructs a new <see cref="InputFileChangeEventArgs"/> instance.
21+
/// </summary>
22+
/// <param name="files">The updated file entries list.</param>
23+
public InputFileChangeEventArgs(IReadOnlyList<IBrowserFile> files)
24+
{
25+
Files = files;
26+
}
27+
}
28+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Components.Web.Extensions
5+
{
6+
internal static class InputFileInterop
7+
{
8+
private const string JsFunctionsPrefix = "_blazorInputFile.";
9+
10+
public const string Init = JsFunctionsPrefix + "init";
11+
12+
public const string EnsureArrayBufferReadyForSharedMemoryInterop = JsFunctionsPrefix + "ensureArrayBufferReadyForSharedMemoryInterop";
13+
14+
public const string ReadFileData = JsFunctionsPrefix + "readFileData";
15+
16+
public const string ReadFileDataSharedMemory = JsFunctionsPrefix + "readFileDataSharedMemory";
17+
18+
public const string ToImageFile = JsFunctionsPrefix + "toImageFile";
19+
}
20+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using Microsoft.JSInterop;
7+
8+
namespace Microsoft.AspNetCore.Components.Web.Extensions
9+
{
10+
internal class InputFileJsCallbacksRelay : IDisposable
11+
{
12+
private readonly IInputFileJsCallbacks _callbacks;
13+
14+
public IDisposable DotNetReference { get; }
15+
16+
public InputFileJsCallbacksRelay(IInputFileJsCallbacks callbacks)
17+
{
18+
_callbacks = callbacks;
19+
20+
DotNetReference = DotNetObjectReference.Create(this);
21+
}
22+
23+
[JSInvokable]
24+
public Task NotifyChange(BrowserFile[] files)
25+
=> _callbacks.NotifyChange(files);
26+
27+
public void Dispose()
28+
{
29+
DotNetReference.Dispose();
30+
}
31+
}
32+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Runtime.InteropServices;
5+
6+
namespace Microsoft.AspNetCore.Components.Web.Extensions
7+
{
8+
[StructLayout(LayoutKind.Explicit)]
9+
internal struct ReadRequest
10+
{
11+
[FieldOffset(0)]
12+
public string InputFileElementReferenceId;
13+
14+
[FieldOffset(4)]
15+
public int FileId;
16+
17+
[FieldOffset(8)]
18+
public long SourceOffset;
19+
20+
[FieldOffset(16)]
21+
public byte[] Destination;
22+
23+
[FieldOffset(20)]
24+
public int DestinationOffset;
25+
26+
[FieldOffset(24)]
27+
public int MaxBytes;
28+
}
29+
}

0 commit comments

Comments
 (0)