user7806247
user7806247

Reputation:

WebView2 (2020/06) -> Where's the DOM?

I moved a sort of working Excel VBA app that automated IE as part of a web-scraping and order placing system I wrote for my wife who was spending hours tortured by the 90's style Usborne Children Books Consultant Portal to a C#/Windows Forms/WebView2 project.

Currently, it appears the basics are not there.

For example...

    1- if(browser.navigationComplete == true){

    That simple check isn't available. 
    How is one supposed to flow sequences of user actions without this?

    2- htmlagilitypack like tools?

    In VBA you can easily work with the DOM since there's types for pretty much everything. Where's that in WebView2?

Maybe I got it wrong, but it appears the webview2 is simply a browser with none of the automation tooling VBA has had for over twenty years. Please tell me I do not have to crawl back to my medieval Excel VBA dungeon. I have seen the light and it is good!

Any architectural or actual tested working code would be greatly appreciated :)

thank you kindly from Vancouver,

Antoine

Upvotes: 10

Views: 9896

Answers (2)

amaitland
amaitland

Reputation: 4420

For those interested I've just released WebView2.DevTools.Dom to NuGet.org. It's free for anyone to use.

Unlike some of the other packages currently available it's Chrome DevTools Protocol based framework for JavaScript execution, DOM access/manipulation and automation. Requires .Net 4.6.2 or .Net Core 3.1 or greater

  • Direct connection to browser via Microsoft.Web.WebView2.DevToolsProtocolExtension (doesn't open a remote debugging port).
  • QuerySelector and QuerySelectorAll support
  • Can be used for automation, supports emulating keyboard and mouse events (like puppeteer)
  • Fully asynchronous
  • Supports multiple frames
  • Approximately 280 xUnit tests
  • Supports returning of complex types from JavaScript

More details and examples in the Readme

 await webView.EnsureCoreWebView2Async();
 
 // Create one instance per CoreWebView2
 // Reuse devToolsContext if possible, dispose (via DisposeAsync) before creating new instance
 // Make sure to call DisposeAsync when finished or await using as per this example
// Add using WebView2.DevTools.Dom; to access the CreateDevToolsContextAsync extension method
 await using var devToolsContext = await webView2Browser.CoreWebView2.CreateDevToolsContextAsync();

// Get element by Id
// https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector
var element = await devToolsContext.QuerySelectorAsync<HtmlElement>("#myElementId");

//Strongly typed element types
//Only a subset of element types have been added so far, use HtmlElement as a generic type for all others
var htmlDivElement = await devToolsContext.QuerySelectorAsync<HtmlDivElement>("#myDivElementId");
var htmlSpanElement = await devToolsContext.QuerySelectorAsync<HtmlSpanElement>("#mySpanElementId");
var htmlSelectElement = await devToolsContext.QuerySelectorAsync<HtmlSelectElement>("#mySelectElementId");
var htmlInputElement = await devToolsContext.QuerySelectorAsync<HtmlInputElement>("#myInputElementId");
var htmlFormElement = await devToolsContext.QuerySelectorAsync<HtmlFormElement>("#myFormElementId");
var htmlAnchorElement = await devToolsContext.QuerySelectorAsync<HtmlAnchorElement>("#myAnchorElementId");
var htmlImageElement = await devToolsContext.QuerySelectorAsync<HtmlImageElement>("#myImageElementId");
var htmlTextAreaElement = await devToolsContext.QuerySelectorAsync<HtmlImageElement>("#myTextAreaElementId");
var htmlButtonElement = await devToolsContext.QuerySelectorAsync<HtmlButtonElement>("#myButtonElementId");
var htmlParagraphElement = await devToolsContext.QuerySelectorAsync<HtmlParagraphElement>("#myParagraphElementId");
var htmlTableElement = await devToolsContext.QuerySelectorAsync<HtmlTableElement>("#myTableElementId");

// Get a custom attribute value
var customAttribute = await element.GetAttributeAsync<string>("data-customAttribute");

//Set innerText property for the element
await element.SetInnerTextAsync("Welcome!");

//Get innerText property for the element
var innerText = await element.GetInnerTextAsync();
//Can also be acessed via calling GetPropertyValueAsync
//Can use this method to get any property that isn't currently mapped
innerText = await element.GetInnerTextAsync();

//Get all child elements
var childElements = await element.QuerySelectorAllAsync("div");

//Change CSS style background colour
await element.EvaluateFunctionAsync("e => e.style.backgroundColor = 'yellow'");

//Type text in an input field
await element.TypeAsync("Welcome to my Website!");

//Scroll Element into View (if needed)
//Can optional specify a Rect to be scrolled into view, relative to the node's border box,
//in CSS pixels. When omitted, center of the node will be used
await element.ScrollIntoViewIfNeededAsync();

//Click The element
await element.ClickAsync();

// Simple way of chaining method calls together when you don't need a handle to the HtmlElement
var htmlButtonElementInnerText = await devToolsContext.QuerySelectorAsync<HtmlButtonElement>("#myButtonElementId")
    .AndThen(x => x.GetInnerTextAsync());

//Event Handler
//Expose a function to javascript, functions persist across navigations
//So only need to do this once
await devToolsContext.ExposeFunctionAsync("jsAlertButtonClick", () =>
{
    _ = devToolsContext.EvaluateExpressionAsync("window.alert('Hello! You invoked window.alert()');");
});

var jsAlertButton = await devToolsContext.QuerySelectorAsync("#jsAlertButton");

//Write up the click event listner to call our exposed function
_ = jsAlertButton.AddEventListenerAsync("click", "jsAlertButtonClick");

//Get a collection of HtmlElements
var divElements = await devToolsContext.QuerySelectorAllAsync<HtmlDivElement>("div");

foreach (var div in divElements)
{
    // Get a reference to the CSSStyleDeclaration
    var style = await div.GetStyleAsync();

    //Set the border to 1px solid red
    await style.SetPropertyAsync("border", "1px solid red", important: true);

    await div.SetAttributeAsync("data-customAttribute", "123");
    await div.SetInnerTextAsync("Updated Div innerText");
}

//Using standard array
var tableRows = await htmlTableElement.GetRowsAsync().ToArrayAsync();

foreach(var row in tableRows)
{
    var cells = await row.GetCellsAsync().ToArrayAsync();
    foreach(var cell in cells)
    {
        var newDiv = await devToolsContext.CreateHtmlElementAsync<HtmlDivElement>("div");
        await newDiv.SetInnerTextAsync("New Div Added!");
        await cell.AppendChildAsync(newDiv);
    }
}

//Get a reference to the HtmlCollection and use async enumerable
//Requires Net Core 3.1 or higher
var tableRowsHtmlCollection = await htmlTableElement.GetRowsAsync();

await foreach (var row in tableRowsHtmlCollection)
{
    var cells = await row.GetCellsAsync();
    await foreach (var cell in cells)
    {
        var newDiv = await devToolsContext.CreateHtmlElementAsync<HtmlDivElement>("div");
        await newDiv.SetInnerTextAsync("New Div Added!");
        await cell.AppendChildAsync(newDiv);
    }
}

Upvotes: 5

1. browser.navigationComplete: You should be able to use the WebView2.NavigationCompleted event.

2. htmlagilitypack: While WebView2 doesn't provide direct DOM access, you could get the html as a string and use the htmlagilitypack LoadHtml function. There's also the htmlagilitypack from web functionality, but I'm not sure if that's just using a WebBrowser under the covers.

Upvotes: 3

Related Questions