I am building a simple app that counts words in files. I chose blazor because I wanted to try it out and because it is relatively easy to keep the interface responsive.
I have a service for doing the actual work. In my UI I am calling the service like this:
private async void InputFileChanged(InputFileChangeEventArgs e)
{
if (Service.Running)
{
return;
}
IReadOnlyList<IBrowserFile> files = e.GetMultipleFiles();
try
{
cancellationTokenSource = new CancellationTokenSource();
wordCounts = await Service.GetWordCounts(files, cancellationTokenSource.Token);
progressPercentage = 100;
}
catch (OperationCanceledException ex)
{
Service.Stop();
wordCounts = null;
progressPercentage = 0;
}
StateHasChanged();
}
Now, you might notice the Service.Stop();
inside the catch
block. That is exactly what I'm not happy about.
Essentially, when cancelling the service process, the service throws an exception, as is common with cancellation tokens. When throwing the exception, I am already calling that Stop()
method. However, the service seems to never actually reach the code blocks containing throw
, even though it is throwing the exception. I checked by using break points, and it never breaks on the throws inside the service.
Any ideas on what I'm doing wrong or what I could improve to not have to call that Stop()
method outside of the service?
FileCounterService:
using Microsoft.AspNetCore.Components.Forms;
using System.ComponentModel;
namespace FileWordCounter.Service
{
public class FileCounterService
{
public bool Running { get; private set; }
public event ProgressChangedEventHandler? ProgressChanged;
private StreamReader? reader = null;
private long totalBytes;
private long progressedBytes;
public async Task<IOrderedEnumerable<KeyValuePair<string, int>>?> GetWordCounts(
IEnumerable<IBrowserFile> files,
CancellationToken cancellationToken)
{
//initialize service
Running = true;
Dictionary<string, int> wordCounts = new Dictionary<string, int>();
//reset progress values
totalBytes = files.Sum(x => x.Size);
progressedBytes = 0;
//loop each file
foreach(IBrowserFile file in files)
{
//maximum file size 1 GiB should cover most of the clients files
long maxFileSize = 1024L * 1024 * 1024 * 1024;
try
{
reader = new StreamReader(file.OpenReadStream(maxFileSize, cancellationToken));
}
catch (OperationCanceledException ex)
{
Stop();
throw ex;
}
string? line = await reader.ReadLineAsync();
while (line != null)
{
if (line == "")
{
line = await reader.ReadLineAsync();
continue;
}
string[] words = line.Split(' ');
foreach (string word in words)
{
if (string.IsNullOrWhiteSpace(word))
{
continue;
}
if (wordCounts.ContainsKey(word))
{
wordCounts[word]++;
}
else
{
wordCounts.Add(word, 1);
}
//cancel execution in case cancellation was requested
if (cancellationToken.IsCancellationRequested)
{
Stop();
throw new OperationCanceledException();
}
}
//since files are ANSI, we can assume 1 byte per character
//assuming windows line endings, so 2 additional characters per line
progressedBytes += line.Length + 2;
//multiply by 100 as float to make sure no comma values are lost
int progressPercentage = (int)(progressedBytes * 100f / totalBytes);
ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(progressPercentage, null));
//cancel execution in case cancellation was requested
if (cancellationToken.IsCancellationRequested)
{
Stop();
throw new OperationCanceledException();
}
line = await reader.ReadLineAsync();
}
reader?.Dispose();
}
var returnValue = wordCounts.OrderByDescending(wordCount => wordCount.Value);
Running = false;
return returnValue;
}
public void Stop()
{
Running = false;
reader?.Dispose();
}
}
}
You are probably trying to achieve your objective the wrong way. Your Stop code is there to reset the state in the case of a cancellation or exception.
I've revamped your code to demonstrate a different approach that uses a manually created Task as the main process control mechanism. Ask a question if there's something you want clarifying in my answer.
You should be able to take this code, drop it into an empty project and run it.
A FileData object to manage and encapsulate the dictionary of words.
Next your FileCounterService. It:
And finally the Demo Page:
Note: I haven't checked all the word conting logic, so there may be a bug or two in there.