I'm developing a MAUI application that uploads files to Azure Blob Storage. The app has a toggle switch to start/stop uploads, but I'm encountering issues with UI responsiveness during cancellation and exception handling.
// Toggle switch handler
partial void OnIsSyncOnChanged(bool oldValue, bool newValue)
{
if (newValue)
{
_gtecSyncCts?.Cancel();
_gtecSyncCts?.Dispose();
_gtecSyncCts = new CancellationTokenSource();
Task.Run(async () => StartSyncAsync(_gtecSyncCts.Token));
}
else
{
Task.Run(async () =>
{
await StopSync();
ProgressMessage = "Sync stopped by user";
});
}
}
// Main sync method
private async Task StartSyncAsync(CancellationToken token)
{
try
{
ActiveTransfers.Clear();
var filesToUpload = await _fileMetadataManager.GetFilesToUploadAsync(token);
await PrepareUploadsAsync(filesToUpload, new Progress<string>(message => UpdateProgressUI(message, 0)));
await UploadFilesAsync(token, new Progress<string>(message => UpdateProgressUI(message, 0)));
}
catch (OperationCanceledException)
{
Debug.WriteLine("Sync operation was canceled.");
UpdateProgressUI("Sync operation was canceled", 0);
await CancelPendingUploadsAsync();
}
}
// Upload preparation
private async Task PrepareUploadsAsync(List<FileMetadata> filesToUpload, IProgress<string> progress)
{
ActiveTransfers.Clear();
int totalFiles = filesToUpload.Count;
for (int i = 0; i < totalFiles; i++)
{
var file = filesToUpload[i];
var blobClient = _uploadManager.GetBlobClient(
file.destination_folder,
file.filename,
file.FileExtension
);
var uploadViewModel = new FileUploadViewModel(file, blobClient);
uploadViewModel.UploadStateChanged += OnUploadStateChanged;
ActiveTransfers.Add(uploadViewModel);
progress.Report($"Prepared {i + 1}/{totalFiles} files for upload");
}
}
// Concurrent upload handling
private async Task UploadFilesAsync(CancellationToken token, IProgress<string> progress)
{
using var semaphore = new SemaphoreSlim(4);
var tasks = new List<Task>();
var completedUploads = 0;
var totalUploads = ActiveTransfers.Count;
foreach (var vm in ActiveTransfers)
{
await semaphore.WaitAsync(token);
tasks.Add(Task.Run(async () =>
{
try
{
await vm.StartUploadAsync(token);
var completed = Interlocked.Increment(ref completedUploads);
}
finally
{
semaphore.Release();
}
}));
}
await Task.WhenAll(tasks);
}
// Individual file upload
public async Task StartUploadAsync(CancellationToken externalToken)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalToken);
try
{
await UpdateState(UploadState.Checking);
var blobExists = await BlobClient.ExistsAsync(_cts.Token);
if (blobExists)
{
FileMetadata.BlobStoragePath = BlobClient.Uri.ToString();
await UpdateState(UploadState.AlreadyUploaded);
return;
}
await UpdateState(UploadState.Uploading);
var options = new BlobUploadOptions
{
TransferOptions = new StorageTransferOptions
{
MaximumConcurrency = 8,
MaximumTransferSize = 4 * 1024 * 1024,
InitialTransferSize = 4 * 1024 * 1024
},
ProgressHandler = new Progress<long>(ReportProgress)
};
await BlobClient.UploadAsync(FileMetadata.FilePath, options, _cts.Token);
FileMetadata.BlobStoragePath = BlobClient.Uri.ToString();
await UpdateState(UploadState.Completed);
}
catch (OperationCanceledException)
{
await UpdateState(UploadState.Cancelled, "Upload was cancelled by the user");
}
catch (Exception ex)
{
await UpdateState(UploadState.Failed, ex.Message);
}
}
somehow this design manage two work but the UI become really unresponsive when i Turn the switch off throwing the cancellation and the console gets clogged with exceptions: Exception thrown: 'System.Threading.Tasks.TaskCanceledException' in System.Private.CoreLib.dll
secondly this way I have to handle separatley the uploads already started that will end in a OperationCanceledException and the ones waiting to start that are handled by the method CancelPendingUploadsAsync().
I am also pretty sure that I am making abuse of the Task.Run call. my StartSyncAsync is already fired on a separate thread bur Lord AI suggested to use it also in UploadFilesAsync() to lunch each upload Task on a single thread.
Finally I am using the Semaphore Slim because I was not able to make good use of
TransferOptions = new StorageTransferOptions
{
MaximumConcurrency = 8,
in the Azure API. even with this call, if I start 100 uploads they all start concurrently maybe I misunderstood the purpose of MaximumConcurrency proprierty