Accessing document elements when using Windows.Forms.WebBrowser - winforms

I'm new to automating webpage access, so forgive what is probably a remedial question. I'm using C#/Windows.Forms in a console app. I need to programmatically enter the value of an input on a webpage that I cannot modify and that is running javascript. I have successfully opened the page (triggering WebBrowser.DocumentCompleted). I set browser emulation mode to IE11 (in registry), so scripts run without errors. When DocumentCompleted() triggers, I am unable to access the document elements without first viewing the document content via MessageBox.Show(), which is clearly not acceptable for my unattended app.
What do I need to do so that my document elements are accessbile in an unattended session (so I can remove MessageBox.Show() from the code below)? Details below. Thank you.
The input HTML is:
<input class="input-class" on-keyup="handleKeyPress($key)" type="password">
My DocumentCompleted event handler is:
private static void LoginPageCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
WebBrowser wb = ((WebBrowser)sender);
var document = wb.Document;
// I'm trying to eliminate these 3 lines
var documentAsIHtmlDocument = (mshtml.IHTMLDocument)document.DomDocument;
var content = documentAsIHtmlDocument.documentElement.innerHTML;
MessageBox.Show(content);
String classname = null;
foreach (HtmlElement input in document.GetElementsByTagName("input"))
{
classname = input.GetAttribute("className");
if (classname == "input-class")
{
input.SetAttribute("value", password);
break;
}
}
}

The problem for me was that the page I'm accessing is being created by javascript. Even though documentComplete event was firing, the page was still not completely rendered. I have successfully processed the first page by waiting for the document elements to be available and if not available, doing Application.DoEvents(); in a loop until they are, so I know now that I'm on the right track.
This SO Question helped me: c# WebBrowser- How can I wait for javascript to finish running that runs when the document has finished loading?
Note that checking for DocumentComplete does not accurately indicate the availability of the document elements on a page generated by javascript. I needed to keep checking for the elements and running Application.DoEvents() until they became available (after the javascript generated them).

