I need to fire a callback when the foreach loop has finished searching through each item int the List<>.
private async void startSearchBtn_Click(object sender, EventArgs e)
{
await Search(files, selectTxcDirectory.SelectedPath, status);
}
private static async Task Search(List<string> files, string path, Label statusText)
{
foreach (string file in files)
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(file);
statusText.Text = "Started scanning...";
using (XmlReader reader = XmlReader.Create(new StringReader(xmlDoc.InnerXml), new XmlReaderSettings() { Async = true }))
{
while (await reader.ReadAsync())
{
if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "LineName"))
{
Console.WriteLine(reader.ReadInnerXml());
}
}
}
}
}
Is this possible and if so how can it be done?
It is very simple, just pass a method as a delegate in parameter. then invoke it wherever you need.
private async void startSearchBtn_Click(object sender, EventArgs e)
{
await Search(files, selectTxcDirectory.SelectedPath, status, SearchCompleted); // <-- pass the callback method here
}
private static async Task Search(List<string> files, string path, Label statusText, Action<string> callback)
{
foreach (string file in files)
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(file);
statusText.Text = "Started scanning...";
using (XmlReader reader = XmlReader.Create(new StringReader(xmlDoc.InnerXml), new XmlReaderSettings() { Async = true }))
{
while (await reader.ReadAsync())
{
if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "LineName"))
{
Console.WriteLine(reader.ReadInnerXml());
}
}
}
// Here you're done with the file so invoke the callback that's it.
callback(file); // pass which file is finished
}
}
private static void SearchCompleted(string file)
{
// This method will be called whenever a file is processed.
}
I'd code it like below. This way, you still keep track of the pending task (_pendingSearch), while startSearchBtn_Click remains synchronous.
You should be keeping track of pending tasks (and be able to cancel them). Otherwise, user may click startSearchBtn twice in a row and spawn two search tasks. This still may be a valid scenario in your case, but usually it is not.
Task _pendingSearch = null;
private void startSearchBtn_Click(object sender, EventArgs e)
{
// check if is _pendingSearch != null and still pending here
_pendingSearch = Search(files,
selectTxcDirectory.SelectedPath, status).ContinueWith((finishedTask) =>
{
// Place your completion callback code here
}, TaskScheduler.FromCurrentSynchronizationContext);
}
private static async Task Search(List<string> files, string path, Label statusText)
{
// ...
}
[EDITED] Using await:
Task _pendingSearch = null;
private async void startSearchBtn_Click(object sender, EventArgs e)
{
// check if is _pendingSearch != null and still pending here
_pendingSearch = Search(files, selectTxcDirectory.SelectedPath, status);
await _pendingSearch;
// Place your completion callback code here
}
Since you're using await, your code in startSearchBtn_Click won't continue until Search is finished.
All you need is something like this:
private async void startSearchBtn_Click(object sender, EventArgs e)
{
await Search(files, selectTxcDirectory.SelectedPath, status);
// run your callback here
}
Related
I want to call a web api method on page load event of my project. But I want to wait for the execution of the function 'GetSelectedTaskDetails' to complete. So that I can manage with the values from DataRow row.
Could you please suggest how can i achieve this?
private DataRow row;
protected void Page_Load(object sender, EventArgs e)
{
GetSelectedTaskDetails(Id);
//other codes
}
private async void GetSelectedTaskDetails(int? selected_task_id)
{
try
{
url = baseUrl + "GetSelectedTaskDetails?task_id=" + selected_task_id;
using (var objClient = new HttpClient())
{
using (var response = await objClient.GetAsync(url))
{
if ((int)response.StatusCode == 401)//unauthorised or token expired
{
Response.Redirect("Default.aspx");
}
if (response.IsSuccessStatusCode)
{
var GetResponse = await response.Content.ReadAsStringAsync();
DataTable dt = JsonConvert.DeserializeObject<DataTable>(GetResponse);
if (dt.Rows.Count == 1)
{
row = dt.Rows[0];
}
}
}
}
}
catch (Exception ex)
{
var message = new JavaScriptSerializer().Serialize(ex.Message.ToString());
var script = string.Format("alert({0});", message);
ScriptManager.RegisterClientScriptBlock(this, this.GetType(), "", script, true);
}
}
You should avoid async void - it's intended for event handlers. So GetSelectedTaskDetails should be async Task instead of async void. Once GetSelectedTaskDetails is properly returning a Task, you can await it in your Page_Load:
protected async void Page_Load(object sender, EventArgs e)
{
await GetSelectedTaskDetails(Id);
...
}
Note that for async to work properly on ASP.NET pre-Core, you need to set Page.Async to true and ensure httpRuntime#targetFramework is set to 4.5 or newer in your web.config
I have been trying a lot of different solutions with wait and async. Nothing seems to work. I was not able to find solution that actually fully waits until page has been fully loaded. All codes are waiting some time but not until page has been loaded and I am getting an error on next process.
How I can set for example code into wait mode until Document.GetElementById("quickFind_text_0") element has been found on page?
Here is my code:
private void button7_Click(object sender, EventArgs e)
{
webBrowser1.Navigate("https://company.crm4.dynamics.com/main.aspx?app=d365default&pagetype=entitylist&etn=opportunity");
webBrowser1.Document.GetElementById("shell-container").Document.GetElementById("quickFind_text_0").SetAttribute("value", "Airbus");
webBrowser1.Document.GetElementById("shell-container").Document.GetElementById("quickFind_text_0").InnerText = "Airbus";
//Thread.Sleep(2000);
HtmlElement fbLink = webBrowser1.Document.GetElementById("shell-container").Document.GetElementById("mainContent").Document.GetElementById("quickFind_button_0"); ;
fbLink.InvokeMember("click");
}
P.S. I have to do this "twice" otherwise it is not working:
webBrowser1.Document.GetElementById("shell-container").Document.GetElementById("quickFind_text_0").SetAttribute("value", "Airbus");
webBrowser1.Document.GetElementById("shell-container").Document.GetElementById("quickFind_text_0").InnerText = "Airbus";
In VBA this works:
While .Busy
DoEvents
Wend
While .ReadyState <> 4
DoEvents
Wend
Is it possible to do the same in C#?
EDIT:
My full code below. For some reason async/await does not work.
System.NullReferenceException HResult=0x80004003 Message=Object
reference not set to an instance of an object. Source=v.0.0.01
StackTrace: at v._0._0._01.Browser.<button7_Click>d__7.MoveNext()
in C:\Users\PC\source\repos\v.0.0.01\v.0.0.01\Browser.cs:line 69
Here is my code:
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace v.0._0._01
{
public static class WebBrowserExtensions
{
public static Task<Uri> DocumentCompletedAsync(this WebBrowser wb)
{
var tcs = new TaskCompletionSource<Uri>();
WebBrowserDocumentCompletedEventHandler handler = null;
handler = (_, e) =>
{
wb.DocumentCompleted -= handler;
tcs.TrySetResult(e.Url);
};
wb.DocumentCompleted += handler;
return tcs.Task;
}
}
public partial class Browser : Form
{
public Browser()
{
InitializeComponent();
}
private async void button7_Click(object sender, EventArgs e)
{
webBrowser1.Navigate("https://company.crm4.dynamics.com/main.aspx?app=d365default&pagetype=entitylist&etn=opportunity");
await webBrowser1.DocumentCompletedAsync(); // async magic
HtmlElement fbLink = webBrowser1.Document.GetElementById("shell-container").Document.GetElementById("mainContent").Document.GetElementById("quickFind_button_0"); ;
fbLink.InvokeMember("click");
}
}
}
Also now I have noticed that quickFind_text_0 and quickFind_button_0 always starts with same words but numbers are changing like quickFind_text_1 and quickFind_button_1 or quickFind_text_2 and quickFind_button_2. However by manual clicking everything works with quickFind_text_0 and quickFind_button_0.
Here is an extension method for easy awaiting of the DocumentCompleted event:
public static class WebBrowserExtensions
{
public static Task<Uri> DocumentCompletedAsync(this WebBrowser wb)
{
var tcs = new TaskCompletionSource<Uri>();
WebBrowserDocumentCompletedEventHandler handler = null;
handler = (_, e) =>
{
wb.DocumentCompleted -= handler;
tcs.TrySetResult(e.Url);
};
wb.DocumentCompleted += handler;
return tcs.Task;
}
}
It can be used like this:
private async void button1_Click(object sender, EventArgs e)
{
webBrowser1.Navigate("https://company.crm4.dynamics.com/main.aspx");
await webBrowser1.DocumentCompletedAsync(); // async magic
HtmlElement fbLink = webBrowser1.Document.GetElementById("quickFind_button_0");
fbLink.InvokeMember("click");
}
The lines after the await will run after the page has completed loading.
Update: Here is another extension method for awaiting a specific element to appear in the page:
public static async Task<HtmlElement> WaitForElementAsync(this WebBrowser wb,
string elementId, int timeout = 30000, int interval = 500)
{
var stopwatch = Stopwatch.StartNew();
while (true)
{
try
{
var element = wb.Document.GetElementById(elementId);
if (element != null) return element;
}
catch { }
if (stopwatch.ElapsedMilliseconds > timeout) throw new TimeoutException();
await Task.Delay(interval);
}
}
It can be used for example after invoking a click event that modifies the page using XMLHttpRequest:
someButton.InvokeMember("click");
var mainContentElement = await webBrowser1.WaitForElementAsync("mainContent", 5000);
I hope I described the problem correctly.
In the following code the drive.IsReady takes some time to complete. Prior to this is the command to print the text "Scanning drives..." in the textbox. The text though appears after the foreach() has completed.
Why is this happening and how can I notify the user prior to the long-running task?
public Form1()
{
InitializeComponent();
button1.Click += new System.EventHandler(this.Button1_Click);
}
private void Button1_Click(object sender, EventArgs e)
{
richTextBox1.Text = "Scanning drives, please wait...";
PopulateComboBox();
}
void PopulateComboBox()
{
System.IO.DriveInfo[] drives = System.IO.DriveInfo.GetDrives();
foreach (System.IO.DriveInfo drive in drives)
{
if (drive.IsReady)
{
comboBox1.Items.Add(drive.Name + drive.VolumeLabel);
}
else
{
comboBox1.Items.Add(drive.Name);
}
}
}
These are the minimal changes required to make the slow part of your code (drive.IsReady) run asynchronously. It won't run faster, the intention is just to keep the UI responsive.
private async void Button1_Click(object sender, EventArgs e) // + async
{
richTextBox1.Text = "Scanning drives, please wait...";
await PopulateComboBox(); // + await
}
async Task PopulateComboBox() // async Task instead of void
{
System.IO.DriveInfo[] drives = System.IO.DriveInfo.GetDrives();
foreach (System.IO.DriveInfo drive in drives)
{
if (await Task.Run(() => drive.IsReady)) // + await Task.Run(() => ...)
{
comboBox1.Items.Add(drive.Name + drive.VolumeLabel);
}
else
{
comboBox1.Items.Add(drive.Name);
}
}
}
In my event load of my form , I call a method loadDg:
private void form_Load(object sender, EventArgs e)
{
loadDg();
}
and
private async Task loadDg()
{
pictureLoading.Visible = true;
await Task.Run(() => { string[] datas = db.row("select * from products");
string[] datas2 = db.row("select * from users");
double one = Convert.ToInt32(datas[0]);
label1.Text = one.toString();
//....
});
pictureLoading.Visible = false; //hide gif animation
}
in my code , db.row This method always returns only 1 row ( string array) , but my ui freezes still , i try update UI continuously with async without freeze at startup
There is nothing to prevent your code run asynchronously. pictureLoading will be invisible even before task is completed. You should fix cross-thread problem and logic of the UI as this:
private void form_Load(object sender, EventArgs e)
{
pictureLoading.Visible = true;
loadDg();
}
private async Task loadDg()
{
await Task.Run(() =>
{
string[] datas = db.row("select * from products");
string[] datas2 = db.row("select * from users");
double one = Convert.ToInt32(datas[0]);
label1.BeginInvoke((Action)delegate ()
{
label1.Text = one.toString();
//hide gif animation
pictureLoading.Visible = false;
});
//....
});
}
Unnecessarily jumping between threads/context should be avoided.
This is an with better resource usage:
private async void form_Load(object sender, EventArgs e)
{
pictureLoading.Visible = true;
try
{
label1.Text = await LoadDgAsync();
}
catch
{
// error handling
}
finally
{
pictureLoading.Visible = false;
}
}
private Task<string> LoadDgAsync()
{
return Task.Run(() =>
{
string[] datas = db.row("select * from products");
string[] datas2 = db.row("select * from users");
double one = Convert.ToInt32(datas[0]);
//....
return one.toString();
});
}
You are calling the loadDg() function synchronously.
Unless you await the loadDg() function call (since its return type is Task) and make the form_Load function asynchronous the function call will be synchronous.
The correct way to fix it is...
private async void form_Load(object sender, EventArgs e)
{
await loadDg();
}
I have a long running method which I made async. I made my button click handler async as well, but when I try to access my label in my button click after the long method is done, it tells me it can't can't access it from another thread. Here is the code:
private void Migrate()
{
for (int i = 2; i <= excelData.GetUpperBound(0); i++)
{
var poco = new ExpandoObject() as IDictionary<string, object>;
foreach (var column in distributionColumnExcelHeaderMappings)
{
if (column.ColumnIndex > 0)
{
var value = excelData[i,column.ColumnIndex]?.ToString();
poco.Add(column.DistributionColumnName.Replace(" ", ""), value);
}
}
pocos.Add(poco);
}
migrationRepository.BulkInsert(insertToTable, "Id", pocos);
}
private async void btnMigrate_Click(object sender, EventArgs e)
{
Task task = new Task(()=> Migrate());
task.Start();
lblStatus.Text = "Migrating data....";
await task;
lblStatus.Text = "Migration Complete";
}
When the button is clicked, I see the status Migrating data..... When that is complete, it throws an error on lblStatus.Text = "Migration Complete". I thought after await, it goes back to the UI thread?
I cleared out most of the code and it still throws the same error. This is a VSTO excel add-in. Could that be part of the problem?
private void Migrate()
{
}
private async void btnMigrate(object sender, EventArgs e)
{
Task.Run(()=>Migrate());
lblStatus.Text = "Done"; //still get error here
}
Try and update your code to the following:
Instead of creating your task and then starting it manually, update it to just await on Task.Run:
private async void btnMigrate_Click(object sender, EventArgs e)
{
lblStatus.Text = "Migrating data....";
await Task.Run(()=> Migrate());
lblStatus.Text = "Migration Complete";
}
Edit:
You can use a helper method that will check to see if the label needs to be invoked before updating.
private async void btnMigrate_Click(object sender, EventArgs e)
{
SetLabelText(lblStatus, "Migrating data....");
await Task.Run(()=> Migrate());
SetLabelText(lblStatus, "Migration complete.");
}
private void SetLabelText(Label label, string text)
{
if (label.InvokeRequired)
{
label.BeginInvoke((MethodInvoker) delegate() {label.Text = text;});
}
else
{
label.Text = text;
}
}