SpeechRecognitionEngine in BackgroundWorker - c#

I am trying to write a C# application using Windows Forms and System.Speech to convert a WAV file to text. I've seen plenty of samples online of how to do this, but none that are very robust. I was hoping to write an application that could parse smaller pieces of a large WAV file using BackgroundWorker threads, but I keep getting the following exception in my threads' DoWork function when it calls engine.Recognize():
"No audio input is supplied to this recognizer. Use the method SetInputToDefaultAudioDevice if a microphone is connected to the system, otherwise use SetInputToWaveFile, SetInputToWaveStream or SetInputToAudioStream to perform speech recognition from pre-recorded audio"
Here is the code in my DoWork() function:
SpeechRecognitionEngine engine = new SpeechRecognitionEngine(new System.Globalization.CultureInfo("en-US"));
engine.SetInputToWaveFile(fname);
engine.LoadGrammar(new DictationGrammar());
engine.BabbleTimeout = TimeSpan.FromSeconds(10.0);
engine.EndSilenceTimeout = TimeSpan.FromSeconds(10.0);
engine.EndSilenceTimeoutAmbiguous = TimeSpan.FromSeconds(10.0);
engine.InitialSilenceTimeout = TimeSpan.FromSeconds(10.0);
BackgroundWorker w = (BackgroundWorker)sender;
while (true)
{
RecognitionResult data = engine.Recognize();
if (data == null)
break;
if (w == null) //our thread died from beneath us
break;
if (!w.IsBusy) //our thread died from beneath us
break;
if (w.CancellationPending) //notice to cancel
break;
w.ReportProgress(0, data.Text);
}
I am launching multiple BackgroundWorker threads that run this code. If i use a single thread, I don't see this problem.

