WebView2

Setup & Installation

For every WebView2 project, use the NuGet package manager within Visual Studio to add the WebView2 SDK to the project. You install the Microsoft.Web.WebView2 SDK NuGet package for use by the current project.

After installation, you will see WebView2 in your Toolbox. If you do not see WebView2 control in your Toolbox, try to Reset Toolbox (right-click), or close and re-open Visual Studio.

Hint: If you still do not see WebView2 in your Toolbox, check path name to your project. It must not contain forbidden chars - including hash #. If you place your project to folder C:\MyProjects\c#\, you obtain unpredictable results. Without warning.

Add WebView2 to Form

Adding WebView2 to Form is simple – and very same as any other Control. You can easily create your own browser. But usually you do want to create browser with some special properties.

Navigation – display given URL

The very first thing is to navigate WebView2 control to some URL address. The easy way is to set WebView2.Source property in design time. Once you set the Source property (e.g. https://www.stavebky.cz), you can run the Form and the web page is displayed.

If you want to navigate to some URL from code, you have tho options. You can set WebView2.Source property, but the property does not accept string, but Uri object:

webView2.Source = new Uri("https://baldsoft.com");

The second option is to set CoreWebView2.Navigate property. Here we set URL as string – not Uri object.

webView2.CoreWebView2.Navigate("https://baldsoft.com");

Both options have the same result. WebView2 will display new URL. But that is not all! Read following:

Initialization of WebView2

If you set WebView2.Source property in design time, then you can navigate in your code to different URL without problems. BUT if you leave WebView2.Source property in design time empty, then do not forget to initialize the WebView2 before you use it. Following example is button click handler navigating to given URL:

        private async void button1_Click(object sender, EventArgs e)
        {
            // Initialize webView2 
            await webView2.EnsureCoreWebView2Async(null);

            // Navigate to URL
            webView2.CoreWebView2.Navigate("https://baldsoft.com");
        }

Initialization is made by webView2.EnsureCoreWebView2Async(null). It is asynchronous function, hence we have to wait for result with await. And that means that the button click handler itself must be async function.

Once again: this initialization is necessary only if the WebView2 control is not initialized by setting Source property in design time.

Waiting for navigation to complete

Navigation of WebView2 is asynchronous. That means, that after setting webView2.Source the program goes to next line although the web page is not loaded yet! You can wait for navigation to complete in WebView2 by using the NavigationCompleted event. This event is raised when the WebView2 has completely loaded or loading stopped with an error.

       private async void button1_Click(object sender, EventArgs e)
       {
           // Wait for webView2 initialization
           await webView2.EnsureCoreWebView2Async(null);

           // Add handler for NavigationCompleted event
           webView2.NavigationCompleted += OnNavigationCompleted;

           // Navigate to URL
           webView2.CoreWebView2.Navigate("https://baldsoft.com");
       }

       
       private void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
       {
           if (e.IsSuccess)
           {
               // Navigation completed successfully
               MessageBox.Show("Navigation completed succesfully!");
           }
           else
           {
               // Handle error
               MessageBox.Show($"Something went wrong,\nWebErrorStatus {e.WebErrorStatus}\nHttpStatusCode: {e.HttpStatusCode}");
           }
       }

In the code above the handler was named OnNavigationCompleted and begins on line 14. Note, that before calling Navigate method, this event handler must be attached (line 7). The handler has two arguments: sender and e. Argument e is of type CoreWebView2NavigationCompletedEventArgs with information about the result of navigation. The most important is e.IsSucces, (see line 16 and MessageBox on line 24).

Hint: Attaching handler in the example above gives warning on line 7. That is because the compiler is not sure, that webView2 is not null. You can get rid of this warning by modifying OnNavigationCompleted declaration (add "?" after object): 
private void OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)

Handler of NavigationCompleted can be attached as a lambda function as well. See following short example:

webView2.NavigationCompleted += OnNavigationCompleted;

