Как дождаться окончания загрузки страницы в WebView2?
Жду пока на страницу загрузится нужный элемент ( через webview2 ), после чего хочу нажать на него через Send.Keys. Не знал как дождаться и придумал это:
private async void CheckLoading()
{
List<string> list = new List<string>();
do
{
list.Clear();
var context = await webView23.CoreWebView2.CreateDevToolsContextAsync(); // Получает что-то вроде Document
var sector = await context.QuerySelectorAllAsync<HtmlDivElement>("div"); // Собирает все <DIV>
foreach (var text in sector)
{
var divtext = await text.GetInnerTextAsync(); // Текст из загруженных <DIV>
list.Add(divtext); // Добавляет в list
}
} while (list[2].ToString() != "Product Activation"); // Если в листе есть нужный текст, то нужный <DIV> загружен, могу с ним работать
MessageBox.Show("Найдено");
}
private void buttonSend_Click_1(object sender, EventArgs e)
{
CheckLoading();
}
Никогда не получаю сообщение "Совпадение", если нажму Click_button_1 на кнопку до появления нужного элемента. ( а в этом и смысл ). Хотя вроде до этого момента, работа должна происходить в цикле.
Я понимаю что это связано как-то с Async методом, но не более. Помогите мне решить эту задачу.
Ответы (2 шт):
Чтобы дождаться окончания загрузки страницы, создайте обработчик
private void OnCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
{
MessageBox.Show("Страница загружена");
}
И подпишитесь на событие, один раз
WebView.CoreWebView2.NavigationCompleted += OnCompleted;
Когда не надо будет вызывать обработчик, отпишитесь обратно
WebView.CoreWebView2.NavigationCompleted -= OnCompleted;
async void нужно использовать с осторожностью. Следует блокировать повторное нажатие кнопки, иначе будет каша. Ну и добавить try-catch для обработки исключений.
Кстати, можно дождаться освобождения браузера асинхронно, пробросив событие NavigationCompleted через TaskCompletionSource.
Пример:
public async Task NavigateAsync(string url)
{
TaskCompletionSource tcs = new();
EventHandler<CoreWebView2NavigationCompletedEventArgs> handler = async (s, e) => tcs.SetResult();
try
{
webView.CoreWebView2.NavigationCompleted += handler;
webView.CoreWebView2.Navigate(url);
await tcs.Task;
}
finally
{
webView.CoreWebView2.NavigationCompleted -= handler;
}
}
private async void buttonSend_Click_1(object sender, EventArgs e)
{
Button btn = (Button)sender;
btn.Enabled = false;
try
{
await NavigateAsync("https://ru.stackoverflow.com");
await WaitForElementAsync();
MessageBox.Show("Страница загружена");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
btn.Enabled = true;
}
private async Task WaitForElementAsync()
{
var context = await webView23.CoreWebView2.CreateDevToolsContextAsync();
while (true)
{
var nodes = await context.QuerySelectorAllAsync<HtmlDivElement>("div");
var text = await nodes.Skip(2).First().GetInnerTextAsync();
if (text != "Product Activation")
{
MessageBox.Show("Найдено");
break;
}
await Task.Delay(50);
}
}
Смысл здесь в том, что пока страница не будет загружена, нет никакого смысл начинать парсить HTML.
Так же подозреваю, что context не следует создавать кучу раз в цикле. Вынесите его создание из цикла. Если нужно провести какое-то сложное ожидание, например отследить появление элемента на странице по каким-то непредсказуемым условиям, то решать такой вопрос следует средствами JavaScript, а не поллингом. В JS есть класс MutationObserver, его можно повесить на ноду в DOM и он вызовет функцию, когда DOM изменится, с этой функции можно вызвать колбэк, который пробросить в C# через тот же JS Promise. Да, в JavaScript тоже есть асиинхронность. Опрашивать DOM 100500 раз в секунду полностью повесив UI поток этими проверками - так себе идея.
Предлагаю рассмотреть "универсальный" вариант решения с прикладным классом WebView2Controller и еще парой вспомогательных классов WebView2Commander и TimeOutTimer: например, чтобы загрузить страницу https://ru.stackoverflow.com и дождаться доступности кнопки [Задать вопрос] в тестовой System.Windows.Forms форме TestForm с MS WebView2 контролом с именем mainWebView, надо будет инициализировать WebView2Controller в событии формы Load:
private WebView2Controller _wvc;
private async void TestForm_Load(Object sender, EventArgs e)
{
_wvc = new WebView2Controller(mainWebView, log);
await _wvc.Init();
}
а затем нажать кнопку cmdConciseCodeNavigateAndWait для выполнения следующего кода:
private string _url = "https://ru.stackoverflow.com/";
private string askQuestionButtonSelector => "#mainbar > div:nth-child(1) > div > a";
private async Task<string> askQuestionButtonCaption()
{
return await _wvc.GetButtonCaption(askQuestionButtonSelector);
}
private async Task<bool> askQuestionButtonFound()
{
return !string.IsNullOrWhiteSpace(await askQuestionButtonCaption());
}
private async void cmdConciseCodeNavigateAndWait_Click(Object sender, EventArgs e)
{
clearLog();
if (!await _wvc.NavigateAndWaitForCondition(_url, askQuestionButtonFound))
log("[Ask Question] button not found");
else
log("[Ask Question] button found");
}
Результат на скриншоте:
Код класса WebView2Controller:
using System;
using System.Diagnostics;
using System.Security.Policy;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms;
namespace WV2.VividBroker
{
public class WebView2Controller: WebView2Commander
{
public WebView2Controller(WebView2 wv,
Func<string, string> logger = null) :
base(wv, logger) { }
public async Task Init()
{
var env = await CoreWebView2Environment.CreateAsync(null, null);
await _wv.EnsureCoreWebView2Async(env);
}
private const int TIMER_TICK_INTERVAL_IN_MS = 50;
private const int TIME_OUT_IN_SECONDS = 10;
private ManualResetEvent _waitFlag;
private TimeOutTimer _smartTimer;
public EventHandler<EventArgs> ConditionVerified { get; set; }
public EventHandler<EventArgs> TimeOut { get; set; }
public EventHandler<EventArgs> Tick { get; set; }
public double NavigationElapsedTimeInSeconds => _smartTimer.ElapsedTimeInSeconds;
public async Task<bool> NavigateAndWaitForCondition(
string url,
Func<Task<bool>> condition,
int timerTickIntervalInMS = TIMER_TICK_INTERVAL_IN_MS,
int timeOutInSeconds = TIME_OUT_IN_SECONDS)
{
var conditionOK = false;
_waitFlag = new ManualResetEvent(false);
_smartTimer = new TimeOutTimer(
condition,
timerTickIntervalInMS,
timeOutInSeconds);
_smartTimer.ConditionVerified += new EventHandler<EventArgs>(conditionVerified);
_smartTimer.TimeOut += new EventHandler<EventArgs>(timeOut);
_smartTimer.Tick += new EventHandler<EventArgs>(tick);
try
{
this.Navigate(url);
_smartTimer.Start();
var sw = Stopwatch.StartNew();
log($"Non-blocking wait started.");
await Task.Run(() =>
{
_waitFlag.WaitOne();
});
log($"Non-blocking wait completed, elapsed = {sw.Elapsed.TotalSeconds:0.000}s");
if (_smartTimer.ConditionVerifiedFlag)
{
log($"Condition verified, elaped time = {_smartTimer.ElapsedTimeInSeconds:0.000}s");
conditionOK = true;
}
else if (_smartTimer.TimeOutFlag)
{
log($"Time-Out, elaped time = {_smartTimer.ElapsedTimeInSeconds:0.000}s");
}
else
{
#if DEBUG
Debug.Assert(false, "It can never happen...");
#else
throw new ApplicationException("It can never happen, but...");
#endif
}
}
finally
{
_smartTimer.ConditionVerified -= new EventHandler<EventArgs>(conditionVerified);
_smartTimer.TimeOut -= new EventHandler<EventArgs>(timeOut);
_smartTimer.Tick -= new EventHandler<EventArgs>(tick);
_smartTimer.Dispose();
}
return conditionOK;
}
private void timeOut(Object sender, EventArgs e)
{
this?.TimeOut?.Invoke(sender, e);
_waitFlag.Set();
}
private void conditionVerified(Object sender, EventArgs e)
{
this?.ConditionVerified?.Invoke(sender, e);
_waitFlag.Set();
}
private void tick(Object sender, EventArgs e)
{
this?.Tick?.Invoke(sender, e);
}
}
}
Код классов WebView2Commander и TimeOutTimer, как и работающие примеры System.Windows.Forms-проектов для .NET Framework 4.8.1 и .NET 9, находятся по ссылке.