You can try this approach. I tested it for Console and Windows Forms application types.
class Program {
public static void Main() {
var r1 = new Recognizer(#"c:\proj\test.wav");
r1.Completed += (sender, e) => Console.WriteLine(r1.Result.Text);
var r2 = new Recognizer(#"c:\proj\test.wav");
r2.Completed += (sender, e) => Console.WriteLine(r2.Result.Text);
Console.ReadLine();
}
}
class Recognizer {
private readonly string _fileName;
private readonly AsyncOperation _operation;
private volatile RecognitionResult _result;
public Recognizer(string fileName) {
_fileName = fileName;
_operation = AsyncOperationManager.CreateOperation(null);
_result = null;
var worker = new Action(Run);
worker.BeginInvoke(delegate(IAsyncResult result) {
worker.EndInvoke(result);
}, null);
}
private void Run() {
try {
SpeechRecognitionEngine engine = new SpeechRecognitionEngine(new System.Globalization.CultureInfo("en-US"));
engine.SetInputToWaveFile(_fileName);
engine.LoadGrammar(new DictationGrammar());
engine.BabbleTimeout = TimeSpan.FromSeconds(10.0);
engine.EndSilenceTimeout = TimeSpan.FromSeconds(10.0);
engine.EndSilenceTimeoutAmbiguous = TimeSpan.FromSeconds(10.0);
engine.InitialSilenceTimeout = TimeSpan.FromSeconds(10.0);
_result = engine.Recognize();
}
finally {
_operation.PostOperationCompleted(delegate {
RaiseCompleted();
}, null);
}
}
public RecognitionResult Result {
get { return _result; }
}
public event EventHandler Completed;
protected virtual void OnCompleted(EventArgs e) {
if (Completed != null)
Completed(this, e);
}
private void RaiseCompleted() {
OnCompleted(EventArgs.Empty);
}
}

Related

How to use Clipboard.Default.SetTextAsync in MAUI with FileSystemWatcher

I have followed https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/data/clipboard?view=net-maui-7.0 in my project, and I understand that:
Access to the clipboard must be done on the main user interface thread. For more information on how to invoke methods on the main user interface thread, see MainThread.
I cannot do Clipboard.Default.SetTextAsync(url) without errors, and I can't understand where I've done wrong.
The URL is generated through the OnCreated() method of FileSystemWatcher.
I understand this is not the UI thread, but I fire an event which I believe should enable UI thread operations
I would like to get this code below fixed so that I can my app in macOS without any issues.
You can also check the code in GitHub by pulling the following two projects:
https://github.com/McoreD/UploaderX/tree/maui
https://github.com/McoreD/HelpersLib.git
My code is as below.
using HelpersLib;
using Microsoft.Extensions.Logging;
using ShareX.HelpersLib;
using ShareX.UploadersLib;
namespace UploaderX;
public partial class MainPage : ContentPage
{
int count = 0;
private FileSystemWatcher _watcher;
private string _watchDir;
private string _destDir;
public delegate void UrlReceivedEventHandler(string url);
public event UrlReceivedEventHandler UrlReceived;
public MainPage()
{
InitializeComponent();
string AppDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "UploaderX");
string AppSettingsDir = Path.Combine(AppDir, "Settings");
App.Settings = ApplicationConfig.Load(Path.Combine(AppSettingsDir, "ApplicationConfig.json"));
App.UploadersConfig = UploadersConfig.Load(Path.Combine(AppSettingsDir, "UploadersConfig.json"));
App.UploadersConfig.SupportDPAPIEncryption = false;
DebugHelper.Init(Path.Combine(AppDir, $"UploaderX-{DateTime.Now.ToString("yyyyMMdd")}-Log.txt"));
_watchDir = Directory.Exists(App.Settings.CustomScreenshotsPath2) ? App.Settings.CustomScreenshotsPath2 : Path.Combine(AppDir, "Watch Folder");
Helpers.CreateDirectoryFromDirectoryPath(_watchDir);
_destDir = _watchDir;
DebugHelper.Logger.WriteLine("Watch Dir: " + _watchDir);
DebugHelper.Logger.WriteLine("Destination Dir: " + _destDir);
_watcher = new FileSystemWatcher();
_watcher.Path = _watchDir;
_watcher.NotifyFilter = NotifyFilters.FileName;
_watcher.Created += OnCreated;
_watcher.EnableRaisingEvents = true;
this.UrlReceived += MainPage_UrlReceived;
}
private async void MainPage_UrlReceived(string url)
{
await Clipboard.Default.SetTextAsync(url);
}
private void OnUrlReceived(string url)
{
UrlReceived?.Invoke(url);
}
private async void OnCounterClicked(object sender, EventArgs e)
{
count++;
if (count == 1)
CounterBtn.Text = $"Clicked {count} time";
else
CounterBtn.Text = $"Clicked {count} times";
await Clipboard.Default.SetTextAsync(CounterBtn.Text);
}
async void OnCreated(object sender, FileSystemEventArgs e)
{
try
{
string fileName = new NameParser(NameParserType.FileName).Parse("%y%mo%d_%ra{10}") + Path.GetExtension(e.FullPath);
string destPath = Path.Combine(Path.Combine(Path.Combine(_destDir, DateTime.Now.ToString("yyyy")), DateTime.Now.ToString("yyyy-MM")), fileName);
FileHelpers.CreateDirectoryFromFilePath(destPath);
if (!Path.GetFileName(e.FullPath).StartsWith("."))
{
int successCount = 0;
long previousSize = -1;
await Helpers.WaitWhileAsync(() =>
{
if (!FileHelpers.IsFileLocked(e.FullPath))
{
long currentSize = FileHelpers.GetFileSize(e.FullPath);
if (currentSize > 0 && currentSize == previousSize)
{
successCount++;
}
previousSize = currentSize;
return successCount < 4;
}
previousSize = -1;
return true;
}, 250, 5000, () =>
{
File.Move(e.FullPath, destPath, overwrite: true);
}, 1000);
WorkerTask task = new WorkerTask(destPath);
UploadResult result = task.UploadFile();
DebugHelper.Logger.WriteLine(result.URL);
OnUrlReceived(result.URL);
}
}
catch (Exception ex)
{
DebugHelper.Logger.WriteLine(ex.Message);
}
}
}
Error once URL is generated:
You can wrap your call to SetTextAsync() with MainThread.BeginInvokeOnMainThread() like so to make sure it's invoked correctly:
private void MainPage_UrlReceived(string url)
{
MainThread.BeginInvokeOnMainThread(() =>
{
Clipboard.Default.SetTextAsync(url);
});
}
There is no need to await the call to SetTextAsync(), either, because you're calling it as a singular operation from within an event handler, which isn't awaitable.
I understand this is not the UI thread, but I fire an event which I believe should enable UI thread operations
Generally, there is no guarantee that event handlers of UI classes are called on the Main Thread.
You can find more information on when and how to invoke methods on the Main Thread in the official documentation: https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/appmodel/main-thread?view=net-maui-7.0

How to implement a wpf launcher with SetForegroundWindow

I'm trying to implement a button command that launches a new WPF application the first time the user clicks the button and then (when the user clicks the button again) sends it to foreground, if it's already running. The whole thing is running on .Net v4.0
What I've tried to do is working fine, as expected, when the launched process is a normal WPF application, but it doesn't play nice if the launched WPF application has a splash screen. The problem is that SetForegroundWindow fails, because I'm unable to retrieve the correct window handle in that specific case. Can you suggest a fix or a work-around? Assume you can modify the source of both the launcher and the launched WPF.
The relevant code from the View Model of the launcher
private void ClaimRptLogic()
{
if (ClaimRptHandle != IntPtr.Zero)
{
ShowWindow(ClaimRptHandle, SW_RESTORE);
LaunchState = SetForegroundWindow(ClaimRptHandle)? "" : "can't set to foreground";
return;
}
Process rpt = new Process();
rpt.StartInfo = new ProcessStartInfo()
{
WorkingDirectory = ConfigurationManager.AppSettings["ClaimRptPath"],
FileName = ConfigurationManager.AppSettings["ClaimRptexe"]
};
rpt.Start();
BackgroundWorker bg = new BackgroundWorker();
bg.DoWork += new DoWorkEventHandler((o, e) => {
rpt.WaitForExit();
});
bg.RunWorkerCompleted += new RunWorkerCompletedEventHandler((o, e) => {
ClaimRptHandle = IntPtr.Zero;
LaunchState = "ClaimRpt closed";
});
bg.RunWorkerAsync();
Thread.Sleep(3000);
ClaimRptHandle = rpt.MainWindowHandle;
}
Assume you can modify the source of both the launcher and the launched
WPF.
Based on this assumption, I could determine the correct handle in the Loaded event of the launched WPF application and send it back to the launcher using a Named Pipe.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var callback = new WindowInteropHelper(this).Handle;
BackgroundWorker bg = new BackgroundWorker();
bg.DoWork += (s, a) =>
{
WritePipe("at loaded evt: " + callback);
};
bg.RunWorkerAsync();
}
private void WritePipe(string line)
{
using (NamedPipeServerStream server =
new NamedPipeServerStream(Environment.UserName, PipeDirection.InOut))
{
server.WaitForConnection();
using (StreamWriter sw = new StreamWriter(server))
{
sw.WriteLine(line);
}
}
}
and read the correct window handle from the same Named Pipe in another background worker of the launcher
bg.RunWorkerAsync();
Thread.Sleep(3000);
if (rpt.HasExited)
{
return;
}
LaunchedHandle = rpt.MainWindowHandle;
BackgroundWorker bgPipe = new BackgroundWorker();
bgPipe.DoWork += new DoWorkEventHandler((o, e) => {
while (!rpt.HasExited)
{
string testHandle = ReadPipe();
if (testHandle.StartsWith("at loaded evt: "))
{
Debug.WriteLine(testHandle);
Debug.WriteLine("CallBack from Launched Process!");
var handle = testHandle.Replace("at loaded evt: ","");
LaunchedHandle = new IntPtr(int.Parse(handle));
return;
}
LaunchedHandle = rpt.MainWindowHandle;
Thread.Sleep(500);
}
Debug.WriteLine("Process exited!");
});
bgPipe.RunWorkerAsync();
CanLaunchCmd = true;
with
private string ReadPipe()
{
string line = "";
using (NamedPipeClientStream client =
new NamedPipeClientStream(".", Environment.UserName, PipeDirection.InOut))
{
client.Connect();
using (StreamReader sr = new StreamReader(client))
{
line = sr.ReadLine();
}
return line;
}
}
Of course, I'm open to different ideas.
Just another option, if you can't modify the launched WPF app, but you know the title caption of its main window, besides the process id, of course.
In that case the background search would be
LaunchedHandle = rpt.MainWindowHandle;
mainWin = rpt.MainWindowHandle;
BackgroundWorker bgTitle = new BackgroundWorker();
bgTitle.DoWork += new DoWorkEventHandler((o, e) => {
while (!rpt.HasExited)
{
LaunchedHandle = MainWindowHandle(rpt);
Thread.Sleep(500);
}
Debug.WriteLine("Process exited!");
});
bgTitle.RunWorkerAsync();
using a filter based on the process id
private IntPtr MainWindowHandle(Process rpt)
{
EnumWindowsProc ewp = new EnumWindowsProc(EvalWindow);
EnumWindows(ewp, new IntPtr(rpt.Id));
return mainWin;
}
and a callback testing the title caption (in this example it's Launched)
private bool EvalWindow(IntPtr hWnd, IntPtr lParam)
{
int procId;
GetWindowThreadProcessId(hWnd, out procId);
if (new IntPtr(procId) != lParam)
{
return true;
}
StringBuilder b = new StringBuilder(50);
GetWindowText(hWnd, b, 50);
string test = b.ToString();
if (test.Equals("Launched"))
{
mainWin = hWnd;
}
return true;
}

How do I report progress to the GUI from a list of Tasks?

I'm currently working on a program that converts a list of files from .ps (PostScript) to .png.
Originally, this was done in a batch file, one file at a time. I am working on code that uses the Ghostscript.NET dll to process these files asynchronously. By splitting these up into tasks, I have cut down the processing time from 30 minutes to about 6 minutes.
I want to be able to show the user some sort of progress on this, so that it doesn't just look like my program is frozen.
I know just enough about threading to frustrate myself, so any suggestions on the best way to do this is greatly appreciated. The code below has a BackgroundWorker implemented to try to show the progress. I have used BGWorker before to show progress, but not on multiple tasks like this. In fact, this is my first time multi-threading without just using BGWorker.
I feel that BGWorker is probably not what I need to be using, but I wanted to try to take a stab at it myself before I asked.
Here is the code that I have so far:
public partial class ProcessStatusForm : Form
{
public string[] testList;
public string wordPath;
public string StatusText;
public GhostscriptVersionInfo _gs_version_info;
public DirectoryInfo dInfo;
public List<Task> tasks;
public float NumberOfTasks;
public bool PS2PNGRunning;
public int ProgressPct;
public float dPercent;
public decimal decPercent;
public ProcessStatusForm(string wordDoc, List<string> runList)
{
InitializeComponent();
this.wordPath = wordDoc;
this.testList = runList.ToArray();
this.StatusText = string.Empty;
this._gs_version_info = GhostscriptVersionInfo.GetLastInstalledVersion(GhostscriptLicense.GPL |
GhostscriptLicense.AFPL, GhostscriptLicense.GPL);
this.dInfo = new DirectoryInfo(SettingsClass.PSFolder);
this.PS2PNGRunning = false;
this.ProgressPct = 0;
this.NumberOfTasks = runList.Count;
}
private void ProcessStatusForm_Shown(object sender, EventArgs e)
{
//Spawn tasks for each of the .ps files in the PS_FILES folder
tasks = new List<Task>(dInfo.GetFiles("*.ps").Length);
//Start the BackgroundWorker
this.PS2PNGRunning = true;
BackgroundWorker.RunWorkerAsync();
foreach (var file in dInfo.GetFiles("*.ps"))
{
//Get fileName to pass fo the ConvertPS2PNG
string inputFile = file.Name;
//Create the Task
var task = Task.Factory.StartNew(() => ConvertPS2PNG(inputFile));
tasks.Add(task);
}
//Wait until all tasks have completed
Task.WaitAll(tasks.ToArray());
PS2PNGRunning = false;
}
private void ConvertPS2PNG(string input)
{
string output = input.Replace(".ps", "_01.png");
input = SettingsClass.PSFolder + input;
output = SettingsClass.PNGFolder + output;
GhostscriptProcessor processor = new GhostscriptProcessor(_gs_version_info, true);
processor.Process(CreateGSArgs(input, output), new ConsoleStdIO(true, true, true));
}
private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
ProgressPct = 0;
while (PS2PNGRunning)
{
Thread.Sleep(1000);
float TasksCompleted = 0;
foreach (var tsk in tasks)
{
if (tsk.Status == TaskStatus.RanToCompletion)
{
TasksCompleted++;
}
}
StatusText = TasksCompleted + " of " + NumberOfTasks + " converted...";
dPercent = TasksCompleted / NumberOfTasks;
dPercent *= 100;
decPercent = (decimal)dPercent;
decPercent = Math.Round(decPercent);
ProgressPct = (int)decPercent;
BackgroundWorker.ReportProgress(ProgressPct);
}
BackgroundWorker.ReportProgress(100);
}
private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.ProgressLabel.Text = this.StatusText;
this.progressBar.Style = ProgressBarStyle.Continuous;
this.progressBar.Value = e.ProgressPercentage;
}
public string[] CreateGSArgs(string inPath, string outPath)
{
List<string> gsArgs = new List<string>();
gsArgs.Add("-dBATCH");
gsArgs.Add("-dNOPAUSE");
gsArgs.Add("-sDEVICE=png16m");
gsArgs.Add("-dQUIET");
gsArgs.Add("-sPAPERSIZE=letter");
gsArgs.Add("-r800");
gsArgs.Add("-sOutputFile=" + outPath);
gsArgs.Add(inPath);
return gsArgs.ToArray();
}
}
When I put breaks in the code of BackgroundWorker_DoWork, everything seems to be coming out right, but when it gets to the BackgroundWorker.ReportProgress(), it never makes it to the BackgroundWorker_ProgressChanged() method.
At the very least, I could live with just having a progressBar.Style as marquee while this is running so that the user can see that the program is working, but reporting the actual progress would be ideal.
As I said before, I haven't done a ton of work with threading, and all of my knowledge on the subject pretty much comes from Google and StackOverflow. If there is a completely different way to do this, I am open to all criticism.
Was the name BackgroundWorker given to the object when you dragged it from the designer screen? If not change your code to use the appropriate name it was given (default should have been backgroundWorker1).
Or...
Try casting the sender object to a BackgroundWorker object in your DoWork method and call ReportProgress() from there.
private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = sender as BackgroundWorker;
if (bw != null)
{
bw.ReportProgress(25);
}
}