private void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
{
    if (e.IsSuccess)
    {
        // Navigation completed successfully
    }
    else
    {
        // Handle error
    }
}

Remember that the attached handler remains attached until it is detached. The following handler will therefore cause an infinite loop:

  private async void button1_Click(object sender, EventArgs e)
  {
      await webView2.EnsureCoreWebView2Async(null);

      // Attach handler for NavigationCompleted event
      webView2.NavigationCompleted += OnNavigationCompleted;

      // Navigate to URL
      webView2.CoreWebView2.Navigate("https://baldsoft.com");
  }

  
  private void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
  {
      if (e.IsSuccess)
      {
         MessageBox.Show("Navigation completed succesfully!");
         // Beware! Following line causes infinite loop!
         webView2.CoreWebView2.Navigate("https://baldsoft.com");
      }
  }

Solution in this case is e.g. detach the handler

private void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
{
    // Detaching the handler
    webView2.NavigationCompleted -= OnNavigationCompleted;
    if (e.IsSuccess)
    {
       MessageBox.Show("Navigation completed succesfully!");        
       webView2.CoreWebView2.Navigate("https://baldsoft.com");
    }
}

…or you can use a variable according to which different methods will be called in the handler.

Passing parameter to NavigationCompleted handler

Another way how to modify behaviour of WebView2.NavigationCompleted handler is to pass some parameter to the handler. You can use some property which is accessible from the handler and set the property before navigation. But sometimes it would be nice to pass some parameter directly to the handler. The handler itself has only two parameters (sender, e). But we can use following trick: the handler (with two parameters) immediately calls our custom handler with custom parameters.

        private async void button1_Click(object sender, EventArgs e)
        {
            await webView2.EnsureCoreWebView2Async(null);

            // Some parameter we want to pass to the handler
            string parameter = "some text";

            // Lambda function (with 2 params) subscribesNavigationCompleted event
            // Lambda function is calling our custom handler with 3 params
            webView2.NavigationCompleted += ((sender, e) => OnNavigationCompleted(sender, e, parameter));

            // Navigate to URL
            webView2.CoreWebView2.Navigate("https://baldsoft.com");
        }

        // Custom handler receives three parameters
        private void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e, string parameter)
        {
            if (e.IsSuccess)
            {
                MessageBox.Show($"Received parameter {parameter}");
            }
        }

Navigate and wait until given tag is displayed

Very often we can not rely on WebView2.NavigationCompleted only. This handler is called after body of page is loaded (body.onload has been raised). But sometimes we have to wait until some script loads next part of the page. Sometimes we can simply wait for some time, but that is not the best way how to solve this problem.

Better way is to check for presence of given tag. In the following example we modify WebView2.NavigationCompleted handler to check repeatedly for presence of some tag. The tag is defined by XPATH. WebView2 itself can not work with XPATH, but we can run JavaScript on the page and intercept result od JS code.

