How to combine asynchronous method with synchronous method in C#? - c#

I'm calling from the main method:
public MainPage()
{
Text_to_Speech.changetospeech("Welcome to Nepal!", newmedia).Wait();
mytxtblck.Text="Hello from Nepal!"
}
What I really want to do is Wait until "Welcome to Nepal" is being spoken and then write "Hello" in mytextblck.
I've gone to several threads and worked, but nothing could make it work.
public async static Task changetospeech(string text, MediaElement mediaa)
{
var synth = new SpeechSynthesizer();
var voices = SpeechSynthesizer.AllVoices;
synth.Voice = voices.First(x => x.Gender == VoiceGender.Female );
SpeechSynthesisStream stream = await synth.SynthesizeTextToStreamAsync(text);
MediaElement media = mediaa;
media.SetSource(stream,stream.ContentType);
media.Play();
}

It sounds like what you really want is to trigger the text change when the MediaEnded event is fired.
You could do that within your ChangeToSpeech method, although it'll be a little ugly:
public async static Task ChangeToSpeech(string text, MediaElement media)
{
var synth = new SpeechSynthesizer();
var voices = SpeechSynthesizer.AllVoices;
synth.Voice = voices.First(x => x.Gender == VoiceGender.Female);
SpeechSynthesisStream stream = await synth.SynthesizeTextToStreamAsync(text);
var tcs = new TaskCompletionSource<int>();
RoutedEventHandler handler = delegate { tcs.TrySetResult(10); };
media.MediaEnded += handler;
try
{
media.SetSource(stream, stream.ContentType);
media.Play();
// Asynchronously wait for the event to fire
await tcs.Task;
}
finally
{
media.MediaEnded -= handler;
}
}

Related

Problems working with async Task and Textbox.Text = "Hello"

