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
Related
I'm creating a simple program that pings all the servers on our network and returns whether the ping requests were successful.
I'm trying to utilise background workers so that the user can press the ping button and the pings run in the background while they can do other things on the UI
DoWork runs fine, there's no loop to keep it there infinitely, and it reaches the line:
r = pinger.Send(s)
and then from my understanding it ends and so the RunWorkCompleted method should be called?
I'm relearning programming after a long abscense so if I missed something obvious I apologise
...
public Form1()
{
InitializeComponent();
backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
}
private void Ping_Btn_Click(object sender, EventArgs e)
{
count = Convert.ToInt32(pingSeconds_TxtBox.Text);
if (backgroundWorker1.IsBusy != true)
{
// Start operation
backgroundWorker1.RunWorkerAsync();
}
}
private static void OnTimedEvent(Object source, ElapsedEventArgs e)
{
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerSupportsCancellation = true;
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
if(worker.CancellationPending == true)
{
e.Cancel = true;
}
else
{
for(int i = 0; i <= count; i++)
{
MessageBox.Show("something is happening");
// Create ping object
Ping pinger = new Ping();
PingReply r;
// IP to test ping
string s = "###";
try
{
r = pinger.Send(s);
}
catch (Exception b)
{
MessageBox.Show(b.ToString());
}
}
}
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show("Show me something");
if(e.Cancelled == true)
{
statusLbl1.Text = "Cancelled";
} else if(e.Error != null)
{
statusLbl1.Text = "Error: " + e.Error.Message;
} else
{
statusLbl1.Text = "YEEEEEEEET";
}
}
...
You need to attach your backgroundWorker1_RunWorkerCompleted event handler to the RunWorkerCompleted event. The C# compiler doesn't hook handlers to events based on naming conventions. You have to do it explicitly.
public Form1()
{
InitializeComponent();
backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
backgroundWorker1.DoWork += backgroundWorker1_DoWork;
backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
}
I strongly suggest you convert this code to use async await which is much better at representing the flow of code control, rather than using the old BackgroundWorker which is basically deprecated.
Note the following:
The main event handler should be async void but all other async functions should be async Task.
Use of SemaphoreSlim.WaitAsync(0) to check if we are busy.
Ping object needs a using or finally to dispose it, as does the CancellationTokenSource.
<= count looks like it should be < count because you begin at 0.
SemaphoreSlim sem = new SemaphoreSlim(1, 1);
CancellationToken token;
private async void Ping_Btn_Click(object sender, EventArgs e)
{
if (!await sem.WaitAsync(0))
return;
var tokenSource = new CancellationTokenSource();
try
{
var count = Convert.ToInt32(pingSeconds_TxtBox.Text);
await RunPingsAsync(count, tokenSource.Token);
statusLbl1.Text = "YEEEEEEEET";
}
catch (OperationCanceledException)
{
statusLbl1.Text = "Cancelled";
}
catch (Exception e)
{
statusLbl1.Text = "Error: " + e.Error.Message;
}
finally
{
sem.Release();
tokenSource.Dispose();
}
MessageBox.Show("Show me something");
}
private Task RunPingsAsync(int count, CancellationToken token)
{
for(int i = 0; i < count; i++)
{
token.ThrowIfCancellationRequested();
MessageBox.Show("something is happening");
// IP to test ping
string s = "###";
// Create ping object
using (Ping pinger = new Ping())
{
var r = await pinger.SendPingAsync(s);
}
}
}
If you want to keep an infinite loop, then you have to make a loop in your backgroundWorker1_DoWork Method. Something like this
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = (BackgroundWorker) sender;
while (!worker.CancellationPending)
{
//Do your stuff here
for(int i = 0; i <= count; i++)
{
MessageBox.Show("something is happening");
// Create ping object
Ping pinger = new Ping();
PingReply r;
// IP to test ping
string s = "###";
try
{
r = pinger.Send(s);
}
catch (Exception b)
{
MessageBox.Show(b.ToString());
}
}
}
}
Also, it is not a good idea to display message boxes from your background thread, Log it in console or any file.
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);
}
}
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;
}
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?
i have my main code executed from MainWindow.xaml.cs and a thread, called testThread.
I need testThread to read inside a folder (with subfolders too) decided by MainWindow and read one file per time, everytime MainWindow needs it. I know that using a thread is pretty useless with this example but i need this mechanic to be implemented in a bigger context.
What can be the best approach to realize this? My tought was to use a semaphore and lock the thread after every file read so i can unlock it everytime i need next file. Here what i tried to implement (and doesn't work):
MainWindow.cs
public static string myRootDir;
private void Button_test_Click(object sender, RoutedEventArgs e)
{
try
{
myRootDir = #"C:\";
testThread myThread = new testThread();
Thread workerThread = new Thread(myThread.start);
workerThread.Start();
//for this test i'm getting the first 10 files
for (int c1 = 1; c1 < 10; c1++ )
{
result.Text += myThread.getNext();
//this is just a test, so i wait 1 second from each request
Thread.Sleep(1000);
}
}
catch (Exception error)
{
result.Text = error.Message;
}
}
testThread.cs
public class testThread
{
private string rootDir = "";
public static string currentFilePath = "";
Semaphore mySemaphore = new Semaphore(0, 2);
public void start()
{
rootDir = MainWindow.myRootDir;
startReading(rootDir);
}
public string getNext()
{
mySemaphore.Release();
return currentFilePath;
}
private void startReading(string root)
{
//read all the files in current dir
foreach (string singleFilePath in Directory.GetFiles(root, "*.*"))
{
currentFilePath = singleFilePath;
mySemaphore.WaitOne();
}
//repeat for all subfolders
foreach (string singleDirPath in Directory.GetDirectories(root))
{
startReading(singleDirPath);
}
}
}
From debug i can see the code doesn't stop on mySemaphore.WaitOne();, and i suspect i'm doing other errors too!