Как дождаться окончания загрузки страницы в WebView2?

Рейтинг: 1Ответов: 2Опубликовано: 12.02.2023

Жду пока на страницу загрузится нужный элемент ( через 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 методом, но не более. Помогите мне решить эту задачу.

Ответы

▲ 3Принят

Чтобы дождаться окончания загрузки страницы, создайте обработчик

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 поток этими проверками - так себе идея.

▲ 0

Предлагаю рассмотреть "универсальный" вариант решения с прикладным классом 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");
}

Результат на скриншоте:

Результат нажатия кнопки cmdConciseCodeNavigate

Код класса 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, находятся по ссылке.