System.Windows.Forms.WebBrowser wait until page has been fully loaded - c#

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);

Related

How can I print a pdf document from Xamarin.Forms UWP?

I have a Xamarin.Forms application that supports only UWP. I cannot find a way to print a pdf document. Whatever I have seen on the web, for some reason doesn't work for me. E.g. I tried
https://www.syncfusion.com/kb/8767/how-to-print-pdf-documents-in-xamarin-forms-platform
It lets me print, but the preview in the print dialog never shows up, and the progress indicator just keeps rotating forever.
I also tried http://zawayasoft.com/2018/03/13/uwp-print-pdf-files-silently-without-print-dialog/
This gives me errors that I cannot fix.
So I wonder if somebody can suggest something else that would actually work. Maybe something newer than what I have tried (I use VS 2017). Printing without the printing dialog would be preferable.
Thank you in advance.
I used a very dirty hack to do that!
What I had to do was to try to print the image version of the pdf (I did the conversion in backend) and then used the following DependencyInjection:
Inside my Print class in UWP project:
class Print : IPrint
{
void IPrint.Print(byte[] content)
{
Print_UWP printing = new Print_UWP();
printing.PrintUWpAsync(content);
}
}
and the class responsible for printing in uwp:
public class Print_UWP
{
PrintManager printmgr = PrintManager.GetForCurrentView();
PrintDocument PrintDoc = null;
PrintDocument printDoc;
PrintTask Task = null;
Windows.UI.Xaml.Controls.Image ViewToPrint = new Windows.UI.Xaml.Controls.Image();
public Print_UWP()
{
printmgr.PrintTaskRequested += Printmgr_PrintTaskRequested;
}
public async void PrintUWpAsync(byte[] imageData)
{
int i = 0;
while (i < 5)
{
try
{
BitmapImage biSource = new BitmapImage();
using (InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream())
{
await stream.WriteAsync(imageData.AsBuffer());
stream.Seek(0);
await biSource.SetSourceAsync(stream);
}
ViewToPrint.Source = biSource;
if (PrintDoc != null)
{
printDoc.GetPreviewPage -= PrintDoc_GetPreviewPage;
printDoc.Paginate -= PrintDoc_Paginate;
printDoc.AddPages -= PrintDoc_AddPages;
}
this.printDoc = new PrintDocument();
try
{
printDoc.GetPreviewPage += PrintDoc_GetPreviewPage;
printDoc.Paginate += PrintDoc_Paginate;
printDoc.AddPages += PrintDoc_AddPages;
bool showprint = await PrintManager.ShowPrintUIAsync();
}
catch (Exception e)
{
Debug.WriteLine(e.ToString());
}
// printmgr = null;
// printDoc = null;
// Task = null;
PrintDoc = null;
GC.Collect();
printmgr.PrintTaskRequested -= Printmgr_PrintTaskRequested;
break;
}
catch (Exception e)
{
i++;
}
}
}
private void Printmgr_PrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args)
{
var deff = args.Request.GetDeferral();
Task = args.Request.CreatePrintTask("Invoice", OnPrintTaskSourceRequested);
deff.Complete();
}
async void OnPrintTaskSourceRequested(PrintTaskSourceRequestedArgs args)
{
var def = args.GetDeferral();
await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
args.SetSource(printDoc.DocumentSource);
});
def.Complete();
}
private void PrintDoc_AddPages(object sender, AddPagesEventArgs e)
{
printDoc.AddPage(ViewToPrint);
printDoc.AddPagesComplete();
}
private void PrintDoc_Paginate(object sender, PaginateEventArgs e)
{
PrintTaskOptions opt = Task.Options;
printDoc.SetPreviewPageCount(1, PreviewPageCountType.Final);
}
private void PrintDoc_GetPreviewPage(object sender, GetPreviewPageEventArgs e)
{
printDoc.SetPreviewPage(e.PageNumber, ViewToPrint);
}
}
Please note that this is not a perfect solution and sometimes it crashes without actually being able to trace the exception (which is really strange) so I am sure there must be better answers even though it does the job.

Running a Webbrowser thread in a task

I have a program that runs and starts 2 long running tasks. One of the tasks is a web scraper in which I have to use the WebBrowser ActiveX control so that I can get the rendered page. In order to use the control I have to start a thread so that I can set the apartment state for the message loop. When I do this, the proogram works fine, or at least for the first page that is fetched. Subsequent pages/calls, the webbrowser times out and it's state seems to remain at "uninitialized". In tracing my code, I never see the "HandleDestroyed" fire for the WebClient.
What do I need to do to Properly Destroy the WebBrowser control and or my own class in order for it to be reused again.
public static string AXFetch(string url, string ua)
{
TestBrowser TB = new TestBrowser();
Thread th = new Thread(() => TB.MakeLiRequest(url,ua));
th.SetApartmentState(ApartmentState.STA);
th.Start();
th.Join(new TimeSpan(0, 0, 90)); //90 second timeout
SiteData = TB.DocumentText;
TB = null;
return SiteData;
}
class TestBrowser
{
public string DocumentText = "";
private bool DocCompleted = false;
public TestBrowser()
{
}
private void reset_fetch_status()
{
this.DocCompleted = false;
this.DocumentText = "";
}
public void MakeLiRequest(string url, string UA)
{
reset_fetch_status();
using (WebBrowser wb = new WebBrowser())
{
wb.Visible = false;
wb.AllowNavigation = true;
wb.ScriptErrorsSuppressed = true;
wb.DocumentCompleted += this.wb_DocumentCompleted;
wb.Navigate(url, "_self", null, "User-Agent: " + UA + "\r\n");
WaitForPage();
wb.Url = null;
wb.DocumentCompleted -= this.wb_DocumentCompleted;
}
}
private void HandleDestroyed(Object sender, EventArgs e)
{
//This never seems to fire, I don't knwo why
Logging.DoLog("You are in the Control.HandleDestroyed event.");
}
private bool WaitForPage()
{
int timer = 0;
while (this.DocCompleted == false)
{
Application.DoEvents();
System.Threading.Thread.Sleep(100);
++timer;
if (timer == (PageTimeOut * 10))
{
Console.WriteLine("WebBrowser Timeout has been reached");
Application.Exit();
return false;
}
}
return true;
}
private void wb_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
WebBrowser wb = (WebBrowser)sender;
if (wb.ReadyState == WebBrowserReadyState.Complete)
{
this.DocumentText = wb.DocumentText;
this.DocCompleted = true;
}
}
}
On handle destroyed will only be called by the parent form.
If you were to try to access from the webbrowser control you would get this error:
Error 1 Cannot access protected member
'System.Windows.Forms.Control.OnHandleDestroyed(System.EventArgs)' via a
qualifier of type 'System.Windows.Forms.WebBrowser'; the qualifier must be of type 'stackoverflowpost47036339.Form1' (or derived from it)
Also you are not hooking it up. But since you haven't given your web browser any parent form, It can't be called. This is how you would hook it up to the parent form.
form1.HandleDestroyed += Form1_HandleDestroyed;
}
void Form1_HandleDestroyed(object sender, EventArgs e)
{
}

