Greetings Programs!
RE: AspNetZero - Angular + Core 10.2.
I am uploading an excel file that I loop through the rows and processing them.
I would like to be able to use a progress bar (https://ej2.syncfusion.com/angular/demos/#/bootstrap5/progress-bar/Linear) so that I can display which row is currently being processed.
Here is my controller method for processing the file; within the loop at the end, I would like to pass back the current row value to the angular component.
Any thoughts as to how I can acheive this?
Thanks,
Wg
public async Task BulkPayrollAdjustments(IFormFile file, string notes)
{
var fileName = Path.GetFileName(file.FileName);
if (fileName.IsNullOrWhiteSpace()) return L("File_Empty_Error");
//New instance of ExcelEngine is created
//Equivalent to launching Microsoft Excel with no workbooks open
//Instantiate the spreadsheet creation engine
var excelEngine = new ExcelEngine();
//Instantiate the Excel application object
var application = excelEngine.Excel;
//var xlFile = new FileStream(tempFilePath, FileMode.Open);
var xlFile = file.OpenReadStream();
var workbook = application.Workbooks.Open(xlFile);
var worksheet = workbook.Worksheets.FirstOrDefault();
if (worksheet == null) return "No worksheet found";
var start = worksheet.UsedRange.Row;
var end = worksheet.UsedRange.LastRow;
var col = worksheet.UsedRange.Column;
var bulkUpdates = new List<BulkUpdateDto>();
// Get the rows.
for (var row = start + 1; row <= end; row++)
{
var bulkUpdateDto = new BulkUpdateDto
{
// Create object from row.
FromBaseId = worksheet.Range[row, col].CalculatedValue;
// ...
};
bulkUpdates.Add(bulkUpdateDto);
}
//Close the instance of IWorkbook
workbook.Close();
//Dispose the instance of ExcelEngine
excelEngine.Dispose();
var currentRow = 1;
foreach (var bulkUpdate in bulkUpdates)
{
//*****************************************************************
// I would like to send the current row number value here.
//*****************************************************************
sendCurrentRowNumberToComponent(currentRow);
await _appService.ProcessUpdate(bulkUpdate)
currentRow++;
await CurrentUnitOfWork.SaveChangesAsync();
}
}
8 Answer(s)
-
0
Hi @wizgod
If you want to update the client side data, you can send a signalR request to client to inform it for the executed row number.
-
0
Hi @ismcagdas,
Is there any code I can look at to do this?
Thanks,
Wg
-
0
Hi @wizgod
We don't have any example since it's a specific use case you need, not common requirement. But as @ismcagdas mentioned, you may connect to the server via signalr and send the current progress of the excel file to the client from server. And show a progress bar in client's ui. It is one of the possible solutions.
-
0
Hi @musa.demir, @ismcagdas,
I have initialized my signalr service along with the chat service:
if (this.appSession.application) { SignalRHelper.initSignalR(() => { this._chatSignalrService.init(); this._commonSignalrService.init(); }); }
In my service, this is working and I am receiving the messages from the server:
registerCommonEvents(connection): void { connection.on('sendProgress', (progress) => { console.log('sendProgress', progress); abp.event.trigger('app.progress.received', progress); }); }
However, in my component, this is not working:
ngOnInit(): void { super.ngOnInit(); abp.event.on('abp.progress.received', (progress) => { console.log('progressReceived', progress); this.progress = progress; }); }
Any thoughts?
Thanks,
Wg
-
0
Hi @musa.demir, @ismcagdas,
I found the issue - it was a typo in the event name in the controller - "abp." instead of "app.".
Wg
-
0
Hi @wizgod, I have to do the same thing as your need. It's very kind of you if you could send me your sample code (both server and client side) to look at. Best regards @dzungle
-
0
Hi @dzungle, sorry for the very late reply.
I've broken it down into what I hope will make sense and hope that I haven't forgotten anything.
In app.component.ts and app.module.ts, I import the service:
import { CommonSignalrService } from '@app/shared/common/signalR/common-signalr.service';
In app.component.ts add to constructor:
private _commonSignalrService: CommonSignalrService,
and init with the chat signalr service:
if (this.appSession.application) { SignalRHelper.initSignalR(() => { this._chatSignalrService.init(); this._commonSignalrService.init(); }); }
The common-signalr.service.ts:
import { Injectable, Injector, NgZone } from '@angular/core'; import { AppComponentBase } from '@shared/common/app-component-base'; import { HubConnection } from '@microsoft/signalr'; @Injectable()export class CommonSignalrService extends AppComponentBase { constructor( injector: Injector, public _zone: NgZone ) { super(injector); } commonHub: HubConnection; isCommonConnected = false; configureConnection(connection): void { // Set the common hub this.commonHub = connection; // Reconnect loop let reconnectTime = 5000; let tries = 1; let maxTries = 8; function start() { return new Promise(function (resolve, reject) { if (tries > maxTries) { reject(); } else { connection.start() .then(resolve) .then(() => { reconnectTime = 5000; tries = 1; }) .catch(() => { setTimeout(() => { start().then(resolve); }, reconnectTime); reconnectTime *= 2; tries += 1; }); } }); } // Reconnect if hub disconnects connection.onclose(e => { this.isCommonConnected = false; if (e) { abp.log.debug('Common signalR connection closed with error: ' + e); } else { abp.log.debug('Common signalR disconnected'); } start().then(() => { this.isCommonConnected = true; }); }); // Register to get notifications this.registerCommonEvents(connection); } registerCommonEvents(connection): void { connection.on('sendProgress', (progress) => { abp.event.trigger('app.progress.received', progress); }); } init(): void { this._zone.runOutsideAngular(() => { abp.signalr.connect(); abp.signalr.startConnection(abp.appPath + 'signalr-common', connection => { this.configureConnection(connection); }).then(() => { abp.event.trigger('app.common.connected'); this.isCommonConnected = true; }); }); }}
My ProgressBarDto:
import { Inject, Injectable } from '@angular/core'; @Injectable()export class ProgressBarDto implements IProgressBarDto { message = ''; progressCount = 0; percentComplete = 0; totalCount = 0; data: any; constructor(@Inject(ProgressBarDto) data?: IProgressBarDto) { if (data) { for (let property in data) { if (data.hasOwnProperty(property)) { (<any>this)[property] = (<any>data)[property]; } } this.data = JSON.parse(data['data']); } } static fromJS(data: any): ProgressBarDto { data = typeof data === 'object' ? data : {}; let result = new ProgressBarDto(); result.init(data); return result; } init(data?: any) { if (data) { this.message = data['message']; this.progressCount = data['progressCount']; this.percentComplete = data['percentComplete']; this.totalCount = data['totalCount']; this.data = JSON.parse(data['data']); } } toJSON(data?: any) { data = typeof data === 'object' ? data : {}; data['message'] = this.message; data['progressCount'] = this.progressCount; data['percentComplete'] = this.percentComplete; data['totalCount'] = this.totalCount; data['data'] = JSON.stringify(this.data); return data; }} export interface IProgressBarDto { message: string; progressCount: number; percentComplete: number; totalCount: number; data: any; }
-
0
And I am implementing it like this using Syncfusion's ProgressBar but other controls pretty much use the same parameters:
import { appModuleAnimation } from '@shared/animations/routerTransition'; import { Component, ElementRef, Injector, OnInit, OnChanges, SimpleChanges, ViewChild, ViewEncapsulation, AfterViewInit } from '@angular/core'; import { AppConsts } from '@shared/AppConsts'; import { AppComponentBase } from '@shared/common/app-component-base'; import { FileUpload } from 'primeng/fileupload'; import { HttpHeaders } from '@angular/common/http'; import { ProgressBarDto } from '@app/shared/common/components/progressBar/ProgressBarDto'; import { ProgressBar } from '@syncfusion/ej2-progressbar'; @Component({ selector: 'app-bulkAdjustments', templateUrl: './bulkAdjustments.component.html', styleUrls: ['./bulkAdjustments.component.css'], encapsulation: ViewEncapsulation.None, animations: [appModuleAnimation()] }) export class BulkAdjustmentsComponent extends AppComponentBase implements OnInit, AfterViewInit { notes = ''; uploadUrl: string; uploadedFiles: any[] = []; path: any; errors: string[] = []; isProcessed = false; isDisabled = false; fileSelected = false; headers = new HttpHeaders(); public progress = new ProgressBarDto(); public showProgress = true; public gapWidth = 5; public width = '100%'; public height = '30px'; public trackThickness = 15; public progressThickness = 15; @ViewChild('progressBar', { static: false }) progressBar: ProgressBar; @ViewChild('fileUpload', { static: false }) fileUpload: FileUpload; @ViewChild('messageLabel', { static: false }) messageLabel: ElementRef; @ViewChild('progressCountLabel', { static: false }) progressCountLabel: ElementRef; @ViewChild('totalCountLabel', { static: false }) totalCountLabel: ElementRef; @ViewChild('transactionIdLabel', { static: false }) transactionIdLabel: ElementRef; constructor(injector: Injector) { super(injector); } ngOnInit(): void { super.ngOnInit(); this.notes = ''; this.uploadUrl = AppConsts.remoteServiceBaseUrl + '/Adjustments/ProcessBulkAdjustments'; this.path = { saveUrl: this.uploadUrl }; this.headers.set('Authorization', 'Bearer ' + abp.auth.getToken()); abp.event.on('app.progress.received', (progress) => { this.updateProgress(new ProgressBarDto(progress)); }); } ngAfterViewInit() { setTimeout(() => { this.showProgress = false; }, 1); } updateProgress(progress: ProgressBarDto): void { let p = progress; if (this.progressBar.segmentCount !== p.totalCount) { this.progressBar.segmentCount = p.totalCount; this.totalCountLabel.nativeElement.innerHTML = p.totalCount; this.progressBar.refresh(); } // If the message is different, update the label. if (this.messageLabel.nativeElement.innerHTML !== p.message) { this.messageLabel.nativeElement.innerHTML = p.message; } this.progressBar.value = p.percentComplete; this.progressCountLabel.nativeElement.innerHTML = p.progressCount; // Grab the value from the custom data. this.transactionIdLabel.nativeElement.innerHTML = p.data?.transactionId; this.progress = p; // Initially, the divs will not refresh until the area is clicked on. document.getElementById('progressBarParent').click(); } reloadPage() { location.reload(); } processBulkAdjustments() { this.progress = new ProgressBarDto(); this.showProgress = true; this.isProcessed = false; this.fileUpload.upload(); } onSelect(event: any) { this.fileSelected = event.currentFiles.length > 0; } onUpload(event: any): void { this.isProcessed = true; this.fileSelected = false; this.notes = ''; this.errors = event.originalEvent.body.result; } onBeforeUpload(event: any): void { event.formData.append('notes', this.notes); } }
bulkAdjustments.component.html:
<div [@routerTransition] [busyIf]='isBusy'> <div class='content d-flex flex-column flex-column-fluid'> <sub-header [title]="'Adjustments' | localize" [description]="'BulkAdjustments' | localize"></sub-header> <div [class]='containerClass'> <div class='card card-custom'> <div class='card-body'> <div *ngIf='isProcessed' class='row'> <div class='col-md-12'> <div *ngIf='errors?.length > 0' class='margin-bottom-25'> <div class='alert alert-danger' role='alert'> <div>{{'ErrorsDuringProcessing' | localize}}</div> </div> <ul> <li *ngFor='let error of errors'><div [innerHTML]='convertLineBreaks(error)'></div></li> </ul> <hr/> </div> <div *ngIf='!errors || errors.length === 0' class='alert alert-success' role='alert'> {{"ProcessedSuccessfully" | localize}} </div> </div> </div> <div class='row'> <div class='col-md-4 padding-bottom-10'> <div class='font-small padding-left-20'>{{'SelectExcelFile' | localize}}</div> <p-fileUpload #fileUpload mode='advanced' [url]='uploadUrl' [showUploadButton]='false' [headers]='headers' (onSelect)='onSelect($event)' (onUpload)='onUpload($event)' (onBeforeUpload)='onBeforeUpload($event)'> </p-fileUpload> </div> </div> <div class='row'> <div class='col-md-12 padding-top-20'> <h5>{{'Notes' | localize}}:</h5> <input type='text' id='notes' name='notes' [(ngModel)]='notes' placeholder='Required' autocomplete='off' class='form-control' /> </div> </div> <div class='row'> <div class='col-md-12 padding-top-20'> <button class='btn btn-info' *ngIf='!isDisabled && notes' (click)='processBulkAdjustments()' [buttonBusy]='isBusy' [busyText]="l('SavingWithThreeDot')"> <i class='fa fa-upload'></i> <span>{{"Process" | localize}}</span> </button> <div><br /><span [innerHTML]="l('BulkAdjustments_ExcelFileRequirement')"></span></div> </div> </div> </div> </div> </div> </div> </div> <div *ngIf='showProgress && !isProcessed' id="overlay"></div> <div *ngIf='showProgress && !isProcessed' id='progressBarParent' class='progressBarParent'> <div class='progressBarContainer'> <div class='progressBar'> <ejs-progressbar #progressBar type='Linear' cornerRadius='Square' [width]='width' [height]='height' [gapWidth]='gapWidth' [trackThickness]='trackThickness' [progressThickness]='progressThickness' [showProgressValue]='true'> </ejs-progressbar> <div [hidden]='progress.progressCount !== 0' class='font-larger'> {{'LoadWithThreeDot' | localize}} </div> <div [hidden]='progress.progressCount === 0'> <div class='font-larger'> <label #messageLabel></label> <label #progressCountLabel></label> {{l('of').toLowerCase()}} <label #totalCountLabel></label> </div> <div class='padding-top-5 font-larger'> {{'TransactionId' | localize}}: <label #transactionIdLabel></label> </div> </div> <div class='padding-top-10'> <button class='btn btn-danger' (click)='reloadPage()'><i class='fas fa-times'></i>{{'Cancel' | localize}}</button> </div> </div> </div> </div>
In my AdjustmentsController I have the following (I've removed some code for brevity):
namespace MyApp.Web.Controllers { public class AdjustmentsController : MyAppControllerBase { private readonly ISignalRCommon _progressBar; private readonly IAppFolders _appFolders; public AdjustmentsController( ISignalRCommon progressBar, IAppFolders appFolders) { _progressBar = progressBar; _appFolders = appFolders; } [HttpPost] public virtual async Task> ProcessBulkAdjustments() { var files = Request.Form.Files; var notes = Request.Form["notes"]; var errors = new List(); if (notes.IsNullOrEmpty()) { errors.Add("Missing notes."); return errors; } if (files == null) { errors.Add("No files found to process."); return errors; } foreach (var file in files.Where(q => q != null && q.Length > 0)) { var error = await BulkAdjustments(file, notes); if (!error.IsNullOrWhiteSpace()) errors.Add($"{file.FileName}\r\n{error}"); } return errors; } public async Task BulkAdjustments(IFormFile file, string notes) { var fileName = Path.GetFileName(file.FileName); if (fileName.IsNullOrWhiteSpace()) return L("File_Empty_Error"); //New instance of ExcelEngine is created //Equivalent to launching Microsoft Excel with no workbooks open //Instantiate the spreadsheet creation engine var excelEngine = new ExcelEngine(); //Instantiate the Excel application object var application = excelEngine.Excel; //var xlFile = new FileStream(tempFilePath, FileMode.Open); var xlFile = file.OpenReadStream(); var workbook = application.Workbooks.Open(xlFile); var worksheet = workbook.Worksheets.FirstOrDefault(); if (worksheet == null) return "No worksheet found"; var start = worksheet.UsedRange.Row; var end = worksheet.UsedRange.LastRow; var col = worksheet.UsedRange.Column; var bulkUpdates = new List<SomeDataDto>(); for (var row = start + 1; row <= end; row++) { if (!int.TryParse(worksheet.Range[row, col].CalculatedValue, out var transactionId)) continue; if (!int.TryParse(worksheet.Range[row, col + 1].CalculatedValue, out var userId)) continue; var someDataDto = new SomeDataDto { TransactionId = transactionId, UserId = userId }; bulkUpdates.Add(someDataDto); await _progressBar.SendProgress(L("ParsedRow"), row, end, someDataDto); } var count = 0; var totalCount = bulkUpdates.Count; var errors = string.Empty; foreach (var bulkUpdate in bulkUpdates) { count++; await _progressBar.SendProgress(L("PreProcessingBulkUpdate"), count, totalCount, bulkUpdate); // Do something. } // Group the list by the UserId. var groupedBulkUpdates = bulkUpdates .Where(q => q.UserId.HasValue) .GroupBy(g => g.UserId) .Select(s => s.ToList()) .ToList(); count = 0; foreach (var bulkUpdateGroup in groupedBulkUpdates) { foreach (var bulkUpdate in bulkUpdateGroup) { count++; await _progressBar.SendProgress(L("ProcessingBulkUpdate"), count, totalCount, bulkUpdate); // Do something. // errors = await doSomething(); } await CurrentUnitOfWork.SaveChangesAsync(); } await _progressBar.SendProgress(L("ProcessingComplete"), count, totalCount); return errors; } } }
In MyApp.Core/Common, I have ISignalRCommon.cs:
namespace MyApp.Common { public interface ISignalRCommon { Task SendProgress(string message, int progressCount, int totalCount, object data = null); } }
In MyApp.Web.Core/Common/SignalR, I have CommonHub.cs and SignalRCommon.cs:
namespace MyApp.Web.Common.SignalR { public class CommonHub : Hub { } }
namespace MyApp.Web.Common.SignalR { public class SignalRCommon : ISignalRCommon, ITransientDependency { private readonly IHubContext _commonHub; public SignalRCommon(IHubContext commonHub) { _commonHub = commonHub; } public async Task SendProgress(string message, int progressCount, int totalCount, object data = null) { var progress = new ProgressDto { Message = message, ProgressCount = progressCount, PercentComplete = (progressCount * 100) / totalCount, TotalCount = totalCount, Data = Newtonsoft.Json.JsonConvert.SerializeObject(data) }; await _commonHub.Clients.All.SendAsync("sendProgress", progress); return progress; } } }
Finally, ProgressDto:
namespace MyApp.Common.Dto { public class ProgressDto { public string Message { get; set; } = string.Empty; public decimal ProgressCount { get; set; } = 0; public decimal PercentComplete { get; set; } = 0; public decimal TotalCount { get; set; } = 0; public string Data { get; set; } = string.Empty; } }