The code also includes a solution for the situation when the tag is not found even after a certain amount of time. This time is passed in the variable waitTime as the number of milliseconds that must elapse before we declare the attempt unsuccessful.

        private async void button1_Click(object sender, EventArgs e)
        {
            await webView2.EnsureCoreWebView2Async(null);

            // We want to wait till element <title> is found
            // Define XPATH to the element
            string xPath = @"//title";

            // How long time to wait for the element (in miliseconds)
            int waitTime = 30000;

            // Attach NavigationCompleted handler - with aditional parameters
            webView2.NavigationCompleted += ((sender, e) => OnNavigationCompleted(sender, e, xPath, waitTime));

            // Navigate to URL
            webView2.CoreWebView2.Navigate("https://baldsoft.com");
        }


        private async void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e, 
                                            string xPath, int waitTime)
        {
            if (e.IsSuccess)
            {
                // Presence of element (given by xPath) will be checked in loop
                // We will wait for some time in between
                int delayTime = 200;

                // Calculate maximal number of loops allowed
                int loopCounter = waitTime / delayTime;

                bool elementFound = false;
                
                while (!elementFound && loopCounter > 0)
                {
                    try
                    {
                        loopCounter--;

                        // Run JavaScript to check if the element is present
                        string script = $@"
                        (function() {{
                            var element = document.evaluate('{xPath}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
                            return element != null;
                        }})();";

                        var result = await webView2.CoreWebView2.ExecuteScriptAsync(script);

                        elementFound = bool.Parse(result);
                        
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show($"An error occurred: {ex.Message}");
                    }

                    if (!elementFound)
                    {
                        await Task.Delay(delayTime); // Wait for a short period before checking again

                    }
                }

                MessageBox.Show("Element found!");
            }
            // If e.IsSuccess is false, display message
            else MessageBox.Show($"Error loading page: {e.WebErrorStatus}");

        }

Navigate and wait synchronously until given tag is displayed

The above given approach has one disadvantage: the waiting routine (method OnNavigationCompleted) is event handler and the calling method is not informed about when the handler is finished or what the result was. Very often we want to do something like:

  1. Navigate to URL
  2. wait untill page is completely loaded and
  3. perform some action (e.g. count anchor tags <a> or something similar).

The examples we have presented so far do points 1 and 2. But the problem is to continue with step #3. Because we call OnNavigationCompleted and we have no signal, when this method is finished.

Sometimes we can get around the problem and add step 3 to step 2. It is, we can perform desired action (counting anchors) directly in step 2 (i.e. in the handler). But if the action in step 3 is complicated or if we want do to several steps, we need different approach. We need to call synchronously some method that will navigate to given URL and waits for specified tag. After this method is finished, it will (synchronously) return to calling method.

And here it is: Button.Click handler calls new method WaitForElementAsync. And this method does exactly what we need: Loads given URL, waits till XPATH is present and returns back. Note that we call await WaitForElementAsync, that is, next line is executed afterwards WaitForElementAsync is finished. And really: next line is MessageBox informing about result.

private async void button1_Click(object sender, EventArgs e)
{
    string xPath = @"//title";
    int waitTime = 10000;
    bool loaded = await WaitForElementAsync(xPath, waitTime);
    MessageBox.Show($"Waiting for {xPath}\nResult:{loaded}");
}

async Task<bool> WaitForElementAsync(string xPath, int waitTime)
{
    var tcs = new TaskCompletionSource<bool>();

    // Presence of element (given by xPath) will be checked in loop
    // We will wait for some time in between
    int delayTime = 100;

    // Calculate maximal number of loops allowed
    int loopCounter = waitTime / delayTime;

    // Attach handler for NavigationCompleted event
    webView2.NavigationCompleted += async (s, e) =>
    {
        while (true)
        {
            loopCounter--;

            string script = $@"
                (function() {{
                    var element = document.evaluate('{xPath}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
                    return element != null;
                }})();";

            // Run JavaScript and wait for result
            var resultScript = await webView2.ExecuteScriptAsync(script);
            bool result = bool.Parse(resultScript);

            // If tag given by XPATH was found, return true
            if (result == true)
            {
                tcs.SetResult(true);
                return;
            }
            // If tag was not found, return false
            else if (loopCounter == 0)
            {
                tcs.SetResult(false);
                return;
            }
            await Task.Delay(delayTime);    // Check every 100ms (delayTime)
        }
    };

    // Initialize webView2 and navigate
    await webView2.EnsureCoreWebView2Async();
    webView2.CoreWebView2.Navigate("https://baldsoft.com");

    return await tcs.Task; // Wait for the element to appear and return result
}

How does it work? The key are last three lines in WaitForElementAsync. We

  • initialize webView2 by calling EnsureCoreWebView2Async
  • navigate to desired URL and
  • wait synchronously (await) for TaskCompletionSource tcs.

Handler webView2.NavigationCompleted is attached via lambda function and it works in similar way as previous versions if this handler. The only (but crucial) difference is, that this handler signals result via TaskCompletionSource tcs.

Leave a Reply

Your email address will not be published. Required fields are marked *