Can't access winforms label after await?

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;
}
}

Print Multiple Pages From a UWA

I have about 8 records that I want to print in one batch, each on a separate page. However, the UWP sample for this uses over 600 lines of code to accomplish it. It seems to me that it has to be much, much easier than that. I thought all we'd have to do is add each page to the PrintDocument and send the print job. Apparently not. I'm using this:
async void Print()
{
var printDocument = new PrintDocument();
var printDocumentSource = printDocument.DocumentSource;
var printMan = PrintManager.GetForCurrentView();
printMan.PrintTaskRequested += PrintTaskRequested;
var pages = new List<Page>();
foreach (var item in items)
{
(//Set up variables)
var printPage = new PageToPrint() { //Set properties };
printPage.Set_Up(); //Set up fields
pages.Add(printPage);
}
printDocument.SetPreviewPage(1, page);
printDocument.SetPreviewPageCount(pages.Count, PreviewPageCountType.Final);
foreach (var page in pages)
{
printDocument.AddPage(page);
}
printDocument.AddPagesComplete();
await PrintManager.ShowPrintUIAsync();
}
void PrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs e)
{
PrintTask printTask = null;
printTask = e.Request.CreatePrintTask("Kimble Print Job", sourceRequested =>
{
printTask.Completed += PrintTask_Completed;
sourceRequested.SetSource(printDocumentSource);
});
}
private async void PrintTask_Completed(PrintTask sender, PrintTaskCompletedEventArgs args)
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
PrintManager printMan = PrintManager.GetForCurrentView();
printMan.PrintTaskRequested -= PrintTaskRequested;
});
}
However, it won't generate the print preview. It just sits there spinning and spinning, and if I hit "print" it doesn't succeed (PDF can't open, job never gets to a physical printer.)
I was hoping printing would be at least reasonably easy with the PrintDocument, and I still think it looks like it should be. Am I just missing it here, or does it really take 600+ lines of code to dispatch a simple print job?
However, it won't generate the print preview.
This is because the setPreview method printDocument.SetPreviewPage(1, page); must be put in printDocument.GetPreviewPageevent handle. So you should register the event handle firstly. Same with printDocument.AddPages event handle.You messed up the event handle register and callback function all in one.Here I do a little change of your code and I tested it works well.
protected PrintDocument printDocument;
protected IPrintDocumentSource printDocumentSource;
List<Page> pages = new List<Page>();
Page printPage = new PageToPrint();
public MainPage()
{
this.InitializeComponent();
RegisterForPrinting();
}
private async void BtnPrint_Click(object sender, RoutedEventArgs e)
{
await PrintManager.ShowPrintUIAsync();
}
public void RegisterForPrinting()
{
printDocument = new PrintDocument();
printDocumentSource = printDocument.DocumentSource;
pages.Add(printPage);
printDocument.GetPreviewPage += GetPrintPreviewPage;
printDocument.AddPages += AddPrintPages;
PrintManager printMan = PrintManager.GetForCurrentView();
printMan.PrintTaskRequested += PrintTaskRequested;
}
private void AddPrintPages(object sender, AddPagesEventArgs e)
{
foreach (var page in pages)
{
printDocument.AddPage(page);
}
printDocument.AddPagesComplete();
}
private void GetPrintPreviewPage(object sender, GetPreviewPageEventArgs e)
{
printDocument.SetPreviewPage(1, printPage);
printDocument.SetPreviewPageCount(pages.Count, PreviewPageCountType.Final);
}
void PrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs e)
{
PrintTask printTask = null;
printTask = e.Request.CreatePrintTask("Kimble Print Job", sourceRequested =>
{
printTask.Completed += PrintTask_Completed;
sourceRequested.SetSource(printDocumentSource);
});
}
private async void PrintTask_Completed(PrintTask sender, PrintTaskCompletedEventArgs args)
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
PrintManager printMan = PrintManager.GetForCurrentView();
printMan.PrintTaskRequested -= PrintTaskRequested;
});
}
Although you may not need all the code of the sample, but I recommend you to follow the official sample structure and build a PrintHelper class.

Fire callback after async Task method

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
}

Categories