Using recorded wave file (NAudio)

This is the code I use to record an audio file:
internal class AudioRecorder
{
public WaveIn waveSource = null;
public WaveFileWriter waveFile = null;
public string RECORDING_PATH;
public AudioRecorder(string fileName)
{
RECORDING_PATH = fileName;
}
public void Start()
{
waveSource = new WaveIn();
waveSource.WaveFormat = new WaveFormat(44100, 1);
waveSource.DeviceNumber = 0;
waveSource.DataAvailable += new EventHandler<WaveInEventArgs>(waveSource_DataAvailable);
waveSource.RecordingStopped += new EventHandler<StoppedEventArgs>(waveSource_RecordingStopped);
waveFile = new WaveFileWriter(RECORDING_PATH, waveSource.WaveFormat);
System.Timers.Timer t = new System.Timers.Timer(30000);
t.Elapsed += new ElapsedEventHandler(Stop);
waveSource.StartRecording();
t.Start();
}
private void Stop(object sender, ElapsedEventArgs args)
{
waveSource.StopRecording();
}
private void waveSource_DataAvailable(object sender, WaveInEventArgs e)
{
if (waveFile != null)
{
waveFile.Write(e.Buffer, 0, e.BytesRecorded);
waveFile.Flush();
}
}
private void waveSource_RecordingStopped(object sender, StoppedEventArgs e)
{
if (waveSource != null)
{
waveSource.Dispose();
waveSource = null;
}
if (waveFile != null)
{
waveFile.Dispose();
waveFile = null;
}
}
}
In the main method I do:
AudioRecorder r = new AudioRecorder(dialog.FileName);
r.Start();
FileInfo file = new FileInfo(r.RECORDING_PATH);
// Do somehting with the recorded audio //
The problem is that when I do r.Start() the thread does not block and keeps running. So I get a corrupt file error. When I try things like Thread.Sleep to keep the thread waiting until recording finishes, this time the AudioRecorder code does not work well (i.e. recording never finishes).
Any ideas about what should I do to correctly wait the recording to finish so that I can safely use the recorded file ?
If you want to record for 30 seconds exactly, just call StopRecording in the DataAvailable event handler once you have enough data. There is absolutely no need for a complicated threading strategy. I do exactly this in the open source .NET voice recorder application.
Dispose the WaveFileWriter in the RecordingStopped event.
If you absolutely must have a blocking call, then use WaveInEvent, and wait on an event which is set in the RecordingStopped handler, as suggested by Rene. By using WaveInEvent, you remove the need for windows message pump to be operational.
You use a ManualResetEvent to wait for the Stop event to be called, giving other threads a change to proceed.
I've only added the new bits...
internal class AudioRecorder
{
private ManualResetEvent mre = new ManualResetEvent(false);
public void Start()
{
t.Start();
while (!mre.WaitOne(200))
{
// NAudio requires the windows message pump to be operational
// this works but you better raise an event
Application.DoEvents();
}
}
private void Stop(object sender, ElapsedEventArgs args)
{
// better: raise an event from here!
waveSource.StopRecording();
}
private void waveSource_RecordingStopped(object sender, EventArgs e)
{
/// ... your code here
mre.Set(); // signal thread we're done!
}
It is good idea to avoid any multi-threaded code if it is not required and Mark's answer is explaining this perfectly.
However, if you are writing a windows application and the requirement is to record 30 seconds than it is a must not to block a main thread in waiting (for 30 seconds). The new async C# feature can be very handy here. It will allow you to keep code logic straightforward and implement waiting in a very efficient way.
I have modified your code slightly to show how the async feature can be used in this case.
Here is the Record method:
public async Task RecordFixedTime(TimeSpan span)
{
waveSource = new WaveIn {WaveFormat = new WaveFormat(44100, 1), DeviceNumber = 0};
waveSource.DataAvailable += new EventHandler<WaveInEventArgs>(waveSource_DataAvailable);
waveSource.RecordingStopped += new EventHandler<StoppedEventArgs>(waveSource_RecordingStopped);
waveFile = new WaveFileWriter(RECORDING_PATH, waveSource.WaveFormat);
waveSource.StartRecording();
await Task.Delay(span);
waveSource.StopRecording();
}
Example of using Record from click handler of WPF app:
private async void btnRecord_Click(object sender, RoutedEventArgs e)
{
try
{
btnRecord.IsEnabled = false;
var fileName = Path.GetTempFileName() + ".wav";
var recorder = new AudioRecorder(fileName);
await recorder.RecordFixedTime(TimeSpan.FromSeconds(5));
Process.Start(fileName);
}
finally
{
btnRecord.IsEnabled = true;
}
}
However, you have to watch out for timing here. Task.Delay does not guarantee that it will continue execution after the exact specified time span. You might get records slightly longer than is required.

Async/await for long-running API methods with progress/cancelation

Edit
I suppose the proper way of forcing await to invoke the worker asynchronously is with a Task.Run, like this:
await Task.Run(() => builder.Build(dlg.FileName, cts.Token, new Progress(ReportProgress)));
Got some light from http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx.
this should be easy but I'm new to async/await so bear with me. I am building a class library exposing an API with some long-running operations. In the past, I used a BackgroundWorker to deal with progress reporting and cancelation, like in this simplified code fragment:
public void DoSomething(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;
// e.Argument is any object as passed by consumer via RunWorkerAsync...
do
{
// ... do something ...
// abort if requested
if (bw.CancellationPending)
{
e.Cancel = true;
break;
} //eif
// notify progress
bw.ReportProgress(nPercent);
}
}
and the client code was like:
BackgroundWorker worker = new BackgroundWorker
{ WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
worker.DoWork += new DoWorkEventHandler(_myWorkerClass.DoSomething);
worker.ProgressChanged += WorkerProgressChanged;
worker.RunWorkerCompleted += WorkerCompleted;
worker.RunWorkerAsync(someparam);
Now I'd like to leverage the new async pattern. So, first of all here is how I'd write a simple long-running method in my API; here I'm just reading a file line by line, just to emulate a real-world process where I'll have to convert a file format with some processing:
public async Task DoSomething(string sInputFileName, CancellationToken? cancel, IProgress progress)
{
using (StreamReader reader = new StreamReader(sInputFileName))
{
int nLine = 0;
int nTotalLines = CountLines(sInputFileName);
while ((sLine = reader.ReadLine()) != null)
{
nLine++;
// do something here...
if ((cancel.HasValue) && (cancel.Value.IsCancellationRequested)) break;
if (progress != null) progress.Report(nLine * 100 / nTotalLines);
}
return nLine;
}
}
For the sake of this sample, say this is a method of a DummyWorker class. Now, here is my client code (a WPF test app):
private void ReportProgress(int n)
{
Dispatcher.BeginInvoke((Action)(() => { _progress.Value = n; }));
}
private async void OnDoSomethingClick(object sender, RoutedEventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog { Filter = "Text Files (*.txt)|*.txt" };
if (dlg.ShowDialog() == false) return;
// show the job progress UI...
CancellationTokenSource cts = new CancellationTokenSource();
DummyWorker worker = new DummyWorker();
await builder.Build(dlg.FileName, cts.Token, new Progress(ReportProgress));
// hide the progress UI...
}
The implementation for the IProgress interface comes from http://blog.stephencleary.com/2010/06/reporting-progress-from-tasks.html, so you can refer to that URL. Anyway, in this usage test the UI is effectively blocked and I see no progress. So what would be the full picture for such a scenario, with reference to the consuming code?
As noted on the top of that blog post, the information in that post is outdated. You should use the new IProgress<T> API provided in .NET 4.5.
If you're using blocking I/O, then make your core method blocking:
public void Build(string sInputFileName, CancellationToken cancel, IProgress<int> progress)
{
using (StreamReader reader = new StreamReader(sInputFileName))
{
int nLine = 0;
int nTotalLines = CountLines(sInputFileName);
while ((sLine = reader.ReadLine()) != null)
{
nLine++;
// do something here...
cancel.ThrowIfCancellationRequested();
if (progress != null) progress.Report(nLine * 100 / nTotalLines);
}
return nLine;
}
}
and then wrap it in Task.Run when you call it:
private async void OnDoSomethingClick(object sender, RoutedEventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog { Filter = "Text Files (*.txt)|*.txt" };
if (dlg.ShowDialog() == false) return;
// show the job progress UI...
CancellationTokenSource cts = new CancellationTokenSource();
DummyWorker worker = new DummyWorker();
var progress = new Progress<int>((_, value) => { _progress.Value = value; });
await Task.Run(() => builder.Build(dlg.FileName, cts.Token, progress);
// hide the progress UI...
}
Alternatively, you could rewrite Build to use asynchronous APIs and then just call it directly from the event handler without wrapping it in Task.Run.

Categories