If the problem comes from the creation of a STAThread, necessary to instantiate the underlying Activex component of WebBrowser control, this is
a modified version of Hans Passant's code as shown in the SO Question you linked.
Tested in a Console project.
class Program
{
static void Main(string[] args)
{
NavigateURI(new Uri("[SomeUri]", UriKind.Absolute), "SomePassword");
Console.ReadLine();
}
private static string SomePassword = "SomePassword";
private static void NavigateURI(Uri url)
{
Thread thread = new Thread(() => {
WebBrowser browser = new WebBrowser();
browser.DocumentCompleted += browser_DocumentCompleted;
browser.Navigate(url);
Application.Run();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}
protected static void browser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
WebBrowser browser = ((WebBrowser)sender);
if (browser.Url == e.Url)
{
while (browser.ReadyState != WebBrowserReadyState.Complete)
{ Application.DoEvents(); }
HtmlDocument Doc = browser.Document;
if (Doc != null)
{
foreach (HtmlElement input in Doc.GetElementsByTagName("input"))
{
if (input.GetAttribute("type") == "password")
{
input.InnerText = SomePassword;
//Or
//input.SetAttribute("value", SomePassword);
break;
}
}
}
Application.ExitThread();
}
}
}

Related

Can WebView2 currently receive Keyboard inputs

I am trying to create a software in WPF which hosts a browser (WebView2 currently 1.0.818.41) and also show a OnScreenKeyboard when there is a input field focused in the browser.
I have done this kind of stuff with CefSharp in WPF before but I cannot do it with WebView2 currently. My problem is I do not find a way to send keystrokes from the OnScreenKeyboard (or from the WPF Window) to the Browser.
In CefSharp there we have a function called ChromiumWebBrowser.GetHost().SendKeyEvent() but I cannot find something similar in WebView2.
Am I blind or is this something which is currently not implemented (or maybe not planed)?
Thank you in advance!
There is no direct way. What can be done is execute some JS, which in turn posts a message to WebView. This message can then be caught back in wv2_WebMessageReceived event.
There is extensive documentation on the interop between.NET and JS and interop between JS and .NET WPF Forms here.
A solution would be to inject a sendMessage JS function in the NavigationStarting event:
private void wv2_NavigationStarting(Microsoft.UI.Xaml.Controls.WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs args){
var sc = "function sendMessage(txt) { window.chrome.webview.postMessage(txt); }";
wv2.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(sc);
}
Now you collect input fields and add onfocus and onblur events to these input fields for example in the NavigationCompleted event like this:
private void wv2_NavigationCompleted(Microsoft.UI.Xaml.Controls.WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs args){
string script = "const collection ="+
"document.getElementsByTagName(\"input\");" +
"for (let i = 0; i < collection.length; i++){" +
"collection[i].onfocus= ()=>{ sendMessage('onFocus('+collection[i].name')'); }; " +
"collection[i].onblur= (ev)=>{ sendMessage('onBlur('+collection[i].name')'); };"+
"}";
sender.ExecuteScriptAsync(script);
}
Now catch the message in the wv2_WebMessageReceived event:
private void wv2_WebMessageReceived(Microsoft.UI.Xaml.Controls.WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2WebMessageReceivedEventArgs args)
{
var postMess = args.TryGetWebMessageAsString();
if (postMess == "onFocus(nameOfField)" )
{
// here activate the button(keyboard)
// store the Name on focusField variable
}
if (postMess == "onBlur" && paneShown)
{
// here deactivate the button(keyboard)
// release the focusField
}
}
Now you can send a click event to the input fields:
private void btn_Clicked(Object sender, EventArgs args)
{
var script = "var field "+
"= document.getElementsByName("+focusField+");" +
" field.value+=field.value"+args.keyValue();
wv2.CoreWebView2.ExecuteScriptAsync(script);
}
wv2 is an instance of WebView2 and the code is typed directly here and not compiled. Hope you get the idea.

Awesomium web browser doesn't show the page

I have made a form which takes a few URLs in the constructor and loads them into some awesomuim web controls on the form.
It worked fine in the first computer but when I move the program to another computer (with the code), it doesn't show the page in the control. when I move the mouse over the control, it shows the busy icon.
But, if i put a breakpoint somewhere in the constructor and then click continue in visual studio, it works fine! I tried to put a sleep in the constructor but it didn't do the job.
here's the code :
public BrowsersWindow(List<string> links)
{
InitializeComponent();
// Thread.Sleep(5000);
Links = links;
Browsers = groupBox1.Controls;
}
private void timer1_Tick(object sender, EventArgs e)
{
if (index < Links.Count && index < Browsers.Count)
{
Awesomium.Windows.Forms.WebControl current =
(Awesomium.Windows.Forms.WebControl)Browsers[index];
try
{
current.Source = new Uri(Links[index]);
}
catch { }
index++;
}
}

CefSharp.Wpf: Open csv, mailto and pdf with Process.Start(...)

I need to handle different content types from f:///. My application renders offline websites in a WPF application. Everything is working except for links to other content files (csv, mailto and pdf).
If I register a CefCustomScheme for "mailto", then I get the ProcessRequestAsync and can run the Process.Start(...). However another blank window also popup.
If I then add a second CefCustomScheme for "file", then nothing happens. None of the ISchemeHandler ProcessRequestAsync methods are invoked.
I must be able to handle all requests, excluding *.html, in a separate handler
Essentially I just want to replicate the behavior of the MS Web-browser Control. There all I did was point to the entry page (index.htm), and everything loaded. Then if a user clicks any link, the control handled the action and started the correct process (content handler, i.e. Excel for Csv).
The code:
// Startup
var settings = new CefSettings();
settings.LogFile = #"c:\temp\ceflog.txt";
settings.LogSeverity = LogSeverity.Verbose;
settings.IgnoreCertificateErrors = true;
CefCustomScheme mailtoScheme = new CefCustomScheme();
mailtoScheme.SchemeName = "mailto";
mailtoScheme.SchemeHandlerFactory = new SchemeHandlerFactory();
CefCustomScheme filesScheme = new CefCustomScheme();
mailtoScheme.SchemeName = "file";
mailtoScheme.SchemeHandlerFactory = new SchemeHandlerFactory();
settings.RegisterScheme(mailtoScheme);
settings.RegisterScheme(filesScheme);
if (!Cef.Initialize(settings))
throw new InvalidOperationException("Failed to initialize the browser factory");
-- SchemeHandlerFactory
public class SchemeHandlerFactory : ISchemeHandlerFactory {
public ISchemeHandler Create() {
return new CustomSchemeHandler();
}
}
-- Handler
public class CustomSchemeHandler : ISchemeHandler {
private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public bool ProcessRequestAsync(IRequest request, ISchemeHandlerResponse response, OnRequestCompletedHandler requestCompletedCallback) {
_log.DebugFormat("Processing url: {0}", request.Dump());
var knownContentTypes = new[] {".csv", ".xsls", ".xlsx", ".pdf", ".txt"};
var ext=Path.GetExtension(request.Url);
if(knownContentTypes.Contains(ext)) {
_log.DebugFormat("Starting process for: {0}",request.Url);
Process.Start(request.Url);
return false;
}
return true;
}
The solution was to implement an IRequestHandler and use the OnBeforeResourceLoad event to check what content was requested. The ISchemeHandler is used for the "mailto" actions.
In my case I had to assign the request handler after the frame loaded. This allowed the web browser to render all content first.
Code sample GitHub example

How to ensure wcf service client finishs his works in silverlight?

I use wcf service client to submit changes of data for a silverlight project. The correlative codes like this:
public class DispatcherCollection : UpdatableCollection<DocumentDispatcher>
{
public override void SubmitChanges()
{
DocumentServiceClient client = new DocumentServiceClient();
client.NewDocumentCompleted += (s, e) =>
{
// (s as DocumentServiceClient).CloseAsync();
// do something
};
client.UpdateColumnCompleted += (s, e) =>
{
// (s as DocumentServiceClient).CloseAsync();
// do something
};
client.RemoveDocumentCompleted += (s, e) =>
{
// (s as DocumentServiceClient).CloseAsync();
// do something
};
foreach (DocumentDispatcher d in this)
{
if (d.IsNew)
{
// d=>object[] data
client.NewDocumentAsync(data);
d.IsNew=false;
}
else
{
foreach (string propertyName in d.modifiedProperties)
{
client.UpdateColumnAsync(d.ID, GetPropertyValue(propertyName));
}
dd.ClearModifications();
}
}
foreach (DocumentDispatcher dd in removedItems)
{
client.RemoveDocumentAsync(dd.ID);
}
removedItems.Clear();
}
}
Class UpdatableCollection derives from ObserableCollection, and I implemtent logics in class DocumentDispatcher and UpdatableCollection to buffer the changes of data such as new created, property modified and removed. I use SubmitChanges method to submit all changes to server.
Now I am stuck:
1. I am at a loss when to close the client after a bunlde fo async calls. I don't know which callback is the last one.
2. What will happen when a user closes the IE immediately right after clicking the save button (it seems to be done because it runs async but in fact the updating threads are industriously running.)?
You can keep a counter or use an isbusy function to monitor the callbacks from your Async calls - to make sure they all finished.
If the user fires off a request to the WCF service, the WCF service will complete but there will be no call back - as the application will be closed.
I think that there is no wait handle for silverlight asynchornized call brings inconvenience. Here is my experence. I want to check and submit modifications of data which are not expicitly submitted when browser is closing. I have implemented codes in App_Exit like this:
private void Application_Exit(object sender, EventArgs e)
{
Document doc = EDPViewModel.CurrentViewModel.Document;
if (doc != null) new ServiceClient().SubmitChangesAsync(doc);
}
provided that in the SubmitChangesAsync method, not submitted modifications of doc are found out and submitted. Therefore, because of the asynchronized running features, while the service invoking is being sent, the application is yet immediately closed. And that will dispose related resouces of the application, including Service Invoking Tasks. So the codes above work not. I hope so eagerly that somewhere exists a mechanism, which can export a wait handle from silverlight asynchronized call, so that I can update the above codes whith this:
private void Application_Exit(object sender, EventArgs e)
{
Document doc = EDPViewModel.CurrentViewModel.Document;
if (doc != null)
{
Task t = new TaskFactory().StartNew(() => new ServiceClient().SubmitChangesAsync(doc));
t.Wait();
}
}
With wait operation I can really be sure that all modifications are really definitely submitted. So is there any similar pattern that can be used in silverlight?
It's for me a good news, as you put it, that calls could work like the mode "requesting and forgetting". So I needn' to worry too much about data losing during submitting.
To ensure all service calls are sent out before application is closed, I think, counter is a simple and effient idea. I will try to implement it in my project.
Thank you for your help!

Launch default web browser, but not if URL already open

I have a link on my app UI that launches a URL using System.Diagnostics.Process.Start(). If the user clicks the link several times, it opens several tabs.
Is there a way, maybe a command-line option, to still use the default web browser, but have it just reopen the same tab if the URL is already open? It would be OK if it doesn't work with every possible browser out there, but nice if it at least works with IE, Firefox and Chrome.
I doubt it, but since I didn't see any other questions/answers on this topic, I figured I'd ask.
This is somewhat of a workaround but it might get you started. I have used the System.Diagnostics.Process.ProcessId.
As an example I have used IE, I will explain later why I did this. The code is just "quick and dirty" but I just made it as proof of concept.
I have created a basic WinForm app with one button that will open google in IE, if it has already been opened by the application it will not be opened again.
I added the System.Diagnostics reference.
public int ProcessID;
public Form1()
{
InitializeComponent();
}
private void MyButton_Click(object sender, EventArgs e)
{
if (ProcessID == null)
{
StartIE();
}
else
{
if (!ProcessIsRunning())
{
StartIE();
}
}
}
private bool ProcessIsRunning()
{
bool ProcessRunning = false;
foreach (Process p in Process.GetProcesses())
{
try
{
if (p.Id == ProcessID)
{
ProcessRunning = true;
}
}
catch { }
}
return ProcessRunning;
}
private void StartIE()
{
System.Diagnostics.Process proc = new System.Diagnostics.Process();
proc.StartInfo.FileName = "iexplore.exe";
proc.StartInfo.Arguments = "http://www.google.be";
proc.Start();
ProcessID = proc.Id;
}
This does not completely do what you requested but it might be a good start. There are a few reasons why I did it this way and what possible options are..
If you would use the url as the Filename, it would indeed open up the webpage in the default browser, it would however not return a processID. This is why the snippet shows usage of IE. (If you would use this option, you could use the System.IO.File.Exists to make sure the desired browser is installed)
If you would like to use this option, you can query the registry to pick up what te default browser is, if you have that you could launch that from the value obtained from the registry. If you then change the process.startinfo.filename to this value, then you will launch the default browser but you will still obtain a processId so this might be the way forward. You can check how to do this over here: http://social.msdn.microsoft.com/Forums/en/netfxbcl/thread/b200903e-ce69-4bd4-a436-3e20a7632dc4
Showing the internet window if it would already be opened, can be done by using the SetForegroundWindow property. As this is already documented in this article, I did not add it in this snippet.
I hope this helps to get you on your way.

Resources