I have an asynchronous function. It is called only once when the form is displayed for the first time. My function should ping devices asynchronously when I open the program. But it turns out that when you close the child form, another poll is launched. Tell me where the error may be.
Function call (I tried to call it in formLoad):
private async void MainForm_Shown(object sender, EventArgs e)
{
await Start();
}
Function itself:
public async Task Start()
{
while (keyOprosDev)
{
for (int i = 0; i < devicesListActivity.Count; i++)
{
devicesListActivity[i].DevicesList.DevicesTotalPing++;
string ipAdresDevice = devicesListActivity[i].DevicesList.DevicesName;
int portDevice = devicesListActivity[i].DevicesList.DevicesPort;
int activeDevice = devicesListActivity[i].DevicesList.DevicesActiv;
int sendTimeDevice = devicesListActivity[i].DevicesList.DevicesTimeSend;
int respTimeDevice = devicesListActivity[i].DevicesList.DevicesTimeResp;
using (TcpClient client = new TcpClient())
{
if (activeDevice == 1)
{
client.SendTimeout = sendTimeDevice;
client.ReceiveTimeout = respTimeDevice;
var ca = client.ConnectAsync(ipAdresDevice, portDevice);
await Task.WhenAny(ca, Task.Delay(sendTimeDevice));
client.Close();
if (ca.IsFaulted || !ca.IsCompleted)
{
textBox1.AppendText($"{DateTime.Now.ToString()} Server refused connection." + " " + ipAdresDevice + string.Format(" [{0}/{1}]", devicesListActivity[i].DevicesList.DevicesSuccessPing, devicesListActivity[i].DevicesList.DevicesTotalPing) + " " + System.Math.Round((double)(devicesListActivity[i].DevicesList.DevicesSuccessPing / devicesListActivity[i].DevicesList.DevicesTotalPing * 100)) + " %");
textBox1.AppendText("\r\n");
devicesListActivity[i].DevicesList.DevicesImage = 1;
}
else
{
devicesListActivity[i].DevicesList.DevicesSuccessPing++;
textBox1.AppendText($"{DateTime.Now.ToString()} Server available" + " " + ipAdresDevice + string.Format(" [{0}/{1}]", devicesListActivity[i].DevicesList.DevicesSuccessPing, devicesListActivity[i].DevicesList.DevicesTotalPing) + " " + System.Math.Round((double)(devicesListActivity[i].DevicesList.DevicesSuccessPing / devicesListActivity[i].DevicesList.DevicesTotalPing * 100)) + " %");
textBox1.AppendText("\r\n");
devicesListActivity[i].DevicesList.DevicesImage = 2;
}
}
else
{
}
}
await Task.Delay(interval);
}
}
}
And here is the opening of the child form:
try
{
DbViewer dbViewer = new DbViewer();
dbViewer.FormClosed += new FormClosedEventHandler(refr_FormClosed);
dbViewer.ShowDialog();
}
catch (Exception ex)
{
writeEventInDb(ex.Message);
}
This is the event that handles the closure of the child form:
void refr_FormClosed(object sender, FormClosedEventArgs e)
{
try
{
kryptonTreeView1.Nodes[0].Nodes[0].Nodes.Clear();
kryptonTreeView1.Nodes[0].Nodes[1].Nodes.Clear();
loadIpListFromDb();
loadComListFromDb();
kryptonTreeView1.ExpandAll();
}
catch (Exception ex)
{
writeEventInDb(ex.Message);
}
}
You need to pass a cancellation token in. Somewhere outside of this code you need to create a CancellationTokenSource the best place is probably an property of the form:
class MainForm
{
CancellationTokenSource cts;
...
You then initialize this and pass this to Start():
private async void MainForm_Shown(object sender, EventArgs e)
{
cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
await Start(ct);
}
In your start loop you need to monitor for the cancellation token:
Because you're using a delay to timeout the ConnectAsync() you need the Task.Delay() to know when cancellation is requested so you need to pass the token to Task.Delay():
await Task.WhenAny(ca, Task.Delay(sendTimeDevice,ct));
After the TcpClient.Close() you need to test if the cancellation is requested, and stop loops if it is:
if (ct.IsCancellationRequested)
break;
You'll need to perform the same test in the while loop, and also you should perform it immediately before the ConnectAsync(). While the most likely place you will encounter ct.IsCancellationRequested == true will be immediately after the Task.WhenyAny or immediately after the Loop interval there's no point starting a ConnectAsync() if cancellation has been requested.
You should also pass the CancellationToken to the Loop interval, otherwise you could end up waiting interval before your form closes:
// This will throw an OperationCancelled Exception if it is cancelled.
await Task.Delay(interval,ct);
Because you're going to continue anyway and just exit if Cancellation is registered you could avoid writing a separate try/catch that does nothing and await the interval like this, it's almost certainly less efficient, but it's cleaner.
// Leave any exceptions of Task.Delay() unobserved and continue
await Task.WhenAny(Task.Delay(interval,ct));
Finally you need to dispose of the CancellationTokenSource, I guess you would do this in something like a MainForm_Closed() function?
private void MainForm_Closed(object sender, EventArgs e)
{
cts.Dispose();
The only thing left to do is to work out when you want to fire the CancellationRequest, based on what you have said you want to do this when the form close button has been clicked, so:
private void MainForm_Closing(object sender, EventArgs e)
{
cts.Cancel();
That will cause the CancellationToken to transition to a cancelled state and your Start() routine will see that and exit.
In your code there is no single place to check for the CancellationToken being set, the rule of thumb is to check for it before and after any await and in your case you should check for it in both the while and the for loop.
Related
Good day.
I'm having a problem exiting a task with the cancellation token.
My program freezes when I get to the token2.ThrowIfCancellationRequested();.
Following it with the breakpoints is shows that the token2 is cancelled, but the program doesn't revert back to the previous sub routine where I try and catch
try
{
Task.Run(() => SendData_DoWork(_tokenSource3));
}
catch (OperationCanceledException ex)
{
SetText("Communivation error with device");
SetText("");
}
finally
{
token.Dispose();
}
}//comms routine
//send Meter Address to communicate to meter
private void SendData_DoWork(CancellationTokenSource token)
{
var token2 = token.Token;
var _tokenSource4 = new CancellationTokenSource();
try
{
timer.Interval = 10000;
timer.Start();
timer.Elapsed += OnTimerElapsed;
NetworkStream stream = client.GetStream();
SerialConverter serialConverter = new SerialConverter();
Thread.Sleep(1000);
string newtext = null;
newtext = $"/?{address}!\r\n";
SetText("TX: " + newtext);
byte[] newData = stringSend(newtext);
stream.Write(newData, 0, newData.Length);
Thread.Sleep(50);
byte[] message = new byte[23];
int byteRead;
while (true)
{
byteRead = 0;
try
{
byteRead = stream.Read(message, 0, 23);
if (message[0] == (char)0x15)
{
token.Cancel();
}
}
catch
{
token.Cancel();
}
if ((byteRead == 0))
{
token.Cancel();
}
timer.Stop();
timer.Dispose();
ASCIIEncoding encoder = new ASCIIEncoding();
string newresponse = encoder.GetString(serialConverter.convertFromSerial(message));
SetText("RX: " + newresponse);
if (newresponse[0].ToString() == SOH)
{
token.Cancel();
}
if (newresponse != null)
{
/* NEXT SUB ROUTINE*/
}
else { break; }
}//while looop
}//try
catch (Exception ex)
{
token.Cancel();
}
if (token2.IsCancellationRequested)
{
timer.Stop();
timer.Dispose();
token2.ThrowIfCancellationRequested();
}
}//sendData subroutine
You are launching a Task, and ignoring the result; the only time Task.Run would throw is if the task-method is invalid, or enqueuing the operation itself failed. If you want to know how SendData_DoWork ended, you'll need to actually check the result of the task, by capturing the result of Task.Run and awaiting it (preferably asynchronously, although if we're talking async, SendData_DoWork should probably also be async and return a Task).
Your catch/finally will probably be exited long before SendData_DoWork even starts - again: Task.Run just takes the time required to validate and enqueue the operation; not wait for it to happen.
I think you have missunderstood how cancellation tokens are supposed to work. Your work method should take a CancellationToken, not a CancellationTokenSource. And it should call ThrowIfCancellationRequested inside the loop, not after. I would suspect that you would get some issues with multiple cancel calls to the same cancellation token.
Typically you would use a pattern something like like this:
public void MyCancelButtonHandler(...) => cts.Cancel();
public async void MyButtonHandler(...){
try{
cts = new CancellationTokenSource(); // update shared field
await Task.Run(() => MyBackgroundWork(cts.Token));
}
catch(OperationCancelledException){} // Ignore
catch(Exception){} // handle other exceptions
}
private void MyBackgroundWork(CancellationToken cancel){
while(...){
cancel.ThrowIfCancellationRequested();
// Do actual work
}
}
So in my particular case it seems like changing the sub-routines from private async void ... to private async Task fixes the particular issue that I'm having.
I have been attempting to have a re-usable modal progress window (I.e. progressForm.ShowDialog()) to show progress from a running async task, including enabling cancellation.
I have seen some implementations that launch start the async task by hooking the Activated event handler on the form, but I need to start the task first, then show the modal dialog that will show it's progress, and then have the modal dialog close when completed or cancellation is completed (note - I want the form closed when cancellation is completed - signalled to close from the task continuation).
I currently have the following - and although this working - are there issues with this - or could this be done in a better way?
I did read that I need to run this CTRL-F5, without debugging (to avoid the AggregateException stopping the debugger in the continuation - and let it be caught in the try catch as in production code)
ProgressForm.cs
- Form with ProgressBar (progressBar1) and Button (btnCancel)
public partial class ProgressForm : Form
{
public ProgressForm()
{
InitializeComponent();
}
public event Action Cancelled;
private void btnCancel_Click(object sender, EventArgs e)
{
if (Cancelled != null) Cancelled();
}
public void UpdateProgress(int progressInfo)
{
this.progressBar1.Value = progressInfo;
}
}
Services.cs
- Class file containing logic consumed by WinForms app (as well as console app)
public class MyService
{
public async Task<bool> DoSomethingWithResult(
int arg, CancellationToken token, IProgress<int> progress)
{
// Note: arg value would normally be an
// object with meaningful input args (Request)
// un-quote this to test exception occuring.
//throw new Exception("Something bad happened.");
// Procressing would normally be several Async calls, such as ...
// reading a file (e.g. await ReadAsync)
// Then processing it (CPU instensive, await Task.Run),
// and then updating a database (await UpdateAsync)
// Just using Delay here to provide sample,
// using arg as delay, doing that 100 times.
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(arg);
progress.Report(i + 1);
}
// return value would be an object with meaningful results (Response)
return true;
}
}
MainForm.cs
- Form with Button (btnDo).
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private async void btnDo_Click(object sender, EventArgs e)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// Create the ProgressForm, and hook up the cancellation to it.
ProgressForm progressForm = new ProgressForm();
progressForm.Cancelled += () => cts.Cancel();
// Create the progress reporter - and have it update
// the form directly (if form is valid (not disposed))
Action<int> progressHandlerAction = (progressInfo) =>
{
if (!progressForm.IsDisposed) // don't attempt to use disposed form
progressForm.UpdateProgress(progressInfo);
};
Progress<int> progress = new Progress<int>(progressHandlerAction);
// start the task, and continue back on UI thread to close ProgressForm
Task<bool> responseTask
= MyService.DoSomethingWithResultAsync(100, token, progress)
.ContinueWith(p =>
{
if (!progressForm.IsDisposed) // don't attempt to close disposed form
progressForm.Close();
return p.Result;
}, TaskScheduler.FromCurrentSynchronizationContext());
Debug.WriteLine("Before ShowDialog");
// only show progressForm if
if (!progressForm.IsDisposed) // don't attempt to use disposed form
progressForm.ShowDialog();
Debug.WriteLine("After ShowDialog");
bool response = false;
// await for the task to complete, get the response,
// and check for cancellation and exceptions
try
{
response = await responseTask;
MessageBox.Show("Result = " + response.ToString());
}
catch (AggregateException ae)
{
if (ae.InnerException is OperationCanceledException)
Debug.WriteLine("Cancelled");
else
{
StringBuilder sb = new StringBuilder();
foreach (var ie in ae.InnerExceptions)
{
sb.AppendLine(ie.Message);
}
MessageBox.Show(sb.ToString());
}
}
finally
{
// Do I need to double check the form is closed?
if (!progressForm.IsDisposed)
progressForm.Close();
}
}
}
Modified code - using TaskCompletionSource as recommended...
private async void btnDo_Click(object sender, EventArgs e)
{
bool? response = null;
string errorMessage = null;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
using (ProgressForm2 progressForm = new ProgressForm2())
{
progressForm.Cancelled +=
() => cts.Cancel();
var dialogReadyTcs = new TaskCompletionSource<object>();
progressForm.Shown +=
(sX, eX) => dialogReadyTcs.TrySetResult(null);
var dialogTask = Task.Factory.StartNew(
() =>progressForm.ShowDialog(this),
cts.Token,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
await dialogReadyTcs.Task;
Progress<int> progress = new Progress<int>(
(progressInfo) => progressForm.UpdateProgress(progressInfo));
try
{
response = await MyService.DoSomethingWithResultAsync(50, cts.Token, progress);
}
catch (OperationCanceledException) { } // Cancelled
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
progressForm.Close();
}
await dialogTask;
}
}
if (response != null) // Success - have valid response
MessageBox.Show("MainForm: Result = " + response.ToString());
else // Faulted
if (errorMessage != null) MessageBox.Show(errorMessage);
}
I think the biggest issue I have, is that using await (instead of
ContinueWith) means I can't use ShowDialog because both are blocking
calls. If I call ShowDialog first the code is blocked at that point,
and the progress form needs to actually start the async method (which
is what I want to avoid). If I call await
MyService.DoSomethingWithResultAsync first, then this blocks and I
can't then show my progress form.
The ShowDialog is indeed a blocking API in the sense it doesn't return until the dialog has been closed. But it is non-blocking in the sense it continues to pump messages, albeit on a new nested message loop. We can utilize this behavior with async/await and TaskCompletionSource:
private async void btnDo_Click(object sender, EventArgs e)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// Create the ProgressForm, and hook up the cancellation to it.
ProgressForm progressForm = new ProgressForm();
progressForm.Cancelled += () => cts.Cancel();
var dialogReadyTcs = new TaskCompletionSource<object>();
progressForm.Load += (sX, eX) => dialogReadyTcs.TrySetResult(true);
// show the dialog asynchronousy
var dialogTask = Task.Factory.StartNew(
() => progressForm.ShowDialog(),
token,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
// await to make sure the dialog is ready
await dialogReadyTcs.Task;
// continue on a new nested message loop,
// which has been started by progressForm.ShowDialog()
// Create the progress reporter - and have it update
// the form directly (if form is valid (not disposed))
Action<int> progressHandlerAction = (progressInfo) =>
{
if (!progressForm.IsDisposed) // don't attempt to use disposed form
progressForm.UpdateProgress(progressInfo);
};
Progress<int> progress = new Progress<int>(progressHandlerAction);
try
{
// await the worker task
var taskResult = await MyService.DoSomethingWithResultAsync(100, token, progress);
}
catch (Exception ex)
{
while (ex is AggregateException)
ex = ex.InnerException;
if (!(ex is OperationCanceledException))
MessageBox.Show(ex.Message); // report the error
}
if (!progressForm.IsDisposed && progressForm.Visible)
progressForm.Close();
// this make sure showDialog returns and the nested message loop is over
await dialogTask;
}
I have this event handler in asp.net page:
protected void SetDescPoint(object sender, EventArgs e)
{
try
{
ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(foo));
}
catch (Exception)
{ }
}
private void foo(object a)
{
try
{
System.Diagnostics.Debug.WriteLine("Start - " + DateTime.Now.ToString("h:mm:ss tt"));
TimeSpan minutes = TimeSpan.FromMinutes(10);
System.Threading.Thread.Sleep(minutes);
string path = UniquePath();
File.Delete(path);
System.Diagnostics.Debug.WriteLine("Deleted - " + DateTime.Now.ToString("h:mm:ss tt"));
}
catch (Exception ex)
{
Debug.WriteLine("EXCEPTION - " + ex.Message);
}
}
SetDescPoint is event handler and fired in response to client event.As you can see the function foo has Thread.Sleep(10minutes) there is might be situation when event handler fired in time interval less than 10 minutes, so in that situation I need to delete current task(foo()) in pool threads.
Any Idea how can I implement it?
Rewrite your code to use Task.Delay. This has two benefits: you're not holding up a thread anymore (as Task.Delay uses a timer internally), and you can use a cancellation token to cancel the wait:
protected CancellationTokenSource CancellationToken { get; private set; }
protected void SetDescPoint(object sender, EventArgs e)
{
try
{
Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
this.CancellationToken = new CancellationTokenSource();
Task.Run(() => foo(this.CancellationToken.Token), this.CancellationToken.Token);
}
catch (Exception)
{ }
}
private async Task foo(CancellationToken token)
{
try
{
System.Diagnostics.Debug.WriteLine("Start - " + DateTime.Now.ToString("h:mm:ss tt"));
TimeSpan minutes = TimeSpan.FromMinutes(10);
await Task.Delay(minutes, token);
string path = UniquePath();
File.Delete(path);
System.Diagnostics.Debug.WriteLine("Deleted - " + DateTime.Now.ToString("h:mm:ss tt"));
}
catch (Exception ex)
{
Debug.WriteLine("EXCEPTION - " + ex.Message);
}
}
Whenever you want to cancel your task, just call CancellationToken.Cancel()
A rather simplified way of dealing with this would be to keep track of a CancellationTokenSource for each path your create (I'm not sure if there a multiple paths or a simple path, but just in case), and then look it up once the event fires again.
Using this with Task.Delay, which asynchronously yields control in a non-blocking fashion, can achieve what you want:
private ConcurrentDictionary<string, CancellationTokenSource> pathsToTokens =
new ConcurrentDictionary<string, CancellationTokenSource>();
protected async void SetDescPointAsync(object sender, EventArgs e)
{
CancellationTokenSource existingTokenSource;
var path = UniquePath();
if (pathsToTokens.TryGetValue(path, out existingTokenSource))
{
existingTokenSource.Cancel();
}
var cancellationTokenSource = new CancellationTokenSource();
pathsToTokens.AddOrUpdate(path, cancellationTokenSource,
(pathToFile, token) => cancellationTokenSource);
try
{
await Task.Delay(TimeSpan.FromMinutes(10), cancellationTokenSource.Token)
.ConfigureAwait(false);
}
catch (TaskCanceledException tce)
{
// Token was cancelled, do something?
}
Foo(path);
pathsToTokens.TryRemove(path, out cancellationTokenSource);
}
private void Foo(string path)
{
try
{
File.Delete(path);
Debug.WriteLine("Deleted - " + DateTime.Now.ToString("h:mm:ss tt"));
}
catch (Exception ex)
{
Debug.WriteLine("EXCEPTION - " + ex.Message);
}
}
What happens with this code is that for each path you create, you allocate a new CancellationTokenSource. Every time the event is triggered, you check for an existing token. If it is in place, that means the event still hasn't finished, and you want to cancel it. Then, you asynchronously wait the amount of time you need. Note that Task.Delay is wrapped in a try-catch as calling CancellationTokenSource.Cancel will cause it to throw an exception once completed.
I want to show in textbox the time execution of a function that is called using Task.Run, since takes some time
to complete and I'd created a thread for that.
The issue is when I click on begin button is printed inmediately the time in textBox1 and I want to show the
Elapsed time but only just after MyFunction completes the processing or when Cancel button is pressed.
Where should go sw.Stop()?
My current code for Begin and cancel button is:
void Begin_Click(object sender, EventArgs e)
{
Stopwatch sw = Stopwatch.StartNew();
// Pass the token to the cancelable operation.
cts = new CancellationTokenSource();
Task.Run(() => MyFunction(inputstring, cts.Token), cts.Token);
sw.Stop();
textBox1.Text += Math.Round(sw.Elapsed.TotalMilliseconds / 1000, 4) + " sec";
}
void Cancel_Click(object sender, EventArgs e)
{
if (cts != null)
{
cts.Cancel();
cts = null;
}
}
You're not waiting for the MyFunction to complete, You're simply calculating the start time of the Task.Run call. To wait for the MyFunction to complete, you can await the Task returned by Task.Run.
async void Begin_Click(object sender, EventArgs e)//<--Note the async keyword here
{
Stopwatch sw = Stopwatch.StartNew();
// Pass the token to the cancelable operation.
cts = new CancellationTokenSource();
await Task.Run(() => MyFunction(inputstring, cts.Token), cts.Token);//<--Note the await keyword here
sw.Stop();
textBox1.Text += Math.Round(sw.Elapsed.TotalMilliseconds / 1000, 4) + " sec";
}
If you're new to asynchronous programming start by reading here and here
I'm developing a WinPhone 8 App.
On this App there is a Button 'Send SMS'.
When the user clicks on this button two things should happen:
(Method A) Get the geo-coordinate of the current Location (using Geolocator and GetGeopositionAsync).
(Method B) Compose and send an SMS with the geo-coordinate as part of the body.
The Problem: GetGeopositionAsync is an asynchronous method. Before the coordinate is detected (which takes a few seconds) the SMS is sent (of course with no coordinates).
How can I tell Method 2 to wait until the coordinates are available?
OK, here is my code:
When the user presses the button, the coordinates are determined by the first method and the second method sends the SMS which includes the coordinates in its body:
private void btnSendSms_Click(object sender, RoutedEventArgs e)
{
GetCurrentCoordinate(); // Method 1
// -> Gets the coordinates
SendSms(); // Method 2
// Sends the coordinates within the body text
}
The first method GetCurrentCoordinate() looks as follows:
...
private GeoCoordinate MyCoordinate = null;
private ReverseGeocodeQuery MyReverseGeocodeQuery = null;
private double _accuracy = 0.0;
...
private async void GetCurrentCoordinate()
{
Geolocator geolocator = new Geolocator();
geolocator.DesiredAccuracy = PositionAccuracy.High;
try
{
Geoposition currentPosition = await geolocator.GetGeopositionAsync(
TimeSpan.FromMinutes(1),
TimeSpan.FromSeconds(10));
lblLatitude.Text = currentPosition.Coordinate.Latitude.ToString("0.000");
lblLongitude.Text = currentPosition.Coordinate.Longitude.ToString("0.000");
_accuracy = currentPosition.Coordinate.Accuracy;
MyCoordinate = new GeoCoordinate(
currentPosition.Coordinate.Latitude,
currentPosition.Coordinate.Longitude);
if (MyReverseGeocodeQuery == null || !MyReverseGeocodeQuery.IsBusy)
{
MyReverseGeocodeQuery = new ReverseGeocodeQuery();
MyReverseGeocodeQuery.GeoCoordinate = new GeoCoordinate(
MyCoordinate.Latitude,
MyCoordinate.Longitude);
MyReverseGeocodeQuery.QueryCompleted += ReverseGeocodeQuery_QueryCompleted;
MyReverseGeocodeQuery.QueryAsync();
}
}
catch (Exception)
{ // Do something }
}
private void ReverseGeocodeQuery_QueryCompleted(object sender,
QueryCompletedEventArgs<IList<MapLocation>> e)
{
if (e.Error == null)
{
if (e.Result.Count > 0)
{
MapAddress address = e.Result[0].Information.Address;
lblCurrAddress.Text = address.Street + " " + address.HouseNumber + ",\r" +
address.PostalCode + " " + address.City + ",\r" +
address.Country + " (" + address.CountryCode + ")";
}
}
}
}
And the Methode 'SendSms()':
private void SendSms()
{
SmsComposeTask smsComposeTask = new SmsComposeTask();
smsComposeTask.To = "0123456";
smsComposeTask.Body = "Current position: \rLat = " + lblLatitude.Text +
", Long = " + lblLongitude.Text +
"\r" + lblCurrAddress.Text;
// -> The TextBoxes are still empty!
smsComposeTask.Show();
}
The problem is, that all these TextBoxes (lblLatitude, lblLongitude, lblCurrAddress) are still empty when the method SendSms() sets the SmsComposeTask object.
I have to ensure that the TextBoxes are already set BEFORE the method SendSms() starts.
You should almost never mark a method async void unless it's a UI event handler. You're calling an asynchronous method without waiting for it to end. You are basically calling those 2 methods in parallel, so it's clear why the coordinates aren't available.
You need to make GetCurrentCoordinate return an awaitable task and await it, like this:
private async Task GetCurrentCoordinateAsync()
{
//....
}
private async void btnSendSms_Click(object sender, RoutedEventArgs e)
{
await GetCurrentCoordinateAsync();
// You'll get here only after the first method finished asynchronously.
SendSms();
}
This is one of the primary reasons you should avoid async void. void is a very unnatural return type for async methods.
First, make your GetCurrentCoordinate an async Task method instead of async void. Then, you can change your click handler to look like this:
private async void btnSendSms_Click(object sender, RoutedEventArgs e)
{
await GetCurrentCoordinate();
SendSms();
}
Your click handler is async void only because event handlers have to return void. But you should really strive to avoid async void in all other code.
There two things you're doing wrong here:
Using void returning async methods when you need to await on them. This is bad because you can't await on execution of these methods and should only be used when you can't make the method return Task or Task<T>. That's why you're not seeing anything on the text boxes when SendSmsis called.
Mixing UI and non-UI code. You should transfer data between UI and non-UI code to avoid tight coupling between code with different responsibilities. IT also makes it easy to read and debug the code.
ReverseGeocodeQuery does not have an awaitable async API but you can easily make your own:
private async Task<IList<MapLocation>> ReverseGeocodeQueryAsync(GeoCoordinate geoCoordinate)
{
var tcs = new TaskCompletionSource<IList<MapLocation>>();
EventHandler<QueryCompletedEventArgs<IList<MapLocation>>> handler =
(s, e) =>
{
if (e.Cacelled)
{
tcs.TrySetCancelled();
}
else if (e.Error != null)
{
tcs.TrySetException(e.Error);
}
else
{
tcs.TrySetResult(e.Result);
}
};
var query = new ReverseGeocodeQuery{ GeoCoordinate = geoCoordinate };
try
{
query.QueryCompleted += handler;
query.QueryAsync();
return await tcs.Task;
}
finally
{
query.QueryCompleted -= handler;
}
}
This way you'll get full cancellation and error support.
Now let's make the retrieval of the geo coordinate information all in one chunk:
private async Task<Tuple<Geocoordinate, MapLocation>> GetCurrentCoordinateAsync()
{
try
{
var geolocator = new Geolocator
{
DesiredAccuracy = PositionAccuracy.High
};
var currentPosition = await geolocator.GetGeopositionAsync(
TimeSpan.FromMinutes(1),
TimeSpan.FromSeconds(10))
.ConfigureAwait(continueOnCapturedContext: false);
var currentCoordinate = currentPosition.Coordinate;
var mapLocation = await this.ReverseGeocodeQueryAsync(
new GeoCoordinate(
currentCoordinate.Latitude,
currentCoordinate.Longitude));
return Tuple.Create(
currentCoordinate,
mapLocation.FirstOrDefault());
}
catch (Exception)
{
// Do something...
return Tuple.Create(null, null);
}
}
Now the button eventnt handler becomes much more readable:
private void btnSendSms_Click(object sender, RoutedEventArgs e)
{
var info = await GetCurrentCoordinate();
if (info.Item1 != nuil)
{
lblLatitude.Text = info.Item1.Latitude.ToString("0.000");
lblLongitude.Text = info.Item1.Longitude.ToString("0.000");
}
if (info.Item2 != null)
{
var address = info.Item2.Information.Address;
lblCurrAddress.Text = string.Format(
"{0} {1},\n{2} {3},\n{4} ({5})",
address.Street,
address.HouseNumber,
address.PostalCode,
address.City,
address.Country,
address.CountryCode);
}
SendSms(info.Item1, info.Item2);
}
Does this make sense?