Open Closed

Using progress bar with angular + core #10667


1
wizgod created

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
    ismcagdas created
    Support Team

    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
    wizgod created

    Hi @ismcagdas,

    Is there any code I can look at to do this?

    Thanks,

    Wg

  • 0
    musa.demir created
    Support Team

    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
    wizgod created

    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
    wizgod created

    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
    dzungle created

    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
    wizgod created

    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
    wizgod created

    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>&nbsp;
                        <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;
        }
    }