Skip to content

Commit c1977a9

Browse files
authored
[dev-v5] Add DialogService.RegisterInputFileAsync (#3777)
1 parent ba9a790 commit c1977a9

13 files changed

+628
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
@inject IDialogService DialogService
2+
@implements IAsyncDisposable
3+
4+
<FluentButton Id="OpenInputFile" Appearance="ButtonAppearance.Primary" Margin="@Margin.Vertical3">
5+
Upload files
6+
</FluentButton>
7+
8+
<FluentProgressBar Value="@LoadingPercentage" />
9+
10+
<FluentLabel Weight="LabelWeight.Semibold">Files uploaded:</FluentLabel>
11+
<FluentLabel>@(string.Join("; ", Files.Select(i => i.Name)))</FluentLabel>
12+
13+
@code
14+
{
15+
int LoadingPercentage = 0;
16+
FluentInputFileEventArgs[] Files = [];
17+
18+
protected override async Task OnInitializedAsync()
19+
{
20+
// Register the input file dialog with a button click.
21+
await DialogService.RegisterInputFileAsync("OpenInputFile", OnCompletedAsync, options =>
22+
{
23+
options.Multiple = true;
24+
options.OnFileErrorAsync = (e) => DialogService.ShowErrorAsync(e.ErrorMessage);
25+
options.OnProgressChangeAsync = (e) =>
26+
{
27+
LoadingPercentage = e.ProgressPercent;
28+
StateHasChanged();
29+
return Task.CompletedTask;
30+
};
31+
});
32+
}
33+
34+
private Task OnCompletedAsync(IEnumerable<FluentInputFileEventArgs> files)
35+
{
36+
Files = files.Where(i => !i.IsCancelled).ToArray();
37+
38+
// For the demo, delete these files.
39+
Files.ToList().ForEach(file => file.LocalFile?.Delete());
40+
41+
// Show the files in UI.
42+
LoadingPercentage = 100;
43+
StateHasChanged();
44+
45+
return Task.CompletedTask;
46+
}
47+
48+
// Unregister the input file
49+
public async ValueTask DisposeAsync() => await DialogService.UnregisterInputFileAsync("OpenInputFile");
50+
}

examples/Demo/FluentUI.Demo.Client/Documentation/Components/InputFile/FluentInputFile.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ The component can be customized by adapting the `ChildContent`.
1919
The areas that need to be associated with the opening of the file selection dialog
2020
must be included in a `label for` that references the component `Id`: E.g. `<label for=“my-file-uploader”>browse...</label>`.
2121

22-
> ℹ️ By default, this component uses the `SaveToTemporaryFolder` mode, which creates a local file. However, this might not always be possible depending on the user's permissions on the host system. You may need to change the `InputFileMode` based on your specific use case.
22+
> [!NOTE] By default, this component uses the `SaveToTemporaryFolder` mode, which creates a local file. However,
23+
> this might not always be possible depending on the user's permissions on the host system.
24+
> You may need to change the `InputFileMode` based on your specific use case.
2325
2426
{{ InputFileDefault }}
2527

@@ -30,6 +32,37 @@ specify the component that will trigger the opening of the file selection dialog
3032

3133
{{ InputFileByCode }}
3234

35+
## DialogService
36+
37+
A easy way to open the file selection dialog is to use the injected **DialogService** service.
38+
1. During the `OnInitialized` method, **register** your trigger HTML element
39+
using the method `MyFileInstance = DialogService.RegisterInputFileAsync(id)`.
40+
1. Set the `OnCompletedAsync` method to handle the files uploaded.
41+
1. Don't forget to **unregister** the trigger element in the `Dispose` method
42+
using `MyFileInstance.UnregisterAsync();` or `DialogService.UnregisterInputFileAsync(id)`.
43+
44+
```csharp
45+
[Inject]
46+
public IDialogService DialogService { get; set; }
47+
48+
protected override async Task OnInitializedAsync()
49+
{
50+
await DialogService.RegisterInputFileAsync("MyButton", OnCompletedAsync, options => { /* ... */ });
51+
}
52+
53+
private Task OnCompletedAsync(IEnumerable<FluentInputFileEventArgs> files)
54+
{
55+
// `files` contains the uploaded files
56+
}
57+
58+
public async ValueTask DisposeAsync()
59+
{
60+
await DialogService.UnregisterInputFileAsync("MyButton");
61+
}
62+
```
63+
64+
{{ InputFileDialogService }}
65+
3366
## Mode = InputFileMode.Buffer
3467

3568
By default, the component has a parameter `Mode=“InputFileMode.SaveToTemporaryFolder”`.
@@ -45,7 +78,7 @@ This mode is recommended if you can't store files locally, are working with smal
4578
If you want to transfer very large files and do not want to save your files locally (in a temporary folder, for example),
4679
you can retrieve the file stream from the `OnFileUploaded` event and the `file.Stream` property. Keep in mind that you will need to implement stream handling yourself.
4780

48-
> ⚠️ Remember to always dispose each stream to prevent memory leaks!
81+
> [!WARNING] Remember to always dispose each stream to prevent memory leaks!
4982
5083
{{ InputFileStream }}
5184

src/Core/Components/Dialog/FluentDialogProvider.razor

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,45 @@
66
{
77
@foreach (var dialog in DialogService.Items.Values.OrderBy(i => i.Index))
88
{
9-
<FluentDialog Id="@dialog.Id"
10-
Class="@dialog.Options?.ClassValue"
11-
Style="@dialog.Options?.StyleValue"
12-
Data="@dialog.Options?.Data"
13-
Instance="@dialog"
14-
OnStateChange="@(dialog.Options?.OnStateChange ?? EmptyOnStateChange)"
15-
@attributes="@AdditionalAttributes" />
9+
@* FluentInputFile *@
10+
if (dialog.ComponentType == typeof(Microsoft.AspNetCore.Components.Forms.InputFile))
11+
{
12+
var anchorId = dialog.Options?.Parameters["ElementId"] as string;
13+
var options = dialog.Options?.Parameters["Options"] as InputFileOptions ?? new InputFileOptions();
14+
var onCompleted = dialog.Options?.Parameters["OnCompletedAsync"] as Func<IEnumerable<FluentInputFileEventArgs>, Task> ?? (_ => Task.CompletedTask);
15+
var onFileUploaded = dialog.Options?.Parameters["OnFileUploadedAsync"] as Func<FluentInputFileEventArgs, Task> ?? (_ => Task.CompletedTask);
16+
var onProgressChange = dialog.Options?.Parameters["OnProgressChangeAsync"] as Func<FluentInputFileEventArgs, Task> ?? (_ => Task.CompletedTask);
17+
var onFileError = dialog.Options?.Parameters["OnFileErrorAsync"] as Func<FluentInputFileErrorEventArgs, Task> ?? (_ => Task.CompletedTask);
18+
19+
if (!string.IsNullOrEmpty(anchorId))
20+
{
21+
<FluentInputFile Id="@dialog.Id"
22+
DragDropZoneVisible="false"
23+
AnchorId="@anchorId"
24+
OnCompleted="@(e => onCompleted.Invoke(e))"
25+
Accept="@options.Accept"
26+
Multiple="@options.Multiple"
27+
MaximumFileCount="@options.MaximumFileCount"
28+
MaximumFileSize="@options.MaximumFileSize"
29+
BufferSize="@options.BufferSize"
30+
Mode="@options.Mode"
31+
OnFileUploaded="@(e => onFileUploaded.Invoke(e))"
32+
OnProgressChange="@(e => onProgressChange.Invoke(e))"
33+
OnFileError="@(e => onFileError.Invoke(e))" />
34+
}
35+
}
36+
37+
@* FluentDialog *@
38+
else
39+
{
40+
<FluentDialog Id="@dialog.Id"
41+
Class="@dialog.Options?.ClassValue"
42+
Style="@dialog.Options?.StyleValue"
43+
Data="@dialog.Options?.Data"
44+
Instance="@dialog"
45+
OnStateChange="@(dialog.Options?.OnStateChange ?? EmptyOnStateChange)"
46+
@attributes="@AdditionalAttributes" />
47+
}
1648
}
1749
}
1850
</div>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// ------------------------------------------------------------------------
2+
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
3+
// ------------------------------------------------------------------------
4+
5+
using Microsoft.AspNetCore.Components.Forms;
6+
7+
namespace Microsoft.FluentUI.AspNetCore.Components;
8+
9+
public partial class DialogService : IDialogService
10+
{
11+
/// <see cref="IDialogService.RegisterInputFileAsync(string, Func{IEnumerable{FluentInputFileEventArgs}, Task}, Action{InputFileOptions}?)"/>
12+
public virtual async Task<InputFileInstance> RegisterInputFileAsync(string elementId, Func<IEnumerable<FluentInputFileEventArgs>, Task> onCompletedAsync, Action<InputFileOptions>? options = null)
13+
{
14+
if (this.ProviderNotAvailable())
15+
{
16+
throw new FluentServiceProviderException<FluentDialogProvider>();
17+
}
18+
19+
// Options
20+
var config = new InputFileOptions();
21+
options?.Invoke(config);
22+
23+
// Register the dialog
24+
var instance = new DialogInstance(this, typeof(InputFile), new DialogOptions()
25+
{
26+
// These parameters are passed to the `FluentDialogProvider` component
27+
Parameters = new Dictionary<string, object?>(StringComparer.Ordinal)
28+
{
29+
{ "ElementId", elementId },
30+
{ "OnCompletedAsync", onCompletedAsync },
31+
{ "OnFileUploadedAsync", config.OnFileUploadedAsync },
32+
{ "OnProgressChangeAsync", config.OnProgressChangeAsync },
33+
{ "OnFileErrorAsync", config.OnFileErrorAsync },
34+
{ "Options", config},
35+
},
36+
});
37+
38+
var fileInstance = new InputFileInstance(ServiceProvider, instance, elementId);
39+
40+
// Add the dialog to the service, and render it.
41+
ServiceProvider.Items.TryAdd(fileInstance.Id, instance ?? throw new InvalidOperationException("Failed to register an InputFile."));
42+
await ServiceProvider.OnUpdatedAsync.Invoke(instance);
43+
44+
return fileInstance;
45+
}
46+
47+
/// <see cref="IDialogService.UnregisterInputFileAsync(string)"/>
48+
public virtual async Task UnregisterInputFileAsync(string elementId)
49+
{
50+
var instances = ServiceProvider.Items
51+
.Where(x => x.Value.Options.Parameters.ContainsKey("ElementId") &&
52+
string.Equals(x.Value.Options.Parameters["ElementId"]?.ToString(), elementId, StringComparison.Ordinal))
53+
.ToList();
54+
55+
foreach (var instance in instances)
56+
{
57+
if (ServiceProvider.Items.TryRemove(instance.Value.Id, out var dialogInstance))
58+
{
59+
await ServiceProvider.OnUpdatedAsync.Invoke(dialogInstance);
60+
}
61+
}
62+
}
63+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// ------------------------------------------------------------------------
2+
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
3+
// ------------------------------------------------------------------------
4+
5+
namespace Microsoft.FluentUI.AspNetCore.Components;
6+
7+
/// <summary />
8+
public partial interface IDialogService
9+
{
10+
/// <summary>
11+
/// Registers a new <see cref="FluentInputFile">element</see> based on the trigger element id.
12+
/// </summary>
13+
/// <param name="elementId">HTML element identifier.</param>
14+
/// <param name="onCompletedAsync">Callback to be invoked when the file upload is completed. Call `StateHasChanged` in your method to refresh your UI.</param>
15+
/// <param name="options">Options for the <see cref="FluentInputFile">element</see>.</param>
16+
/// <returns></returns>
17+
Task<InputFileInstance> RegisterInputFileAsync(string elementId, Func<IEnumerable<FluentInputFileEventArgs>, Task> onCompletedAsync, Action<InputFileOptions>? options = null);
18+
19+
/// <summary>
20+
/// Unregisters the <see cref="FluentInputFile">element</see> based on the trigger element id.
21+
/// </summary>
22+
/// <param name="elementId"></param>
23+
Task UnregisterInputFileAsync(string elementId);
24+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// ------------------------------------------------------------------------
2+
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
3+
// ------------------------------------------------------------------------
4+
5+
namespace Microsoft.FluentUI.AspNetCore.Components;
6+
7+
/// <summary>
8+
/// Instance of an input file.
9+
/// </summary>
10+
public class InputFileInstance : IAsyncDisposable
11+
{
12+
private readonly IFluentServiceBase<IDialogInstance> _serviceProvider;
13+
14+
/// <summary />
15+
internal InputFileInstance(IFluentServiceBase<IDialogInstance> serviceProvider, IDialogInstance instance, string anchorId)
16+
{
17+
_serviceProvider = serviceProvider;
18+
AnchorId = anchorId;
19+
20+
Id = instance.Id;
21+
}
22+
23+
/// <summary>
24+
/// Gets the identifier of the input file instance.
25+
/// </summary>
26+
public string Id { get; }
27+
28+
/// <summary>
29+
/// Gets the identifier of the anchor element.
30+
/// </summary>
31+
public string AnchorId { get; }
32+
33+
/// <summary>
34+
/// Removes the <see cref="FluentInputFile"/> from the <see cref="FluentDialogProvider"/>.
35+
/// </summary>
36+
public async ValueTask UnregisterAsync()
37+
{
38+
if (_serviceProvider.Items.TryRemove(Id, out var dialogInstance))
39+
{
40+
await _serviceProvider.OnUpdatedAsync.Invoke(dialogInstance);
41+
}
42+
}
43+
44+
/// <summary>
45+
/// Disposes the <see cref="FluentInputFile"/> from the <see cref="FluentDialogProvider"/>.
46+
/// </summary>
47+
/// <returns></returns>
48+
public async ValueTask DisposeAsync()
49+
{
50+
await UnregisterAsync();
51+
}
52+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// ------------------------------------------------------------------------
2+
// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
3+
// ------------------------------------------------------------------------
4+
5+
namespace Microsoft.FluentUI.AspNetCore.Components;
6+
7+
/// <summary>
8+
/// Represents configuration options for handling file input and upload functionality.
9+
/// </summary>
10+
public class InputFileOptions : IInputFileOptions
11+
{
12+
/// <inheritdoc cref="IInputFileOptions.Multiple" />
13+
public bool Multiple { get; set; }
14+
15+
/// <inheritdoc cref="IInputFileOptions.MaximumFileCount" />
16+
public int MaximumFileCount { get; set; } = 10;
17+
18+
/// <inheritdoc cref="IInputFileOptions.MaximumFileSize" />
19+
public long MaximumFileSize { get; set; } = 10 * 1024 * 1024;
20+
21+
/// <inheritdoc cref="IInputFileOptions.BufferSize" />
22+
public uint BufferSize { get; set; } = 10 * 1024;
23+
24+
/// <inheritdoc cref="IInputFileOptions.Accept" />
25+
public string Accept { get; set; } = string.Empty;
26+
27+
/// <inheritdoc cref="IInputFileOptions.Mode" />
28+
public InputFileMode Mode { get; set; }
29+
30+
/// <summary>
31+
/// Raise when a file is completely uploaded.
32+
/// </summary>
33+
public Func<FluentInputFileEventArgs, Task>? OnFileUploadedAsync { get; set; }
34+
35+
/// <summary>
36+
/// Raise when a progression step is updated.
37+
/// </summary>
38+
public Func<FluentInputFileEventArgs, Task>? OnProgressChangeAsync { get; set; }
39+
40+
/// <summary>
41+
/// Raise when a file raised an error.
42+
/// </summary>
43+
public Func<FluentInputFileErrorEventArgs, Task>? OnFileErrorAsync { get; set; }
44+
}

src/Core/Components/InputFile/FluentInputFile.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
2222
/// cref="Mode"/> property to specify the file reading mode, such as buffering, saving to a temporary folder, or
2323
/// streaming. The <see cref="OnFileUploaded"/> and <see cref="OnCompleted"/> events can be used to handle file upload
2424
/// completion.</remarks>
25-
public partial class FluentInputFile : FluentComponentBase, IAsyncDisposable
25+
public partial class FluentInputFile : FluentComponentBase, IAsyncDisposable, IInputFileOptions
2626
{
2727
private const string JAVASCRIPT_FILE = FluentJSModule.JAVASCRIPT_ROOT + "InputFile/FluentInputFile.razor.js";
2828
private ElementReference? _containerElement;

0 commit comments

Comments
 (0)