First of all, sorry because I am so new at C# and I decided to make this question because I have been choked in this for hours.
I have an GUI that works with Google Cloud Speech services and make a Speech-to-Text operation. I share with you the whole method that runs when a button is clicked:
private async Task<object> StreamingMicRecognizeAsync(int seconds)
{
if (NAudio.Wave.WaveIn.DeviceCount < 1)
{
Console.WriteLine("No microphone!");
return -1;
}
GoogleCredential googleCredential;
using (Stream m = new FileStream(#"..\..\credentials.json", FileMode.Open))
googleCredential = GoogleCredential.FromStream(m);
var channel = new Grpc.Core.Channel(SpeechClient.DefaultEndpoint.Host,
googleCredential.ToChannelCredentials());
var speech = SpeechClient.Create(channel);
var streamingCall = speech.StreamingRecognize();
// Write the initial request with the config.
await streamingCall.WriteAsync(
new StreamingRecognizeRequest()
{
StreamingConfig = new StreamingRecognitionConfig()
{
Config = new RecognitionConfig()
{
Encoding =
RecognitionConfig.Types.AudioEncoding.Linear16,
SampleRateHertz = 48000,
LanguageCode = "es-ES",
},
InterimResults = true,
}
});
// Read from the microphone and stream to API.
object writeLock = new object();
bool writeMore = true;
var waveIn = new NAudio.Wave.WaveInEvent();
waveIn.DeviceNumber = 0;
waveIn.WaveFormat = new NAudio.Wave.WaveFormat(48000, 1);
waveIn.DataAvailable +=
(object sender, NAudio.Wave.WaveInEventArgs args) =>
{
lock (writeLock)
{
if (!writeMore) return;
streamingCall.WriteAsync(
new StreamingRecognizeRequest()
{
AudioContent = Google.Protobuf.ByteString
.CopyFrom(args.Buffer, 0, args.BytesRecorded)
}).Wait();
}
};
// Print responses as they arrive.
Task printResponses = Task.Run(async () =>
{
while (await streamingCall.ResponseStream.MoveNext(default(CancellationToken)))
{
foreach (var result in streamingCall.ResponseStream
.Current.Results)
{
foreach (var alternative in result.Alternatives)
{
Console.WriteLine(alternative.Transcript);
//Textbox1.Text = alternative.Transcript;
}
}
}
});
waveIn.StartRecording();
Console.WriteLine("Speak now.");
Result_Tone.Text = "Speak now:\n\n";
await Task.Delay(TimeSpan.FromSeconds(seconds));
// Stop recording and shut down.
waveIn.StopRecording();
lock (writeLock) writeMore = false;
await streamingCall.WriteCompleteAsync();
await printResponses;
return 0;
}
My problem is that I want to update the content of the Textbox1control but it doesn´t work. It writes perfectly the output into the console with the line Console.WriteLine(alternative.Transcript); but not into my textbox.
If someone could help I would appreciate so much his help.
The problem is that you're using Task.Run, which means your code will be running on a thread-pool thread.
Instead of calling Task.Run(), just move that code into a separate async method:
async Task DisplayResponses(IAsyncEnumerator<StreamingRecognizeResponse> responses)
{
while (await responses.MoveNext(default(CancellationToken)))
{
foreach (var result in responses.Current.Results)
{
foreach (var alternative in result.Alternatives)
{
Textbox1.Text = alternative.Transcript;
}
}
}
}
Then call that method directly (without Task.Run) from code that's already on the UI thread (e.g. an event handler).
The async machinery will make sure that after the await expression, you're back on the UI thread (the same synchronization context). So the assignment to the Text property will occur on the UI thread, and all should be well.
For example:
// This would be registered as the event handler for a button
void HandleButtonClick(object sender, EventArgs e)
{
var stream = client.StreamingRecognize();
// Send the initial config request
await stream.WriteAsync(...);
// Presumably you want to send audio data...
StartSendingAudioData(stream);
await DisplayResponses(stream.ResponseStream);
}
Tasks run on seperate threads, so you must Invoke an action that will be performed on the control's thread
Textbox1.Invoke(new Action(() =>
{
Textbox1.Text= "";
}));
Edit: For WPF, I believe the equivalent is
Textbox1.Dispatcher.Invoke(new Action(() =>
{
Textbox1.Text= "";
}));
have you tried using Dispatcher.InvokeASync()?
await Dispatcher.InvokeAsync(() => {while (await streamingCall.ResponseStream.MoveNext(default(CancellationToken)))
{
foreach (var result in streamingCall.ResponseStream
.Current.Results)
{
foreach (var alternative in result.Alternatives)
{
Textbox1.Text = alternative.Transcript;
}
}
}});

Xamarin Forms Play a sound Asynchronously

I can successfully play sounds using Xamarin forms (Android and iOS) however I also need to achieve the following:
I need to await so that if multiple sounds are 'played', one will complete before the next.
I need to return a boolean to indicate whether operation was a success.
Here is my current simplified code (for the iOS platform):
public Task<bool> PlayAudioTask(string fileName)
{
var tcs = new TaskCompletionSource<bool>();
string filePath = NSBundle.MainBundle.PathForResource(
Path.GetFileNameWithoutExtension(fileName), Path.GetExtension(fileName));
var url = NSUrl.FromString(filePath);
var _player = AVAudioPlayer.FromUrl(url);
_player.FinishedPlaying += (object sender, AVStatusEventArgs e) =>
{
_player = null;
tcs.SetResult(true);
};
_player.Play();
return tcs.Task;
}
To test the method, I have tried calling it like so:
var res1 = await _audioService.PlayAudioTask("file1");
var res2 = await _audioService.PlayAudioTask("file2");
var res3 = await _audioService.PlayAudioTask("file3");
I had hoped to hear the audio for file1, then file2, then file3. However I only hear file 1 and the code doesn't seem to reach the second await.
Thankyou
I think your issue here is that the AVAudioPlayer _player was being cleared out before it was finished. If you were to add debugging to your FinsihedPlaying, you'll notice that you never hit that point.
Try these changes out, I made a private AVAudioPlayer to sit outside of the Task
(I used the following guide as a reference https://developer.xamarin.com/recipes/ios/media/sound/avaudioplayer/)
public async void play()
{
System.Diagnostics.Debug.WriteLine("Play 1");
await PlayAudioTask("wave2.wav");
System.Diagnostics.Debug.WriteLine("Play 2");
await PlayAudioTask("wave2.wav");
System.Diagnostics.Debug.WriteLine("Play 3");
await PlayAudioTask("wave2.wav");
}
private AVAudioPlayer player; // Leave the player outside the Task
public Task<bool> PlayAudioTask(string fileName)
{
var tcs = new TaskCompletionSource<bool>();
// Any existing sound playing?
if (player != null)
{
//Stop and dispose of any sound
player.Stop();
player.Dispose();
}
string filePath = NSBundle.MainBundle.PathForResource(
Path.GetFileNameWithoutExtension(fileName), Path.GetExtension(fileName));
var url = NSUrl.FromString(filePath);
player = AVAudioPlayer.FromUrl(url);
player.FinishedPlaying += (object sender, AVStatusEventArgs e) =>
{
System.Diagnostics.Debug.WriteLine("DONE PLAYING");
player = null;
tcs.SetResult(true);
};
player.NumberOfLoops = 0;
System.Diagnostics.Debug.WriteLine("Start Playing");
player.Play();
return tcs.Task;
}

Windows Phone 8.1 Media Elements

I have currently 2 buttons, Audi TT RS and Audi R8
when the user clicks on button TT RS it plays a mp3, when user clicks on button R8 it opens a different mp3 file.
This works, however... if the user clicks on one of them, and then clicks on the other. both mp3 files will open and it will sound terrible.
those are my functions :
async void Audir8_Click(object sender, RoutedEventArgs e)
{
MediaElement mediaplayer = new MediaElement();
if (mediaplayer.CurrentState == MediaElementState.Playing)
{
mediaplayer.Stop();
}
// get folder app is installed to
var installFolder = Windows.ApplicationModel.Package.Current.InstalledLocation;
// get folders mp3 files are installed to.
var resourcesFolder = await installFolder.GetFolderAsync("Resources");
var mp3FilesFolder = await resourcesFolder.GetFolderAsync("mp3Files");
// open the mp3 file async
var audioFile = await mp3FilesFolder.GetFileAsync("audir8.mp3");
// var stream = await audioFile.OpenAsync(Windows.Storage.FileAccessMode.Read);
using (var stream = await audioFile.OpenAsync(Windows.Storage.FileAccessMode.Read))
{
// play dat funky music
// MediaElement mediaplayer = new MediaElement();
mediaplayer.SetSource(stream, audioFile.ContentType);
var tcs = new TaskCompletionSource<bool>();
mediaplayer.CurrentStateChanged += (_, __) =>
{
if (mediaplayer.CurrentState != MediaElementState.Opening &&
mediaplayer.CurrentState != MediaElementState.Playing &&
mediaplayer.CurrentState != MediaElementState.Buffering)
// mediaplayer.CurrentState != MediaElementState.AcquiringLicense)
{
// Any other state should mean we're done playing
tcs.TrySetResult(true);
}
};
mediaplayer.Play();
await tcs.Task; // Asynchronously wait for media to finish
}
}
and the other one, actually the same..
async void Audittrs_Click(object sender, RoutedEventArgs e)
{
MediaElement mediaplayer = new MediaElement();
if (mediaplayer.CurrentState == MediaElementState.Playing)
{
mediaplayer.Stop();
}
// get folder app is installed to
var installFolder = Windows.ApplicationModel.Package.Current.InstalledLocation;
// get folders mp3 files are installed to.
var resourcesFolder = await installFolder.GetFolderAsync("Resources");
var mp3FilesFolder = await resourcesFolder.GetFolderAsync("mp3Files");
// open the mp3 file async
var audioFile = await mp3FilesFolder.GetFileAsync("ttrs.mp3");
// var stream = await audioFile.OpenAsync(Windows.Storage.FileAccessMode.Read);
using (var stream = await audioFile.OpenAsync(Windows.Storage.FileAccessMode.Read))
{
// play dat funky music
// MediaElement mediaplayer = new MediaElement();
mediaplayer.SetSource(stream, audioFile.ContentType);
var tcs = new TaskCompletionSource<bool>();
mediaplayer.CurrentStateChanged += (_, __) =>
{
if (mediaplayer.CurrentState != MediaElementState.Opening &&
mediaplayer.CurrentState != MediaElementState.Playing &&
mediaplayer.CurrentState != MediaElementState.Buffering)
// mediaplayer.CurrentState != MediaElementState.AcquiringLicense)
{
// Any other state should mean we're done playing
tcs.TrySetResult(true);
}
};
mediaplayer.Play();
await tcs.Task; // Asynchronously wait for media to finish
}
}
I thought this should not happen since I am using
MediaElement mediaplayer = new MediaElement();
if (mediaplayer.CurrentState == MediaElementState.Playing)
{
mediaplayer.Stop();
}
When you set up your mediaplayer local variable, you're creating a new mediaplayer, not re-using the one you've previously created. If you re-use the previously-created one (say by making it a field plus property in your class), then the .Stop() call should stop the previous sound.
If you do this, you'll have to change the first line of each of your ..._Click methods to use the persistent MediaElement.
For example, in your class you can do something like this:
private MediaElement _mediaplayer;
public MediaElement Mediaplayer
{
get{
if( _mediaplayer == null )
{
_mediaplayer = new MediaElement();
}
return _mediaplayer;
}
}
Then in your ..._Click methods, swap out this:
MediaElement mediaplayer = new MediaElement();
... for this:
MediaElement mediaplayer = this.Mediaplayer;
The getter should do the rest, making sure that you're always using the same MediaElement for the various sounds you play, allowing you to stop previously-playing sounds as you desire.

Get ReadyState from WebBrowser control without DoEvents

This has been awnsered many times here and at other sites and its working, but I would like ideas to other ways to:
get the ReadyState = Complete after using a navigate or post, without using DoEvents because of all of its cons.
I would also note that using the DocumentComplete event woud not help here as I wont be navigating on only one page, but one after another like this.
wb.navigate("www.microsoft.com")
//dont use DoEvents loop here
wb.Document.Body.SetAttribute(textbox1, "login")
//dont use DoEvents loop here
if (wb.documenttext.contais("text"))
//do something
The way it is today its working by using DoEvents. I would like to know if anyone have a proper way to wait the async call of the browser methods to only then proceed with the rest of the logic. Just for the sake of it.
Thanks in advance.
Below is a basic WinForms app code, illustrating how to wait for the DocumentCompleted event asynchronously, using async/await. It navigates to multiple pages, one after another. Everything is taking place on the main UI thread.
Instead of calling this.webBrowser.Navigate(url), it might be simulating a form button click, to trigger a POST-style navigation.
The webBrowser.IsBusy async loop logic is optional, its purpose is to account (non-deterministically) for the page's dynamic AJAX code which may take place after window.onload event.
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WebBrowserApp
{
public partial class MainForm : Form
{
WebBrowser webBrowser;
public MainForm()
{
InitializeComponent();
// create a WebBrowser
this.webBrowser = new WebBrowser();
this.webBrowser.Dock = DockStyle.Fill;
this.Controls.Add(this.webBrowser);
this.Load += MainForm_Load;
}
// Form Load event handler
async void MainForm_Load(object sender, EventArgs e)
{
// cancel the whole operation in 30 sec
var cts = new CancellationTokenSource(30000);
var urls = new String[] {
"http://www.example.com",
"http://www.gnu.org",
"http://www.debian.org" };
await NavigateInLoopAsync(urls, cts.Token);
}
// navigate to each URL in a loop
async Task NavigateInLoopAsync(string[] urls, CancellationToken ct)
{
foreach (var url in urls)
{
ct.ThrowIfCancellationRequested();
var html = await NavigateAsync(ct, () =>
this.webBrowser.Navigate(url));
Debug.Print("url: {0}, html: \n{1}", url, html);
}
}
// asynchronous navigation
async Task<string> NavigateAsync(CancellationToken ct, Action startNavigation)
{
var onloadTcs = new TaskCompletionSource<bool>();
EventHandler onloadEventHandler = null;
WebBrowserDocumentCompletedEventHandler documentCompletedHandler = delegate
{
// DocumentCompleted may be called several time for the same page,
// if the page has frames
if (onloadEventHandler != null)
return;
// so, observe DOM onload event to make sure the document is fully loaded
onloadEventHandler = (s, e) =>
onloadTcs.TrySetResult(true);
this.webBrowser.Document.Window.AttachEventHandler("onload", onloadEventHandler);
};
this.webBrowser.DocumentCompleted += documentCompletedHandler;
try
{
using (ct.Register(() => onloadTcs.TrySetCanceled(), useSynchronizationContext: true))
{
startNavigation();
// wait for DOM onload event, throw if cancelled
await onloadTcs.Task;
}
}
finally
{
this.webBrowser.DocumentCompleted -= documentCompletedHandler;
if (onloadEventHandler != null)
this.webBrowser.Document.Window.DetachEventHandler("onload", onloadEventHandler);
}
// the page has fully loaded by now
// optional: let the page run its dynamic AJAX code,
// we might add another timeout for this loop
do { await Task.Delay(500, ct); }
while (this.webBrowser.IsBusy);
// return the page's HTML content
return this.webBrowser.Document.GetElementsByTagName("html")[0].OuterHtml;
}
}
}
If you're looking to do something similar from a console app, here is an example of that.
The solution is simple:
// MAKE SURE ReadyState = Complete
while (WebBrowser1.ReadyState.ToString() != "Complete") {
Application.DoEvents();
}
// Move on to your sub-sequence code...
Dirty and quick.. I am a VBA guys, this logic has been working forever, just took me days and found none for C# but I just figured this out myself.
Following is my complete function, the objective is to obtain a segment of info from a webpage:
private int maxReloadAttempt = 3;
private int currentAttempt = 1;
private string GetCarrier(string webAddress)
{
WebBrowser WebBrowser_4MobileCarrier = new WebBrowser();
string innerHtml;
string strStartSearchFor = "subtitle block pull-left\">";
string strEndSearchFor = "<";
try
{
WebBrowser_4MobileCarrier.ScriptErrorsSuppressed = true;
WebBrowser_4MobileCarrier.Navigate(webAddress);
// MAKE SURE ReadyState = Complete
while (WebBrowser_4MobileCarrier.ReadyState.ToString() != "Complete") {
Application.DoEvents();
}
// LOAD HTML
innerHtml = WebBrowser_4MobileCarrier.Document.Body.InnerHtml;
// ATTEMPT (x3) TO EXTRACT CARRIER STRING
while (currentAttempt <= maxReloadAttempt) {
if (innerHtml.IndexOf(strStartSearchFor) >= 0)
{
currentAttempt = 1; // Reset attempt counter
return Sub_String(innerHtml, strStartSearchFor, strEndSearchFor, "0"); // Method: "Sub_String" is my custom function
}
else
{
currentAttempt += 1; // Increment attempt counter
GetCarrier(webAddress); // Recursive method call
} // End if
} // End while
} // End Try
catch //(Exception ex)
{
}
return "Unavailable";
}
Here is a "quick & dirty" solution. It's not 100% foolproof but it doesn't block UI thread and it should be satisfactory to prototype WebBrowser control Automation procedures:
private async void testButton_Click(object sender, EventArgs e)
{
await Task.Factory.StartNew(
() =>
{
stepTheWeb(() => wb.Navigate("www.yahoo.com"));
stepTheWeb(() => wb.Navigate("www.microsoft.com"));
stepTheWeb(() => wb.Navigate("asp.net"));
stepTheWeb(() => wb.Document.InvokeScript("eval", new[] { "$('p').css('background-color','yellow')" }));
bool testFlag = false;
stepTheWeb(() => testFlag = wb.DocumentText.Contains("Get Started"));
if (testFlag) { /* TODO */ }
// ...
}
);
}
private void stepTheWeb(Action task)
{
this.Invoke(new Action(task));
WebBrowserReadyState rs = WebBrowserReadyState.Interactive;
while (rs != WebBrowserReadyState.Complete)
{
this.Invoke(new Action(() => rs = wb.ReadyState));
System.Threading.Thread.Sleep(300);
}
}
Here is a bit more generic version of testButton_Click method:
private async void testButton_Click(object sender, EventArgs e)
{
var actions = new List<Action>()
{
() => wb.Navigate("www.yahoo.com"),
() => wb.Navigate("www.microsoft.com"),
() => wb.Navigate("asp.net"),
() => wb.Document.InvokeScript("eval", new[] { "$('p').css('background-color','yellow')" }),
() => {
bool testFlag = false;
testFlag = wb.DocumentText.Contains("Get Started");
if (testFlag) { /* TODO */ }
}
//...
};
await Task.Factory.StartNew(() => actions.ForEach((x)=> stepTheWeb (x)));
}
[Update]
I have adapted my "quick & dirty" sample by borrowing and sligthly refactoring #Noseratio's NavigateAsync method from this topic.
New code version would automate/execute asynchronously in UI thread context not only navigation operations but also Javascript/AJAX calls - any "lamdas"/one automation step task implementation methods.
All and every code reviews/comments are very welcome. Especially, from #Noseratio. Together, we will make this world better ;)
public enum ActionTypeEnumeration
{
Navigation = 1,
Javascript = 2,
UIThreadDependent = 3,
UNDEFINED = 99
}
public class ActionDescriptor
{
public Action Action { get; set; }
public ActionTypeEnumeration ActionType { get; set; }
}
/// <summary>
/// Executes a set of WebBrowser control's Automation actions
/// </summary>
/// <remarks>
/// Test form shoudl ahve the following controls:
/// webBrowser1 - WebBrowser,
/// testbutton - Button,
/// testCheckBox - CheckBox,
/// totalHtmlLengthTextBox - TextBox
/// </remarks>
private async void testButton_Click(object sender, EventArgs e)
{
try
{
var cts = new CancellationTokenSource(60000);
var actions = new List<ActionDescriptor>()
{
new ActionDescriptor() { Action = ()=> wb.Navigate("www.yahoo.com"), ActionType = ActionTypeEnumeration.Navigation} ,
new ActionDescriptor() { Action = () => wb.Navigate("www.microsoft.com"), ActionType = ActionTypeEnumeration.Navigation} ,
new ActionDescriptor() { Action = () => wb.Navigate("asp.net"), ActionType = ActionTypeEnumeration.Navigation} ,
new ActionDescriptor() { Action = () => wb.Document.InvokeScript("eval", new[] { "$('p').css('background-color','yellow')" }), ActionType = ActionTypeEnumeration.Javascript},
new ActionDescriptor() { Action =
() => {
testCheckBox.Checked = wb.DocumentText.Contains("Get Started");
},
ActionType = ActionTypeEnumeration.UIThreadDependent}
//...
};
foreach (var action in actions)
{
string html = await ExecuteWebBrowserAutomationAction(cts.Token, action.Action, action.ActionType);
// count HTML web page stats - just for fun
int totalLength = 0;
Int32.TryParse(totalHtmlLengthTextBox.Text, out totalLength);
totalLength += !string.IsNullOrWhiteSpace(html) ? html.Length : 0;
totalHtmlLengthTextBox.Text = totalLength.ToString();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error");
}
}
// asynchronous WebBroswer control Automation
async Task<string> ExecuteWebBrowserAutomationAction(
CancellationToken ct,
Action runWebBrowserAutomationAction,
ActionTypeEnumeration actionType = ActionTypeEnumeration.UNDEFINED)
{
var onloadTcs = new TaskCompletionSource<bool>();
EventHandler onloadEventHandler = null;
WebBrowserDocumentCompletedEventHandler documentCompletedHandler = delegate
{
// DocumentCompleted may be called several times for the same page,
// if the page has frames
if (onloadEventHandler != null)
return;
// so, observe DOM onload event to make sure the document is fully loaded
onloadEventHandler = (s, e) =>
onloadTcs.TrySetResult(true);
this.wb.Document.Window.AttachEventHandler("onload", onloadEventHandler);
};
this.wb.DocumentCompleted += documentCompletedHandler;
try
{
using (ct.Register(() => onloadTcs.TrySetCanceled(), useSynchronizationContext: true))
{
runWebBrowserAutomationAction();
if (actionType == ActionTypeEnumeration.Navigation)
{
// wait for DOM onload event, throw if cancelled
await onloadTcs.Task;
}
}
}
finally
{
this.wb.DocumentCompleted -= documentCompletedHandler;
if (onloadEventHandler != null)
this.wb.Document.Window.DetachEventHandler("onload", onloadEventHandler);
}
// the page has fully loaded by now
// optional: let the page run its dynamic AJAX code,
// we might add another timeout for this loop
do { await Task.Delay(500, ct); }
while (this.wb.IsBusy);
// return the page's HTML content
return this.wb.Document.GetElementsByTagName("html")[0].OuterHtml;
}

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