Skip to content

Commit

Permalink
Markdown: merge uploaded files callback (#5980)
Browse files Browse the repository at this point in the history
* Adds Debug.WriteLine to see the js exception.

* Fixes the issue where reference exists in c#, but the file is deleted (by creating new empty dictionary) in js. Introduces "bug" where file entries in js keeps growing. To be fixed in the next commit.

* Fixes issue where fileEntries were growing indifferently in js. The deleting of the file entry is bound to the lifetime of the c# FileEntry instance. When garbage collecting the instance, the entry is deleted from js. This ensures synchronization of existing (reachable) objects between c# and js. This commit extends the IFileEntryOwner.cs to keep the fileEntry deleting logic out of the interface implementations (FileEdit and Markdown)

* Implements debounce to buffer single calls of imageUploadFunction into one call to .net. Same on the .net side.

* docs update

* general "null" check on js dictionary.

* wrap the RemoveFileEntry in try catch.

* Formating

* RemoveFileEntry after all is ended

* removing fileEntry for fileEdit too. Deleting finalizer.

* Formating

* await for RemoveFileEntry

---------

Co-authored-by: Mladen Macanovic <mladen.macanovic@blazorise.com>
  • Loading branch information
tesar-tech and stsrki authored Feb 25, 2025
1 parent 4b74b38 commit ae0cd5d
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@
<DocsPageSection>
<DocsPageSectionHeader Title="Upload image">
Uploading an image has a similar API to our <Code>FileEdit</Code> component and is fairly simple to use.
<Strong>Note that</Strong> the events related to file uploads fire separately for each file instead of grouping them.
This behavior comes from the underlying Easy Markdown Editor, which processes files individually.
</DocsPageSectionHeader>
<DocsPageSectionContent Outlined FullWidth>
<MarkdownUploadImageExample />
Expand Down
5 changes: 5 additions & 0 deletions Source/Blazorise/Components/FileEdit/FileEdit.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ public async Task NotifyChange( FileEntry[] files )

await Changed.InvokeAsync( new( files ) );

foreach ( var file in files )
{
await file.Owner.RemoveFileEntry( file.Id );
}

await InvokeAsync( StateHasChanged );
}

Expand Down
8 changes: 7 additions & 1 deletion Source/Blazorise/Interfaces/Modules/IJSFileModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,11 @@ public interface IJSFileModule : IBaseJSModule
/// <returns>A task that represents the asynchronous operation.</returns>
ValueTask<IJSStreamReference> ReadDataAsync( ElementReference elementRef, int fileEntryId, CancellationToken cancellationToken = default );


/// <summary>
/// Removes file entry from js dictionary
/// </summary>
/// <param name="elementRef">Reference to the rendered element.</param>
/// <param name="fileEntryId"></param>
/// <returns>A task that represents the asynchronous operation.</returns>
ValueTask RemoveFileEntry( ElementReference elementRef, int fileEntryId );
}
7 changes: 5 additions & 2 deletions Source/Blazorise/Modules/BaseJSModule.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#region Using directives
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Blazorise.Extensions;
using Microsoft.JSInterop;
Expand Down Expand Up @@ -136,8 +137,9 @@ protected virtual async ValueTask InvokeSafeVoidAsync( string identifier, params

await module.InvokeVoidAsync( identifier, args );
}
catch ( Exception )
catch ( Exception exc )
{
Debug.WriteLine( $"Exception form InvokeSafeVoidAsync: {exc.Message}" );
}
}
else
Expand Down Expand Up @@ -181,8 +183,9 @@ protected virtual async ValueTask<TValue> InvokeSafeAsync<TValue>( string identi

return await module.InvokeAsync<TValue>( identifier, args );
}
catch ( Exception )
catch ( Exception exc )
{
Debug.WriteLine( $"Exception form InvokeSafeVoidAsync: {exc.Message}" );
return default;
}
}
Expand Down
4 changes: 4 additions & 0 deletions Source/Blazorise/Modules/JSFileModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public JSFileModule( IJSRuntime jsRuntime, IVersionProvider versionProvider, Bla
public virtual ValueTask<IJSStreamReference> ReadDataAsync( ElementReference elementRef, int fileEntryId, CancellationToken cancellationToken = default )
=> InvokeSafeAsync<IJSStreamReference>( "readFileDataStream", elementRef, fileEntryId );

/// <inheritdoc/>
public virtual ValueTask RemoveFileEntry( ElementReference elementRef, int fileEntryId )
=> InvokeSafeVoidAsync( "removeFileEntry", elementRef, fileEntryId );

#endregion

#region Properties
Expand Down
3 changes: 2 additions & 1 deletion Source/Blazorise/Utilities/IO/FileEntry.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#region Using directives
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -12,7 +13,7 @@ namespace Blazorise;
/// </summary>
public class FileEntry : IFileEntry
{
#region members
#region Members

CancellationTokenSource writeToStreamcancellationTokenSource;
CancellationTokenSource openReadStreamcancellationTokenSource;
Expand Down
23 changes: 23 additions & 0 deletions Source/Blazorise/Utilities/IO/IFileEntryOwner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Blazorise.Modules;
using Microsoft.AspNetCore.Components;
#endregion

namespace Blazorise;
Expand All @@ -27,4 +29,25 @@ public interface IFileEntryOwner
/// <param name="cancellationToken">A cancellation token to signal the cancellation of streaming file data.</param>
/// <returns>Returns the stream for the uploaded file entry.</returns>
Stream OpenReadStream( FileEntry fileEntry, CancellationToken cancellationToken = default );

/// <summary>
/// Removes the file entry from js dictionary.
/// </summary>
/// <param name="fileEntryId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
async Task RemoveFileEntry( int fileEntryId, CancellationToken cancellationToken = default )
{
await JSFileModule.RemoveFileEntry( ElementRef, fileEntryId );
}

/// <summary>
/// Element reference.
/// </summary>
ElementReference ElementRef { get; set; }

/// <summary>
/// JS file module.
/// </summary>
IJSFileModule JSFileModule { get; set; }
}
5 changes: 4 additions & 1 deletion Source/Blazorise/wwwroot/fileEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ export function open(element, elementId) {

// Reduce to purely serializable data, plus build an index by ID
function mapElementFilesToFileEntries(element) {
element._blazorFilesById = {};

if (!element._blazorFilesById) {
element._blazorFilesById = {};
}

let fileList = Array.prototype.map.call(element.files, function (file) {
file.id = file.id ?? ++nextFileId;
Expand Down
4 changes: 4 additions & 0 deletions Source/Blazorise/wwwroot/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ function getFileById(element, fileId) {
return file;
}

export function removeFileEntry(element, fileEntryId) {
delete element._blazorFilesById[fileEntryId];
}

function getArrayBufferFromFileAsync(element, fileId) {
var file = getFileById(element, fileId);

Expand Down
28 changes: 21 additions & 7 deletions Source/Extensions/Blazorise.Markdown/Markdown.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,20 +272,34 @@ public Task NotifyCustomButtonClicked( string name, object value )
/// <summary>
/// Notifies the component that file input value has changed. This method is intended for internal framework use only and should not be called directly by user code.
/// </summary>
/// <param name="file">Changed file.</param>
/// <param name="fileEntries">Array of changed file</param>
/// <returns>A task that represents the asynchronous operation.</returns>
[JSInvokable]
public async Task NotifyImageUpload( FileEntry file )
public async Task NotifyImageUpload( FileEntry[] fileEntries )
{
file.FileUploadEndedCallback = new();
foreach ( var file in fileEntries )
{
file.FileUploadEndedCallback = new();

// So that method invocations on the file can be dispatched back here
file.Owner = (IFileEntryOwner)(object)this;
// So that method invocations on the file can be dispatched back here
file.Owner = this;
}

if ( ImageUploadChanged is not null )
await ImageUploadChanged.Invoke( new( file ) );
{
await ImageUploadChanged.Invoke( new( fileEntries ) );
}

foreach ( var file in fileEntries )
{
file.FileUploadEndedCallback.SetResult();
}

foreach ( var file in fileEntries )
{
await file.Owner.RemoveFileEntry( file.Id );
}

file.FileUploadEndedCallback.SetResult();
await InvokeAsync( StateHasChanged );
}

Expand Down
28 changes: 24 additions & 4 deletions Source/Extensions/Blazorise.Markdown/wwwroot/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export function initialize(dotNetObjectRef, element, elementId, options) {
onError: (e) => { }
};

let fileEntriesToNotifyBuffer = [];
let notifyUploadTimer = null;

const mdeOptions = {
element: document.getElementById(elementId),
hideIcons: options.hideIcons,
Expand Down Expand Up @@ -80,7 +83,7 @@ export function initialize(dotNetObjectRef, element, elementId, options) {
imageUploadNotifier.onError = onError;

// Reduce to purely serializable data, plus build an index by ID
if (element._blazorFilesById == null) {
if (!element._blazorFilesById) {
element._blazorFilesById = {};
}

Expand All @@ -97,9 +100,26 @@ export function initialize(dotNetObjectRef, element, elementId, options) {
// Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON
Object.defineProperty(fileEntry, 'blob', { value: file });

dotNetObjectRef.invokeMethodAsync('NotifyImageUpload', fileEntry).then(null, function (err) {
throw new Error(err);
});
fileEntriesToNotifyBuffer.push(fileEntry);

// Reset debounce timer: if a new file is added within 100ms, reset the timer
if (notifyUploadTimer) {
clearTimeout(notifyUploadTimer);
}

notifyUploadTimer = setTimeout(() => {
// Send batched files to .NET when no more files arrive within 100ms
dotNetObjectRef.invokeMethodAsync('NotifyImageUpload', fileEntriesToNotifyBuffer)
.then(() => {
fileEntriesToNotifyBuffer = [];
})
.catch(err => {
fileEntriesToNotifyBuffer = [];
throw new Error(err);
});

notifyUploadTimer = null;
}, 100);
},

errorMessages: options.errorMessages,
Expand Down

0 comments on commit ae0cd5d

Please sign in to comment.